Metadata-Version: 2.4
Name: irepo-artifact
Version: 0.2.2
Summary: Standalone HTTP artifact downloads
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# irepo-artifact

Python 3.10+ 的独立产物下载包。通过 AP 获取链接后 HTTP 下载，不依赖
`instance-repo`、`ap-client`、`oss2` 或 STS。完整 SDK 与未来的 ap-client
均可单向依赖此包。

安装发布版本：

```bash
python -m pip install 'irepo-artifact==0.2.2'
```

源码开发安装：

```bash
python -m pip install ./sdk/python-artifact
```

## 使用

```python
from irepo_artifact import ArtifactClient

client = ArtifactClient(
    ap_endpoint="https://ap.example.invalid",  # 根地址，不带 /api
    api_key=api_key,
    cluster=cluster,
)
archive = client.download("job-a", output_dir="out/job-a")
expanded = client.download("job-b", output_dir="out/job-b", extract=True)
selected = client.download(
    "job-c", output_dir="out/job-c",
    include_files=["logs", "result.json"], exclude_files=["debug.log"],
)
batch = client.download_many(["job-d", "job-e"], output_dir="out", max_concurrent=8)
group = client.download_group("group-a", output_dir="group-out")
```

默认保存完整 `result.tgz`，不解压。`extract=True` 展开到 `artifacts/output/`，
保留压缩包。单 Job 的 output_dir 就是 Job 目录；批量与 Group 写入
`<output_dir>/jobs/<job_id>/`。不要让不同单 Job 共用一个输出目录。

单 Job 成功返回 `DownloadResult`，失败抛出 `ArtifactError` 子类。批量返回
`dict[job_id, DownloadResult]`，默认 8 个 Job 并发，重复 Job 去重；各 Job
状态为 `success`、`missing`、`no_match` 或 `failed`，一项失败不阻止其他项。
结果提供实际 `archive_path`、`files_dir`、`files` 和 `channel`，调用方无需猜测路径。

## 文件筛选

先读取完整 manifest，include 取并集，再排除 exclude。只传 exclude 时从全部文件中排除；
两个列表都为空时下载完整包。带 `/` 的选择器匹配相对路径或目录前缀，不带 `/` 则匹配
任意层级的完整路径段；不支持 glob。SDK 的每个列表元素是一个选择器，不拆分逗号。

筛选后向 AP 按每批最多 500 个原始路径获取签名链接，下载返回 URL；不自行构造 dataplane
地址。筛选与 `extract=True` 冲突，选中的压缩文件也按原样保存。无匹配文件时报告
`NoMatchingFiles`，不下载整包，不覆盖旧输出。按完整 manifest 识别并去掉唯一共同根目录，
目录条目不足时保留原路径；解压完整包也按 AP 风格归一化到 `artifacts/output/`。

## 传输与输出

完整包请求 `download_mode=both`，OSS 优先；只有 proxy 时直接使用 proxy。
未返回 `downloads`（或为 null/空对象）时使用旧版单 `url`，也支持 dataplane 地址。
单 URL 仅在 hostname 匹配标准 OSS 域名时标记 `channel='oss'`；其余返回
`channel=None`，表示无法从响应确认通道，不表示下载失败。

标准区域 OSS 公网域名会在下载和重定向时转换为对应 `-internal` endpoint，
保留路径、端口及签名查询参数；已有内网域名保持不变。内网 OSS 失败不尝试原公网
地址，有 proxy 链接时仍可回退 proxy。自定义域名和加速域名保持原样，不属于本转换规则。
返回的原始响应保留服务端 URL，不因实际下载地址转换而修改。
OSS 网络、TLS 或 HTTP 下载失败时尝试可用的 proxy，并从头下载；401/403 最多重新签发一次，
429/5xx 同通道最多 3 次尝试后回退。OSS 链接刷新失败仍可使用已有 proxy 链接。
本地 IO 和解压错误不触发切换；不会关闭 TLS 校验。
HTTP 传输成功不代表归档内容有效：默认不解压时不会检查归档完整性；
开启解压后若发现归档损坏，会报错，但不会因此尝试另一通道。
默认 timeout=30 是 socket 阻塞等待上限，不是整个下载的总时长。
签名 URL 不携带 AP 身份头；错误信息不包含 URL 查询参数或响应正文。

