oss-s3-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 OSS Server contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,458 @@
1
+ Metadata-Version: 2.4
2
+ Name: oss-s3-client
3
+ Version: 0.1.0
4
+ Summary: Official Python client SDK for the S3-compatible OSS server (SigV4, path-style, zero dependencies)
5
+ Author: OSS Server Team
6
+ License: Copyright (c) 2026 OSS Server contributors
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
25
+
26
+ Project-URL: Homepage, https://github.com/pddon/easy-oss-server
27
+ Project-URL: Repository, https://github.com/pddon/easy-oss-server
28
+ Project-URL: Documentation, https://github.com/pddon/easy-oss-server/tree/main/clients/python#readme
29
+ Project-URL: Issues, https://github.com/pddon/easy-oss-server/issues
30
+ Keywords: oss,s3,sigv4,object-storage,client
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.8
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: License :: OSI Approved :: MIT License
41
+ Classifier: Operating System :: OS Independent
42
+ Classifier: Topic :: Internet :: WWW/HTTP
43
+ Classifier: Topic :: System :: Archiving :: Backup
44
+ Requires-Python: >=3.8
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Dynamic: license-file
48
+
49
+ # oss-s3-client(Python)
50
+
51
+ Rust 版 OSS 服务端(S3 兼容协议)的官方 Python 客户端。零第三方运行时依赖,仅使用
52
+ `http.client`、`urllib`、`hmac`/`hashlib` 与 `xml.etree` 等标准库。
53
+
54
+ - Python 3.8+
55
+ - SigV4 header 签名 + 预签名 URL(GET/PUT)
56
+ - path-style 访问:`http://endpoint/bucket/key`
57
+ - 桶、对象、列表分页、复制、批量删除、分片上传与流式下载
58
+
59
+ > **跨语言统一契约**:本客户端的方法名在 Python 侧为 snake_case,参数顺序、参数含义、
60
+ > 返回值结构与 Rust / Java / JavaScript 客户端完全一致(见下方契约表)。
61
+
62
+ ## 安装
63
+
64
+ ```powershell
65
+ cd clients\python
66
+ python -m pip install -e .
67
+ ```
68
+
69
+ 不使用 pip 时,也可以直接把 `clients/python` 加入 `PYTHONPATH` 后 `import oss_client`。
70
+
71
+ ## 五行完成配置 → 上传 → 下载 → 预签名
72
+
73
+ ```python
74
+ from oss_client import OSSClient
75
+
76
+ client = OSSClient("http://127.0.0.1:8080", "app-id", "app-secret", bucket="demo")
77
+ client.put_object("demo", "hello.txt", b"hello oss")
78
+ data = client.get_object("demo", "hello.txt")
79
+ url = client.presign_get("demo", "hello.txt", expires_in=600)
80
+ ```
81
+
82
+ `access_key` 即管理 API 创建 App 时返回的 `app_id`,`secret_key` 即 `app_secret`。
83
+
84
+ ## 配置
85
+
86
+ ```python
87
+ from oss_client import OSSClient, OSSConfig
88
+
89
+ config = OSSConfig(
90
+ endpoint="http://127.0.0.1:8080", # 允许路径前缀,如 http://host:8080/api
91
+ access_key="<app_id>",
92
+ secret_key="<app_secret>",
93
+ region="us-east-1", # 任意值,仅参与签名
94
+ bucket="demo", # 默认桶,可被各 API 的 bucket 参数覆盖
95
+ timeout=30.0, # 单次操作超时(秒)
96
+ max_retries=3, # 可重试错误的重试次数(不含首次)
97
+ retry_backoff=0.5, # 指数退避基数(秒)
98
+ )
99
+ client = OSSClient(config)
100
+ ```
101
+
102
+ | 参数 | 说明 | 默认值 |
103
+ | --- | --- | --- |
104
+ | `endpoint` | 服务地址,`http(s)://host[:port][/prefix]`;允许路径前缀 | 必填 |
105
+ | `access_key` | App ID | 必填 |
106
+ | `secret_key` | App Secret | 必填 |
107
+ | `region` | 签名作用域中的 region,服务端不校验 | `us-east-1` |
108
+ | `bucket` | 默认桶名 | `None` |
109
+ | `timeout` | 连接/读取超时(秒) | `30.0` |
110
+ | `max_retries` | 重试次数,仅对 429/500/502/503/504 与网络错误生效 | `3` |
111
+ | `retry_backoff` | 指数退避基数(秒) | `0.5` |
112
+
113
+ `endpoint` 带路径前缀时,请求路径为 `<prefix>/<bucket>/<key>`,签名也使用完整路径;
114
+ 预签名 URL 同样包含前缀。
115
+
116
+ 客户端支持上下文管理器,退出时关闭连接:
117
+
118
+ ```python
119
+ with OSSClient("http://127.0.0.1:8080", "app-id", "app-secret", bucket="demo") as client:
120
+ client.put_object("demo", "a.txt", b"a")
121
+ ```
122
+
123
+ ## 统一契约(23 个方法)
124
+
125
+ 所有方法第一个参数都是 `bucket`(传 `None` 表示使用配置中的默认桶 `OSSConfig.bucket`;
126
+ 配置里也没有默认桶时抛 `ValueError` 并说明原因)。
127
+ 参数顺序固定,不提供关键字以外的变体。
128
+
129
+ | # | 方法签名 | 返回值 |
130
+ | --- | --- | --- |
131
+ | 1 | `create_bucket(bucket)` | 无 |
132
+ | 2 | `delete_bucket(bucket)` | 无 |
133
+ | 3 | `list_buckets()` | 桶列表,每项含 `name`、`created_at` |
134
+ | 4 | `head_bucket(bucket)` | 桶信息,含 `exists`、`region`(取 `x-amz-bucket-region`,缺失回退配置 region) |
135
+ | 5 | `bucket_exists(bucket)` | `bool` |
136
+ | 6 | `get_bucket_location(bucket)` | `str`,空值归一化为 `"us-east-1"` |
137
+ | 7 | `put_object(bucket, key, body, options)` | 含 `etag`、`size` |
138
+ | 8 | `upload_file(bucket, key, path, options)` | 含 `etag`、`size` |
139
+ | 9 | `get_object(bucket, key, range)` | `bytes` |
140
+ | 10 | `get_object_to_file(bucket, key, path, range)` | 无 |
141
+ | 11 | `head_object(bucket, key)` | 含 `size`、`etag`、`content_type`、`metadata`、`last_modified` |
142
+ | 12 | `object_exists(bucket, key)` | `bool` |
143
+ | 13 | `delete_object(bucket, key)` | 无 |
144
+ | 14 | `delete_objects(bucket, keys, quiet=False)` | 含 `deleted`、`errors` |
145
+ | 15 | `copy_object(source_bucket, source_key, dest_bucket, dest_key, metadata, metadata_directive)` | 含 `etag` |
146
+ | 16 | `list_objects_v2(bucket, prefix, delimiter, max_keys, continuation_token, start_after)` | 含 `items`、`common_prefixes`、`is_truncated`、`next_token` |
147
+ | 17 | `create_multipart_upload(bucket, key, content_type, metadata)` | `upload_id` 字符串 |
148
+ | 18 | `upload_part(bucket, key, upload_id, part_number, body)` | `etag` 字符串 |
149
+ | 19 | `complete_multipart_upload(bucket, key, upload_id, parts)` | `etag` 字符串 |
150
+ | 20 | `abort_multipart_upload(bucket, key, upload_id)` | 无 |
151
+ | 21 | `list_parts(bucket, key, upload_id)` | 含 `parts`、`is_truncated` |
152
+ | 22 | `presign_get(bucket, key, expires_in)` | `url` |
153
+ | 23 | `presign_put(bucket, key, expires_in)` | `url` |
154
+
155
+ `put_object` / `upload_file` 的 `options` 为字典,字段名四门语言一致:
156
+
157
+ | 字段 | 说明 | 默认 |
158
+ | --- | --- | --- |
159
+ | `content_type` | 对象 Content-Type | `None` |
160
+ | `metadata` | 自定义元数据字典 | `None` |
161
+ | `content_md5` | base64 字符串或 16 字节 MD5 摘要 | `None` |
162
+ | `part_size` | 自动分片阈值(字节) | `8 * 1024 * 1024` |
163
+ | `auto_multipart` | 超过 `part_size` 是否自动分片 | `True` |
164
+
165
+ ## API 用法
166
+
167
+ ### 桶
168
+
169
+ ```python
170
+ client.create_bucket("demo")
171
+ assert client.bucket_exists("demo")
172
+
173
+ info = client.head_bucket("demo") # BucketInfo
174
+ assert info.exists and info.region == "us-east-1"
175
+
176
+ assert client.get_bucket_location("demo") == "us-east-1"
177
+ print([(b.name, b.created_at) for b in client.list_buckets()])
178
+
179
+ client.delete_bucket("demo") # 桶非空时抛出 BucketNotEmptyError
180
+ ```
181
+
182
+ 桶不存在时 `head_bucket` 返回 `exists=False`,不抛异常;其它错误照常抛出。
183
+
184
+ ### 上传对象
185
+
186
+ ```python
187
+ from pathlib import Path
188
+
189
+ # bytes / 文本内容
190
+ client.put_object("demo", "notes.txt", b"hello", {"content_type": "text/plain"})
191
+ client.put_object("demo", "notes2.txt", "hello") # str 按内容处理
192
+
193
+ # 文件路径(Path;流式上传,不会整体读入内存)
194
+ client.put_object("demo", "big.bin", Path("big.bin"))
195
+
196
+ # 文件对象(流式)
197
+ with open("big.bin", "rb") as fp:
198
+ client.put_object("demo", "big3.bin", fp)
199
+
200
+ # 文件路径上传:四门语言同名同义
201
+ client.upload_file("demo", "big4.bin", "big.bin")
202
+
203
+ # 元数据 + Content-MD5
204
+ import hashlib
205
+ client.put_object(
206
+ "demo",
207
+ "meta.txt",
208
+ b"x",
209
+ {
210
+ "metadata": {"owner": "alice"},
211
+ "content_md5": hashlib.md5(b"x").digest(),
212
+ },
213
+ )
214
+ ```
215
+
216
+ `put_object` 的 `body` 为 `str` 时按内容处理;按路径上传请用 `pathlib.Path` 或
217
+ `upload_file`。`auto_multipart=True`(默认)时,超过 `part_size` 会自动走分片上传,
218
+ 与 `upload_file` 是同一条代码路径,行为一致(分片失败自动 abort)。
219
+
220
+ 自定义元数据在本地校验:key 必须符合 RFC 7230 token 字符集;value 只允许可打印 ASCII
221
+ (`0x20..0x7E`),Tab、CR、LF、NUL 等控制字符一律拒绝。非法时直接抛出 `ValueError`,
222
+ 不会发出请求。
223
+
224
+ #### Content-MD5 的三种情况
225
+
226
+ | 场景 | 发送内容 | 校验方 |
227
+ | --- | --- | --- |
228
+ | 单次 PUT(未超过 `part_size` 或 `auto_multipart=False`) | 整对象 Content-MD5 | 服务端校验 |
229
+ | 自动分片(超过 `part_size`) | **不发整对象 MD5**,每片上传前计算该片 MD5 并作为该分片 UploadPart 的 Content-MD5 | 服务端逐片校验 |
230
+ | `content_md5` 传入且请求体可重放(bytes、路径、可 seek 文件对象) | 无论单次 PUT 还是自动分片,都先做本地整对象校验;不匹配直接抛 `BadDigestError`(`status=None`,不发请求) | SDK 本地 |
231
+ | 不可重放的一次性流 + `content_md5` | 跳过本地整对象校验 | 服务端(仅单次 PUT) |
232
+
233
+ 分片上传失败(含某片 MD5 不匹配)会自动 abort,不残留 upload 与临时分片。
234
+
235
+ #### 流式输入规则
236
+
237
+ - **未知长度的流**(无法通过 `len()` / `tell()` 得到长度,或不可 seek):一律走单次 PUT
238
+ (流式发送),**不尝试分片**。
239
+ - **已知长度且可重放**(bytes、路径、可 seek 文件对象):超过 `part_size` 时才走自动分片;
240
+ 分片失败可重试。
241
+ - 不可重放的一次性流只尝试一次,不做重试。
242
+
243
+ ### 下载对象
244
+
245
+ ```python
246
+ data = client.get_object("demo", "notes.txt") # bytes
247
+ assert data == b"hello"
248
+
249
+ # 流式写入文件(路径可为 str 或 Path)
250
+ client.get_object_to_file("demo", "big.bin", "download.bin")
251
+
252
+ # Range:字符串或 (start, end) 元组,end=None 表示到末尾
253
+ client.get_object("demo", "big.bin", "bytes=0-1023")
254
+ client.get_object("demo", "big.bin", "bytes=-100")
255
+ client.get_object("demo", "big.bin", (0, 1023))
256
+ client.get_object_to_file("demo", "big.bin", "part.bin", (0, 1023))
257
+
258
+ # 元信息
259
+ head = client.head_object("demo", "notes.txt")
260
+ print(head.size, head.etag, head.content_type, head.metadata, head.last_modified)
261
+ ```
262
+
263
+ ### 删除、复制、批量删除
264
+
265
+ ```python
266
+ client.copy_object("demo", "source.txt", "demo", "dest.txt") # 默认 COPY
267
+
268
+ client.copy_object( # 传 metadata → REPLACE
269
+ "demo", "source.txt", "demo", "dest.txt",
270
+ metadata={"v": "2"},
271
+ )
272
+
273
+ client.delete_object("demo", "dest.txt")
274
+
275
+ result = client.delete_objects("demo", ["a.txt", "b.txt"]) # quiet 默认 False
276
+ assert not result.errors
277
+ assert sorted(result.deleted) == ["a.txt", "b.txt"]
278
+
279
+ assert not client.object_exists("demo", "never.txt")
280
+ ```
281
+
282
+ - `delete_objects` 超过 1000 个 Key 时自动分批(每批 1000 个),返回聚合后的
283
+ `deleted` 与 `errors`(`errors` 只包含失败项)。传入空 `keys` 时直接返回空结果
284
+ (`deleted` 与 `errors` 均为空),不抛异常、不发请求。
285
+ - `copy_object` 未显式指定 `metadata_directive` 时:`metadata` 非空按 `REPLACE`,
286
+ 未传或传入空 dict(`{}`)一律按 `COPY`。
287
+
288
+ ### 列表与分页遍历
289
+
290
+ ```python
291
+ page = client.list_objects_v2("demo", prefix="logs/", delimiter="/", max_keys=100)
292
+ print(page.is_truncated, page.next_token)
293
+ print([item.key for item in page.items], page.common_prefixes)
294
+
295
+ # 手动翻页
296
+ page2 = client.list_objects_v2("demo", prefix="logs/", max_keys=100,
297
+ continuation_token=page.next_token)
298
+
299
+ # 生成器自动翻页
300
+ for item in client.iter_objects_v2("demo", prefix="logs/", max_keys=100):
301
+ print(item.key, item.size, item.etag)
302
+
303
+ # 指定起点
304
+ client.list_objects_v2("demo", prefix="logs/", start_after="logs/2026-01-01.log")
305
+ ```
306
+
307
+ ### 分片上传
308
+
309
+ 低层 API(`upload_part` 与 `complete_multipart_upload` 都只返回 ETag 字符串):
310
+
311
+ ```python
312
+ upload_id = client.create_multipart_upload("demo", "big.bin")
313
+ etag1 = client.upload_part("demo", "big.bin", upload_id, 1, chunk1)
314
+ etag2 = client.upload_part("demo", "big.bin", upload_id, 2, chunk2)
315
+
316
+ parts = client.list_parts("demo", "big.bin", upload_id)
317
+ assert [p.part_number for p in parts.parts] == [1, 2]
318
+
319
+ final_etag = client.complete_multipart_upload(
320
+ "demo",
321
+ "big.bin",
322
+ upload_id,
323
+ [
324
+ {"part_number": 1, "etag": etag1},
325
+ {"part_number": 2, "etag": etag2},
326
+ ],
327
+ )
328
+ # 出错时:
329
+ # client.abort_multipart_upload("demo", "big.bin", upload_id)
330
+ ```
331
+
332
+ `parts` 元素可以是 `{"part_number": n, "etag": e}`、`(n, e)` 元组或 `PartResult`。
333
+ 提交前 SDK 会按 `part_number` 升序排序,不依赖调用方的传入顺序。
334
+
335
+ 高层便捷方法(默认阈值 8MB,超过自动分片;失败自动 abort):
336
+
337
+ ```python
338
+ client.upload_file("demo", "big.bin", "big.bin", {"part_size": 8 * 1024 * 1024})
339
+ ```
340
+
341
+ ### 预签名 URL
342
+
343
+ ```python
344
+ get_url = client.presign_get("demo", "report.pdf") # 默认 900 秒
345
+ put_url = client.presign_put("demo", "upload.bin", expires_in=600)
346
+ ```
347
+
348
+ 第三方可直接使用,无需签名:
349
+
350
+ ```python
351
+ import urllib.request
352
+
353
+ with open("upload.bin", "rb") as fp:
354
+ request = urllib.request.Request(put_url, data=fp.read(), method="PUT")
355
+ urllib.request.urlopen(request)
356
+
357
+ body = urllib.request.urlopen(get_url).read()
358
+ ```
359
+
360
+ `expires_in` 默认 900 秒,有效范围 1..604800 秒。
361
+
362
+ ## 错误处理
363
+
364
+ 所有异常都继承自 `OSSError`,统一暴露 `code`、`message`、`status`、`request_id`
365
+ 四个字段(网络层错误没有 HTTP 状态码,`status` 与 `request_id` 为 `None`):
366
+
367
+ ```python
368
+ from oss_client import OSSError
369
+
370
+ try:
371
+ client.get_object("demo", "not-exist.txt")
372
+ except OSSError as exc:
373
+ print(type(exc).__name__, exc.code, exc.message, exc.status, exc.request_id)
374
+ ```
375
+
376
+ 服务端错误抛出 `OSSServiceError` 的具体子类,关键属性:
377
+
378
+ - `code`:S3 错误码,如 `NoSuchKey`、`NoSuchBucket`、`AccessDenied`
379
+ - `message`:错误描述
380
+ - `status`(`status_code` 别名):HTTP 状态码
381
+ - `request_id`:服务端 RequestId
382
+ - `resource`:错误 XML 中的资源路径
383
+ - `operation`:触发错误的 SDK 操作(如 `PutObject`)
384
+ - `retryable`:该状态码是否会触发 SDK 自动重试
385
+
386
+ 常见映射:`NoSuchKeyError`(404)、`NoSuchBucketError`(404)、`NoSuchUploadError`(404)、
387
+ `BucketNotEmptyError`(409)、`BucketAlreadyOwnedByYouError`(409)、`AccessDeniedError`(403)、
388
+ `SignatureDoesNotMatchError`(403)、`InvalidAccessKeyIdError`(403)、`BadDigestError`(400)、
389
+ `PreconditionFailedError`(412)、`InvalidRangeError`(416)。
390
+
391
+ 行为约定:
392
+
393
+ | 场景 | 行为 |
394
+ | --- | --- |
395
+ | 404 | 不重试;对象操作抛 `NoSuchKeyError`,桶操作抛 `NoSuchBucketError`;`object_exists` / `bucket_exists` 返回 `False`,`head_bucket` 返回 `exists=False` 的 `BucketInfo` |
396
+ | 403 | 不重试;按错误码抛 `AccessDeniedError` / `SignatureDoesNotMatchError` / `InvalidAccessKeyIdError` |
397
+ | Content-MD5 不一致 | 可重放体:SDK 本地抛 `BadDigestError`(`status=None`,不发请求);自动分片:服务端逐片校验返回 400 `BadDigest` |
398
+ | 429/5xx | 按 `max_retries` 与指数退避自动重试,耗尽后抛对应 `OSSServiceError` |
399
+ | 超时 | 自动重试,耗尽后抛 `OSSTimeoutError`(`code="TimeoutError"`),`attempts` 为实际尝试次数 |
400
+ | 网络错误 | 自动重试,耗尽后抛 `OSSClientError`(`code="NetworkError"`),`attempts` 为实际尝试次数 |
401
+
402
+ 重试策略统一:只有可重放的请求体(bytes、文件路径、可 seek 文件对象)才重试;
403
+ 不可重放的一次性流一律只尝试一次。预签名 URL 过期由服务端返回 403 `AccessDenied`。
404
+
405
+ ## 已知限制
406
+
407
+ - 仅支持 path-style(`/bucket/key`),不支持 virtual-hosted-style(`bucket.host/key`)。
408
+ - 服务端不校验 region,`region` 仅参与签名;签名作用域固定为 `s3`。
409
+ - 预签名 URL 只签名 `host`,PUT 链接不绑定 `Content-Type` 等请求头。
410
+ - bytes/str 请求体使用真实 `x-amz-content-sha256`;文件对象走流式上传时使用
411
+ `UNSIGNED-PAYLOAD`(服务端不校验 body 哈希)。
412
+ - 自动分片时不发送整对象 Content-MD5,改为逐片发送分片 MD5;不可重放的一次性流
413
+ 无法做本地整对象 MD5 校验。
414
+ - 返回的自定义元数据 Key 统一为小写(服务端行为)。
415
+ - 桶删除为软删除(进入回收站);删除后 `HeadBucket` 与 `GetBucketLocation` 一致返回
416
+ 404 `NoSuchBucket`(服务端已修复 `location` 分支的桶存在性校验)。
417
+ - `GetBucketLocation` 返回空字符串时,SDK 归一化为 `us-east-1`。
418
+ - 服务端 `ListParts` 一次性返回全部分片,SDK 未提供分页参数。
419
+ - 客户端复用单个 HTTP 连接,非线程安全;多线程场景请为每个线程创建独立客户端。
420
+ - 未支持的 S3 能力(ACL、版本控制、对象锁、加密等)以服务端协议文档
421
+ `docs/s3-compatible-api.md` 的“未兼容清单”为准。
422
+
423
+ ## 测试
424
+
425
+ 单元测试(AWS 官方 SigV4 测试向量、错误映射、重试/超时、分页,以及统一契约项:
426
+ 自动分片、批量分批、copy directive 推导、endpoint 路径前缀、元数据校验、
427
+ Content-MD5 校验):
428
+
429
+ ```powershell
430
+ cd clients\python
431
+ python -m unittest discover -s tests -p "test_unit.py" -v
432
+ python -m unittest discover -s tests -p "test_signer.py" -v
433
+ ```
434
+
435
+ 集成测试会自动拉起真实服务端,覆盖建桶、上传/下载、HEAD、分页、复制、批量删除、
436
+ 预签名 GET/PUT(含过期)、分片上传全流程、`put_object` 自动分片、Content-MD5 与
437
+ 失败自动 abort:
438
+
439
+ ```powershell
440
+ python -m unittest discover -s tests -p "test_integration.py" -v
441
+ # 跳过集成测试:
442
+ $env:OSS_SKIP_INTEGRATION="1"
443
+ ```
444
+
445
+ 集成测试约定:
446
+
447
+ - 固定地址 `127.0.0.1:18103`,数据目录 `%TEMP%\oss-server-python-data`(每次启动前清空);
448
+ - 优先复用 `target/release/oss-server.exe`,当它比 `src/**/*.rs` 旧时自动改用临时目录
449
+ `%TEMP%\oss-server-build-py` 构建(设置 `CARGO_TARGET_DIR`,不与主构建抢锁);
450
+ - 也可用 `OSS_SERVER_BIN` 指定二进制;
451
+ - 启动后通过管理 API 创建临时 App 获取 `app_id/app_secret`;
452
+ - 测试结束会 terminate/kill 服务端进程,不留下占用端口的后台进程。
453
+
454
+ 运行全部测试:
455
+
456
+ ```powershell
457
+ python -m unittest discover -s tests -v
458
+ ```