Metadata-Version: 2.5
Name: ys-data-client
Version: 0.2.0
Summary: YS 数据中心 Python SDK：可靠事件发布订阅与跨系统 RPC
Requires-Python: >=3.13
Requires-Dist: websockets<18,>=15
Description-Content-Type: text/markdown

# ys-data-client

YS 数据中心 Python SDK。业务系统只配置数据中心地址、端口和系统 token，即可通过装饰器完成两类跨系统能力：

| 需求                           | 机制  | SDK 用法                                    |
| ------------------------------ | ----- | ------------------------------------------- |
| 状态通知、待办、最终状态变化   | Event | `@client.event(...)` / `client.publish()`   |
| 短时查询、需要直接返回值的命令 | RPC   | `@client.rpc(...)` / `client.call()`        |
| 长耗时或高风险命令（付款、审批） | 组合 | RPC 返回“已受理 + 任务 ID”，再用 Event 通知最终状态 |
| 数据中心自己维护的公共数据（金蝶参考数据） | Event | 订阅 `kingdee.reference.snapshot` / `kingdee.reference.changed` |
| 数据中心提供的即时能力（如快照版本） | RPC   | `client.call(method, target=DATA_CENTER)`   |

SDK 封装了 `/system` WebSocket 协议、认证、订阅、方法注册、心跳、指数退避重连、请求关联和 ACK，业务代码不需要手工发送任何 JSON。

**完整接入说明见 [SDK 使用手册](docs/SDK使用手册.md)**：接入前要准备什么、事件与 RPC 的异常处理表、幂等要求、常见问题和三条红线。

## 安装

要求 Python 3.13+。

```text
uv add ys-data-client
```

开发阶段引用本地路径（可编辑安装）：

```toml
[project]
dependencies = ["ys-data-client"]

[tool.uv.sources]
ys-data-client = { path = "../ys_data_client", editable = true }
```

然后执行 `uv sync`，也可以直接 `uv add --editable ../ys_data_client`。

## 快速开始

```python
import asyncio
import os

from ys_data_client import EventContext, RpcBusinessError, RpcContext, YsDataClient

client = YsDataClient(
    url=os.environ.get("YS_DATA_URL", "localhost"),
    port=int(os.environ.get("YS_DATA_PORT", "9101")),
    token=os.environ["YS_DATA_TOKEN"],  # 凭证只放环境变量或密钥管理，不进代码和 Git
)


@client.event("demo.order.created")
async def on_order_created(payload: dict, context: EventContext) -> None:
    # 正常返回即自动 ACK；至少一次投递，业务必须按 context.event_id 幂等
    print("order created", payload, context.event_id)


@client.rpc("demo.inventory.reserve")
async def reserve_inventory(params: dict, context: RpcContext) -> dict:
    if params.get("quantity", 0) <= 0:
        raise RpcBusinessError("INVALID_QUANTITY", "数量必须大于 0", {"quantity": params.get("quantity")})
    return {"reserved": True, "reservation_id": f"RES-{params['order_id']}"}


async def main() -> None:
    async with client:
        await client.wait_until_ready(timeout=10)

        result = await client.call(
            "demo.inventory.reserve",
            target="ys-test02",
            params={"order_id": "ORDER-001", "quantity": 2},
            idempotency_key="order:ORDER-001:reserve",
        )
        print(result)  # 目标系统 handler 的真实返回值

        await client.publish(
            "demo.order.created",
            payload={"order_id": "ORDER-001"},
            event_id="order:ORDER-001:created",
        )


asyncio.run(main())
```

事件订阅和 RPC 方法由装饰器自动收集，认证成功后 SDK 自动发送 `rpc.register` 与 `subscribe`，重连后自动恢复。

## 挂载到应用生命周期

公司业务系统使用 `ys_base.server.AppServer` 时，把 SDK 挂到它的生命周期钩子即可，不需要 FastAPI lifespan：

```python
server = AppServer(settings, app_version=__version__)
server.on_started(client.start)   # 启动完成后连接数据中心
server.on_stopping(client.close)  # 停止前优雅断开
```

业务入口（`EventRouter` 的 WS 事件或 `server.add_router` 挂载的 HTTP 路由）里直接 `await client.call(...)` / `await client.publish(...)`。

仍在用裸 FastAPI 的项目改用：

```python
app = FastAPI(lifespan=client.lifespan(ready_timeout=5))
```

两种情况下的连接、重连、优雅关闭行为一致，细节见使用手册第 5 节。

## 开发

```text
uv sync
uv run pytest -q
uv run ruff check .
uv run pyright src
```

测试使用内置的最小协议模拟服务，不需要真实数据中心。