输出路径及父目录不允许符号链接；系统临时目录若含符号链接别名，请显式选择解析后的真实路径。
已有 `result.tgz` 或 `artifacts/` 时默认拒绝，显式 `overwrite=True` 才替换这两个
受管理位置，其他用户文件保持不变。传输与解压使用暂存内容；失败保留旧结果。
覆盖成功后退役旧模式输出，例如筛选下载完成后删除旧压缩包。提交异常会尝试回滚；
多个路径替换不保证进程崩溃时的事务性。进程锁文件存放在当前用户的系统临时目录中，按输出路径隔离，
不会添加导出文件；锁文件不应手工删除。进程退出
（包括强制终止）后操作系统自动释放锁。回滚自身失败时保留 `.artifact-*` 中的备份供恢复。

## 服务端前提与兼容

整包兼容双通道 `downloads` 和旧版单 `url` 响应。文件 manifest／签发接口当前要求服务端 proxy 配置，
否则报告服务端错误；请确认目标服务已支持所需接口。API Key 只发往 AP。

权限由 AP artifact 接口统一校验，SDK 不再发起 Job 详情预检查。
旧 `IREPO_ARTIFACT_CHECK_JOB_ACCESS` 环境变量已删除，不再影响行为。

## 请求配置与 AP export 接入（0.1.0.dev2）

```python
client = ArtifactClient(
    ap_endpoint="https://ap.example.invalid",
    api_key=api_key, cluster=cluster,
    headers={"X-Custom": "value"},
    api_prefix="/api",  # 直连或 endpoint 已含 /api 时传空字符串
    timeout=30,        # 正数秒，控制面与下载共用的 socket 等待上限
    manifest_wait_timeout=1800,  # 每个 Job 获取 manifest 的累计等待预算
    layout="ap_export",  # 对齐 AP 的共同根规则；无匹配时成功返回空 output
)
```

`headers` 仅用于 AP 请求，显式 api_key/cluster 覆盖同名头，不传播到签名 URL。
默认 layout="sdk" 保留旧版 SDK 的无匹配异常及共同根行为。
`timeout` 是控制面与下载共用的 socket 等待上限，不是整个下载总时限。
0.2.0 优先请求 `/jobs/{job_id}/artifacts/manifest`，收到 `202 preparing` 后按 `Retry-After`
轮询（无效或缺省值使用 2 秒）。首次请求返回 404/405 时尝试旧接口
`/jobs/{job_id}/artifacts-manifest?require_file_download=true`；旧接口仍只接受 200。
权限错误、409、网络错误、429/5xx、无效响应及 preparing 后的失败均不触发协议回退。
manifest 请求的网络错误及 HTTP 408、429、500、502、503、504 最多尝试 3 次，其他状态不重试。
HTTP 退避从 0.5 秒开始翻倍、上限 2 秒；与服务端 Retry-After（秒数或 HTTP 日期）取较大值。
成功的 preparing 响应开启下一轮请求，202 的轮询等待规则保持不变。
筛选文件返回 401/403 时刷新当前批次的剩余链接和有效期，每个文件最多触发一次鉴权刷新。

`manifest_wait_timeout` 默认 1800 秒，必须是有限正数。每个 Job 独立计时，请求、轮询、重试、
退避和旧接口回退共用预算；不包含后续文件签名、下载及解压，也不是整个 Group 的总时限。
不读取 AP 专属环境变量。AP 集成可传入
`manifest_wait_timeout=client.config.artifact_manifest_wait_timeout`。

两条 manifest 路由均返回 404 时保留为协议失败，不能据此认定没有产物；有效响应的
`manifest: null` 才明确表示缺失。错误信息保留新旧接口状态，不包含响应正文。
0.1.2 及此前的 0.1.0.dev3 起版本仅请求旧接口。

