Metadata-Version: 2.4
Name: irepo-artifact
Version: 0.1.1
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.1.1'
```

源码开发安装：

```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。
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-*` 中的备份供恢复。

## 服务端前提与兼容

需要 AP 支持双下载链接协议。文件 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,        # 唯一超时参数；正数秒，控制面与下载共用
    layout="ap_export",  # 对齐 AP 的共同根规则；无匹配时成功返回空 output
)
```

`headers` 仅用于 AP 请求，显式 api_key/cluster 覆盖同名头，不传播到签名 URL。
默认 layout="sdk" 保留旧版 SDK 的无匹配异常及共同根行为。
`timeout` 是控制面与下载共用的 socket 等待上限，不是整个下载总时限。
当前实现仅请求旧 manifest 接口 `/artifacts-manifest?require_file_download=true`，不探测新接口、不轮询 202。
从 0.1.0.dev3 起仅使用旧接口；0.1.0.dev2 采用新接口优先、旧接口回退。

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 权限。
