当前 woofi-cli 已有清晰的 5 层架构(api → services → schemas → utils → constants),核心业务逻辑与 CLI 表现层分离良好。目标是将核心层提取为独立的 woofi Python 包,供第三方应用集成 WOOFi,同时 CLI 改为依赖 SDK。
- SDK 占据最简短包名 —
from woofi import WOOFiClient,不带_sdk后缀 - 两层接口 — 底层 raw 方法返回贴近上游的 Pydantic models,高层 helpers 返回 normalized models;不用
raw=True开关 - SDK 不包含 CLI 契约 — envelope、exit codes、presenters 不进入 SDK;SDK 用 Python 异常暴露错误
- Semver 管理 — SDK 公共 API 的方法名、参数名、返回模型字段一旦发布按 semver 管理
- v1 范围 — 同步接口优先;
quote和build-txes不在 v1 范围内
woofi-cli/ # repo root
├── packages/
│ ├── woofi/ # SDK 包 (pip install woofi)
│ │ ├── pyproject.toml # deps: httpx, pydantic
│ │ ├── src/woofi/
│ │ │ ├── __init__.py # 导出 WOOFiClient + 所有公开模型 + 异常
│ │ │ ├── client.py # WOOFiClient 门面类
│ │ │ ├── exceptions.py # SDK 异常层级
│ │ │ ├── constants.py # ← 从 woofi_cli 移入
│ │ │ ├── api/ # ← 从 woofi_cli/api/ 移入(内部模块)
│ │ │ ├── services/ # ← 从 woofi_cli/services/ 移入(内部模块)
│ │ │ ├── schemas/ # ← 从 woofi_cli/schemas/ 移入(公开模型)
│ │ │ └── utils/ # ← 从 woofi_cli/utils/ 移入 (decimals, time)
│ │ └── tests/
│ │ ├── test_client.py # WOOFiClient 集成测试
│ │ ├── test_services/ # ← 从 tests/test_services/ 移入
│ │ └── test_utils.py # ← 从 tests/test_utils.py 移入
│ └── woofi-cli/ # CLI 包 (pip install woofi-cli)
│ ├── pyproject.toml # deps: woofi, click, rich
│ ├── src/woofi_cli/
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── cli_context.py
│ │ ├── exit_codes.py
│ │ ├── commands/
│ │ ├── presenters/
│ │ └── schemas/common.py # SuccessEnvelope/ErrorEnvelope (CLI 专用)
│ └── tests/
│ ├── conftest.py
│ └── test_commands/
├── pyproject.toml # workspace 根配置 (开发用)
├── CLAUDE.md
└── README.md
from woofi import WOOFiClient
with WOOFiClient(timeout=30) as client:
raw_stats = client.get_stats(period="1d", network="arbitrum") # -> StatResponseRaw
raw_sources = client.get_source_stats(period="1w", network="bsc") # -> SourceStatResponseRaw
raw_yield = client.get_yield(network="arbitrum") # -> YieldResponseRaw
raw_staking = client.get_staking() # -> StakingResponseRawwith WOOFiClient(timeout=30) as client:
# Stats
series = client.get_stat_series(period="1d", network="arbitrum") # -> list[StatBucket]
summary = client.get_stat_summary(period="1d", network="arbitrum") # -> StatSummary
sources = client.list_sources(period="1w", network="bsc", limit=5) # -> list[SourceStat]
top = client.get_top_source(period="1d", network="arbitrum") # -> SourceStat | None
# Yield
vaults = client.list_vaults(network="arbitrum", symbol="USDC") # -> list[YieldVault]
vault = client.get_vault(network="arbitrum", address="0x...") # -> YieldVault | None
ysummary = client.get_yield_summary(network="arbitrum") # -> YieldSummary
# Staking
staking = client.get_staking_info() # -> StakingInfo接口原则:
- SDK 参数保留当前 CLI 的命名,降低迁移和认知成本
- SDK 返回 typed Pydantic model,不返回 CLI envelope
- CLI 风格的
raw=True开关不进入 SDK;通过显式区分 raw method (get_stats) 和 helper method (get_stat_series) 实现
WOOFiError # 基础异常
├── WOOFiHTTPError # 上游返回非 2xx (含 status_code, response_body)
├── WOOFiTimeoutError # 请求超时
├── WOOFiPayloadError # 上游响应 JSON 解析失败
└── WOOFiValidationError # Pydantic 模型校验失败CLI 负责把这些异常映射为现有 exit codes 和 stderr JSON envelope。
- 创建
packages/woofi/目录结构及pyproject.toml- name:
woofi, deps:httpx>=0.27,pydantic>=2.0 - build backend: hatchling
- name:
- 将以下模块从
src/woofi_cli/移动到packages/woofi/src/woofi/:api/(client.py, stats_api.py, source_stats_api.py, yield_api.py, staking_api.py)services/(stats_service.py, yield_service.py, staking_service.py)schemas/(stats.py, source_stats.py, yield_schema.py, staking.py) — 不含 common.pyutils/decimals.py,utils/time.pyconstants.py
- 更新所有移动文件内部 import:
woofi_cli.xxx→woofi.xxx - 移动对应测试到
packages/woofi/tests/
- 创建
packages/woofi/src/woofi/exceptions.py— 四类异常 (HTTP/Timeout/Payload/Validation) - 创建
packages/woofi/src/woofi/client.py:__init__(self, timeout=30, base_url=API_BASE_URL)— lazy httpx.Client__enter__/__exit__上下文管理器- 底层方法:
get_stats(),get_source_stats(),get_yield(),get_staking()— 调用 api 层,返回 raw models - 高层 helpers:
get_stat_series(),get_stat_summary(),list_sources(),get_top_source(),list_vaults(),get_vault(),get_yield_summary(),get_staking_info()— 调用 api 层 + service 层,返回 normalized models _call()内部方法统一捕获 httpx/json/pydantic 异常 → 转为 WOOFiError 子类
- 为 service 返回值创建新的 Pydantic 模型:
StatSummary— 聚合统计汇总YieldSummary— 收益汇总- 给 Normalized 模型添加简短别名导出 (
StatBucket,SourceStat,YieldVault,StakingInfo)
- 创建
__init__.py导出所有公开 API
- 创建
packages/woofi-cli/目录结构及pyproject.toml- deps:
woofi,click>=8.1,rich>=13.0
- deps:
- 将 CLI 层代码移入:
main.py,cli_context.py,exit_codes.py,commands/,presenters/,schemas/common.py - 更新 CLI commands 的 import 指向
woofi - 简化命令层错误处理: 统一捕获
WOOFiError子类 → exit code + error envelope - CLI 的
--raw模式改为调用 SDK 底层 raw 方法 - 移动 CLI 测试到
packages/woofi-cli/tests/ - 更新根
pyproject.toml为开发 workspace 配置
- SDK 测试 (
packages/woofi/tests/):test_client.py: 用 respx mock HTTP,测试全部 raw 方法和 helper 方法、返回类型、异常映射test_services/: 现有 service 测试(更新 import)test_utils.py: 现有工具测试
- CLI 测试 (
packages/woofi-cli/tests/):- 现有 command 测试更新 mock target 为 SDK 路径
- 增加 CLI 适配测试: SDK 成功时保持 JSON envelope,SDK 抛错时保持 exit code + 错误输出
- 第三方接入验证:
- 服务端 import SDK 并直接拿 typed result,不依赖 Click、presenter、CLI envelope
- 文档:
- README 增加 "Python SDK quick start" 和 "CLI quick start" 两段
- 提供最少可用示例: 同步调用、raw vs normalized、错误处理
- 确保两个包各自
pytest全部通过
| 当前路径 | 用途 | 目标 |
|---|---|---|
src/woofi_cli/api/client.py |
HTTP 工厂 | SDK: 融入 WOOFiClient |
src/woofi_cli/api/stats_api.py |
fetch_stat() | SDK: api/ |
src/woofi_cli/services/stats_service.py |
最大 service 模块 | SDK: services/ |
src/woofi_cli/schemas/common.py |
Envelope 模型 | CLI 保留 |
src/woofi_cli/commands/stats.py |
最大 command 模块 | CLI: commands/ |
src/woofi_cli/constants.py |
URL/网络/周期常量 | SDK: constants.py |
src/woofi_cli/utils/errors.py |
WoofiCLIError | 替换为 SDK exceptions |
cd packages/woofi && pytest— 全部通过cd packages/woofi-cli && pytest— 全部通过pip install -e packages/woofi && python -c "from woofi import WOOFiClient; print('OK')"— 成功pip install -e packages/woofi-cli && woofi stats series --period 1d --network arbitrum --format json— 输出正常mypy packages/woofi/src— 无错误- 第三方场景验证 — 纯 SDK import 不触发任何 click/rich 依赖
- v1 只做同步接口;如有高并发需求再补
AsyncWOOFiClient quote和build-txes不在 SDK v1 范围内