Metadata-Version: 2.4
Name: agriopendata
Version: 0.3.0
Summary: agriopendata 数据集/模型上传与下载 CLI 与 SDK
License-Expression: MIT
Keywords: agriopendata,agriculture,opendata,dataset,upload,download,cli
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Dynamic: license-file

# agriopendata — agriopendata 数据集/模型上传与下载工具（CLI + SDK）

面向 agriopendata 网站开发的上传/下载组件，已发布至 PyPI。**不含任何 openxlab 代码痕迹**：命名规则、包结构、异常体系、上传/下载器均为独立设计（上传的分层结构参考 openxlab 的 `Uploader`，接口契约则完全按本仓库 `config.py` 的 `ENDPOINTS` 实现）。

- 安装：`pip install agriopendata`
- 命令：`aod`
- 包：`agriopendata`（SDK：`import agriopendata`）
- 语言：Python ≥ 3.9，依赖仅 `requests`

## 安装与使用

```bash
pip install agriopendata

# 登录（AK/SK 从平台账号信息获取）
aod login <AK> <SK>

# 查看/切换环境（dev / staging / prod）
aod config                 # 查看当前
aod config staging         # 切换

# 数据集
aod search -k 小麦
aod ls 42 -s raw/2024
aod pull 42                          # 整个数据集
aod pull 42 -s raw/2024 -t ./data    # 只下某个子目录
aod pull -n agri/wheat-ndvi-2024     # 按数据集名 owner/datasName 下载

# 上传（文件 / 文件夹，自动识别，保留相对目录结构）
aod upload 42 ./local/raw.csv -t raw/2024          # 上传单个文件到 raw/2024/
aod upload 42 ./local/data -t raw                  # 上传整个文件夹（并发）
aod upload -n agri/wheat-ndvi-2024 ./local/raw.csv # 按数据集名 owner/datasName（与 id 二选一）

# 模型仓（按模型名 / 文件路径，缓存复用）
aod model pull owner/repo -m faster_rcnn -m yolov8 -o ./weights

# 任意直链：经服务端中转（riverpass）下载，失败自动回退
aod wget "https://example.com/large/file.zip" -o ./downloads
```

SDK 用法：

```python
import agriopendata

# 数据集
kind, local = agriopendata.download(dataset_id=42, source_path="raw", target_path="./data")
files = agriopendata.list_files(42, "raw")      # [{path, size, sha256}, ...]

# 上传（dataset_id 支持数字 id / o:数字 / owner/datasName）
info = agriopendata.upload_file(42, "./local/raw.csv", target_path="raw/2024")
infos = agriopendata.upload_folder(42, "./local/data", target_path="raw", workers=4)

# 模型
paths = agriopendata.download_model(
    "owner/repo", model_names=["faster_rcnn", "yolov8"], output="./weights"
)

# 任意直链（riverpass 中转 + 回退）
local = agriopendata.wget("https://example.com/file.zip", output="./downloads")
```

## Endpoint 配置（重点）

所有接口地址集中在 [agriopendata/config.py](agriopendata/config.py) 的 `ENDPOINTS` 注册表；环境域名在 `ENVIRONMENTS`。**对接真实后端时只改这一个文件**，业务代码（`portal.py` / `transfer.py`）无需改动。

- 环境：`aod config`（写 `~/.agriopendata/settings.json`）或 `AOD_ENV` 环境变量，默认 `prod`
- 基址：`AOD_BASE_URL` 环境变量可覆盖一切
- 凭证：`AOD_AK` / `AOD_SK` 环境变量 > `~/.agriopendata/credentials.json`。`aod login <AK> <SK>` 用 AK/SK 调后端登录接口换 `optoken` 并保存（`~/.agriopendata/token`）；之后所有 API 请求头带 `AOD_TOKEN`；token 缺失/临近过期时自动用 AK/SK 重新登录换取（服务端提前吊销时 401 后自动刷新重试一次）。`AOD_TOKEN` 环境变量可整体覆盖 token
- 缓存：`~/.agriopendata/cache/`
- 查看全部契约：`aod endpoints`

| 键名 | 方法 | 路径 | 参数 | 用途 |
|---|---|---|---|---|
| `login` | GET | `/api/v1/me` | AK/SK 头（`X-Access-Key-Id` / `X-Secret-Access-Key`） | AK/SK 换 `optoken`（响应 `{optoken, expires_in, ...}`） |
| `search_datasets` | GET | `/api/v1/datasets` | `keyword` `category` `page` `page_size` | 检索数据集 |
| `dataset_detail` | GET | `/api/v1/datasets/{dataset_id}` | — | 元信息 |
| `dataset_by_name` | GET | `/api/v1/datasets/{owner}/{datas_name}` | — | 按 owner/datas_name 反查数据集（`aod pull -n`） |
| `list_files` | GET | `/api/v1/datasets/{dataset_id}/files` | `prefix` `cursor` `limit` `detail` | 文件清单（分页） |
| `ticket` | GET | `/api/v1/datasets/{dataset_id}/ticket` | `path` | 下载前置校验（鉴权/配额/表单） |
| `resolve` | GET | `/api/v1/datasets/{dataset_id}/blob/main/{file_path}` | — | 解析文件直链（302 或 JSON） |
| `model_file_urls` | POST | `/api/v1/model-repos/{owner}/{repo}/file-urls` | body: `model_names` `file_paths` `ignore` | 模型仓批量取直链 |
| `riverpass_fetch` | GET | `/api/v1/riverpass/fetch` | `url` | 中转下载入口（代理抓取任意直链） |
| `upload_url` | GET | `/api/v1/datasets/{dataset_id}/upload-url` | `path` `expires` | 取预签名 PUT 直链（对象存储直传） |
| `upload_file` | PUT | `/api/v1/datasets/{dataset_id}/files/{file_path}` | body: 文件字节 | 代理上传（平台写存储并登记仓单，返回 sha256） |