DownloadResult 与 ArtifactError 均提供 `artifacts_response`、`manifest_response`，未取得对应
JSON 对象时为 None。原始对象保留未知字段，校验失败和产物缺失也可读取；批量失败结果提供同名字段。
链接刷新成功且通道可用时替换整份整包响应，刷新失败保留此前对象；文件 URL 刷新不覆盖 manifest。
这些字段不出现在默认 repr/str 中，调用方自行决定是否保存含签名链接的 JSON。

SDK 不写 artifacts.json 或 artifacts-manifest.json。AP 整包模式保存 artifacts_response；筛选模式
保存 manifest_response，并继续自行生成 `{"job_id": job_id, "url": None, "mode": "selected_files"}`。
筛选模式不会额外请求整包链接，artifacts_response 为 None。AP 仍负责 job.json、metrics.json、
缺失时保留/清理导出目录和 skipped 统计。示例：

```python
from irepo_artifact import ArtifactError
try:
    result = client.download("job-a", output_dir="out/job-a", extract=True)
    metadata = result.artifacts_response
except ArtifactError as error:
    metadata = error.artifacts_response
    # 调用方可保存 metadata，再依据异常类型决定跳过或报错。
    raise
```

单个下载通道返回 404，或错误正文前 64 KiB 含 `NoSuchKey`，表示该通道缺失。
OSS 缺失仍会尝试 proxy；只有所有可用下载通道均确认缺失时才报告 `ArtifactNotFound`。
若同时出现普通下载故障，则报错而非按缺失跳过。正文读取失败时保留原 HTTP 状态处理，异常不包含正文。

解压保留文件、目录的普通权限位和修改时间，不恢复归档属主或 setuid/setgid/sticky 位。
支持最终输出目录范围内的相对符号链接、硬链接，以及按归档顺序覆盖的重复文件；
拒绝越界链接和设备文件。`layout="ap_export"` 的筛选下载保留 AP 的 `artifacts` 目录 0700 权限。


## 调试日志（0.1.2 起）

`ArtifactClient(..., verbose=True)` 启用精简 stderr 日志；默认 `False`，实例间开关独立，
不修改 root logger，也不读取 AP 环境变量。AP 统一构造入口可传入
`verbose=client.config.verbose`，但须先升级到包含此参数的 SDK 版本（0.1.2.dev0 尚不支持）。

日志仅包含服务端请求方法和接口、Job/Group 标识、返回的通道及下载地址，以及请求失败的
HTTP 状态或错误类型；文件链接带文件路径，刷新链接沿用相同日志。
标准 OSS 地址转换时同一行附带 `internal_url`，表示转换后的候选地址，不表示已完成该通道下载。
日志中的下载地址（包括 `internal_url`）和服务端接口隐藏全部查询参数，显示为 `?<redacted>`。
完整整包链接仍保留在 `result.artifacts_response` 中，可由调用方显式读取用于 curl；SDK 不自动写入 JSON 文件。
不输出 AP 认证头、API Key、响应正文、下载进度或解压日志。
未知单 URL 的通道记录为 `unknown`。日志写入失败不改变下载结果。

```text
[irepo-artifact] job=j1 POST https://ap.example/api/jobs/artifacts
[irepo-artifact] job=j1 channel=proxy url=https://dp.example/result.tgz?<redacted> links_received
```

### OSS 内网短探测

整包下载同时取得 OSS 和 proxy 链接时，在首次 OSS 下载前对最终内网地址做 TCP 连接探测，
连接超时为 `min(0.5, timeout)` 秒，不发送 HEAD 或签名查询参数。
每次 `download()`、`download_many()`、`download_group()` 调用共享一次结果（假设同次调用的
OSS endpoint 一致），并发线程和后续批次不重复探测；调用结束即丢弃，没有跨调用缓存。
探测失败则直接尝试 proxy；探测成功仍使用原 `timeout` 下载 OSS，失败后保留原有 proxy 回退。
仅 OSS、仅 proxy 以及筛选下载不执行此探测。返回的原始元数据保持完整。

TCP 成功不保证 TLS/HTTP 下载成功；连接超时不包含 DNS 和所有候选地址的严格总耗时限制。
同次调用中网络恢复后不会重新探测，下一次调用重新判断。