### 下载链路

```
数据集:  list_files(42, prefix) → ticket(42, path) → resolve(42, path) → transfer.download_file
模型仓:  model_file_urls(owner, repo, model_names) → 逐文件 → 落缓存 → 软链/复制到输出
任意直链: riverpass_fetch(url) → 失败回退原始 url → transfer.download_file → md5 缓存复用
```

### 上传链路（参考 openxlab `Uploader` 分层的两段化实现）

```
单个文件:  本地 size/sha256 → portal.upload_url（预签名直链）→ transfer.upload_file PUT 字节直传 MinIO
           └── 取直链失败（如内存 mock 的 503）→ 回退 portal.upload_file 代理上传（返回服务端 sha256，本地比对待校验）
文件夹:    os.walk 铺平相对路径（跳隐藏项/.DS_Store）→ 每文件一个任务 → ThreadPoolExecutor 并发 → 逐文件打印进度
```

上传的每一层职责与 openxlab 对应：本地信息预计算≈`FileInfo.get_size/calculate_sha256`，直链≈`pre_object_upload` 的 putUrl，PUT 字节≈`put_part`，代理登记+sha256 校验≈`post_object_upload`——只是把后两阶段合并成了 mock 契约里的一次代理 PUT。

### 参数约定（后端实现方须遵守）

0. `login` 返回 `{optoken, expires_in?}`；`expires_in`（秒）缺省时客户端不主动续期，靠 401 后自动刷新兜底。
1. `list_files` 返回 `{files: [{path, size, sha256}], has_more, next_cursor}`；`sha256` 缺省时跳过校验。
2. `ticket` 对需补用户信息/申请表的场景返回 **426**；无权 403、未登录 401。
3. `resolve` / 模型直链：优先 **302 Location**；非 30x 则返回 `{download_url, expires_in}`。直链需支持 HTTP `Range`，否则并发分片自动降级为单线程。
4. `model_file_urls` 返回 `{models: {name: {url, file_name, sha256}}, files: {path: {...}}}`，`size`/`Content-Length` 单位均为字节。
5. `riverpass_fetch` 返回 302 Location 或 `{download_url}`；客户端在中转不可用时自动回退原始直链。
6. `upload_url` 返回 `{upload_url, path, expires_in}`；`upload_url` 是对象存储预签名 PUT 直链，需带 `Content-Length`（requests 文件对象自动设置）。对象存储未启用时返回 **503**，客户端回退代理上传。
7. `upload_file`（代理上传）返回 `{path, size, sha256, stored}`；`sha256` 由服务端对接收到的字节计算，客户端用本地预计算的 sha256 比对校验。上传后该文件立即可被 `list_files`/`ticket`/`resolve`/`blob` 访问。

## 文件结构

```
agriopendata/
  __init__.py    SDK 入口 + CLI（aod：pull/ls/search/login/upload/model/wget/config/endpoints）
  config.py      ★ endpoint 注册表 + 多环境预设（唯一需要改的配置点；upload_url/upload_file 契约已内置）
  auth.py        凭证存取（env / 本地文件）
  portal.py      平台交互（清单/票/直链/模型直链/中转/上传直链/代理上传）
  upload.py      上传编排（参考 openxlab Uploader：本地预计算 → 预签名直传/代理上传 → sha256 校验）
  transfer.py    字节传输（下载：分片并发/断点续传/sha256；上传：流式 PUT 到直链）
  errors.py      异常体系
  utils.py       重试/容量/sha256
  __main__.py    python -m agriopendata
pyproject.toml   打包配置与 `aod` 入口
LICENSE          MIT
```

## 发布到 PyPI

```bash
# 1. 构建
pip install build twine
python -m build

# 2. 上传（需要 PyPI 账号，或在 pypi.org 配置 API token）
twine upload dist/*

# 3. 验证
pip install agriopendata
aod --version
python -c "import agriopendata; print(agriopendata.__version__)"
```

每次发布记得在 `pyproject.toml` 中递增 `version`。

## 本地开发

```bash
pip install -e .     # 或 pip install -e .[dev]
aod pull 42 -s raw -t ./data
```

## 说明

- 分片下载用 `.aod_cache_<文件名>/` 存临时块，下载完成自动拼接清理；中断后重跑自动续传。
- 模型下载走本地缓存：命中即软链（Windows 上自动降级为复制），不重复下载。
- 大文件阈值：单文件 ≤ 8MB 不分片。
- 网络类异常指数退避重试 5 次。
