Metadata-Version: 2.4
Name: DRMsgClient
Version: 0.1.1
Summary: 与 DataRelay 的 MsgServer（sharedmsg）通信的 Python 客户端
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: msgpack>=1.0

# msgclient（Python 版）

与 MsgServer（sharedmsg，默认监听 `0.0.0.0:5099`）通信的 Python 客户端。

包里有两个客户端，按需要挑一个用：

| 模块           | 类                | 说明                                                         |
| -------------- | ----------------- | ------------------------------------------------------------ |
| `client.py`    | `MsgClient`       | **推荐**。自带一份最小 MessagePack 编解码（`msgclient/_codec.py`），纯标准库；线程安全、断线自动重连、异常分类、有一组便捷查询方法 |
| `client2.py`   | `SharedMsgClient` | 极简封装。依赖 `msgpack` 包，只做「发一条 GET、把原始响应还给你」 |

线协议（4 字节大端长度前缀 + msgpack 消息体、长连接）见上一级的
[PROTOCOL.md](../PROTOCOL.md)。

## 安装 / 引用

```bash
pip install -e msgclient/python          # 从仓库根目录装成可编辑包
```

或者不安装，把 `msgclient/python` 加进 `sys.path` 就能 `import msgclient`；
也可以直接把 `msgclient/` 这个包目录拷进你的项目。

> 注意：`msgclient/__init__.py` 目前会无条件导入 `client2`，而 `client2` 顶层有
> `import msgpack`，所以环境里没装 `msgpack` 时 `import msgclient` 会整体
> `ModuleNotFoundError`（`MsgClient` 自己并不需要它）。`pip install` 会顺带装好
> msgpack，不用 `SharedMsgClient` 的话它只是个多余依赖。

## 一、MsgClient（推荐）

```python
from msgclient import MsgClient

# 默认连 127.0.0.1:5099，timeout 是读写的秒数
with MsgClient(timeout=5.0) as client:
    # 批量查：返回值与 key 一一对应，缺失的位置是 None
    client.get("symbols", ["600000", "不存在的key", "000001"])
    # -> ['中国平安', None, '平安银行']

    client.get_one("symbols", "600000")            # '中国平安'；查不到返回 None
    client.get_one("symbols", "x", default="-")    # 指定缺失时的默认值 -> '-'
    client.get_dict("symbols", ["600000", "x"])    # {'600000': '中国平安', 'x': None}
    client.try_get("没有这张map", ["a"])            # None（map 不存在）

    response = client.request("GET", "symbols", ["600000"])   # 原始响应
    response.ok        # True
    response.value     # ['中国平安']
```

以上返回值都是对着假服务端实测过的。

### 接口

| 方法                                          | 返回                        | 说明                                              |
| --------------------------------------------- | --------------------------- | ------------------------------------------------- |
| `MsgClient(host, port, timeout, connect=True)` |                             | 默认 `127.0.0.1:5099`，构造时即连接（`connect=False` 可延后） |
| `get(mapname, keys)`                          | `list[str \| None]`         | map 不存在时抛 `MsgServerError`                   |
| `try_get(mapname, keys)`                      | `list \| None`              | 同上，但 map 不存在时返回 `None`                  |
| `get_one(mapname, key, default=None)`         | `str \| None`               | 查单个 key，缺失或 map 不存在时返回 `default`     |
| `get_dict(mapname, keys)`                     | `dict[str, str \| None]`    | `{key: value}`                                    |
| `request(cmd, mapname, keys, retry=True)`     | `Response(ok, value)`       | 原始往返；`ok=false` 不抛异常，自己看 `ok`        |
| `connect()` / `close()`                       |                             | `close()` 可重复调用；也支持 `with`               |
| `address` / `connected`                       | `str` / `bool`              | `'127.0.0.1:5099'`、当前是否连着                  |

- `keys` 传单个字符串、列表、元组、任何可迭代对象都行（`get_dict` 一样）。
- 所有方法都有 `retry=True` 参数，见下面「自动重连」。

### 同一实例多线程共用

`MsgClient` 内部有一把锁，每个往返是原子的，可以直接给多个线程共用；
要更高并发就开多个实例（服务端每条连接一个 task，不介意多连）。

### 自动重连

连接被对端关掉时（比如应用里点了「停服务」再「启服务」），下一次请求会
自动重连并重试一次。不想要这个行为就传 `retry=False`。

### 异常

| 异常               | 含义                                                          |
| ------------------ | ------------------------------------------------------------- |
| `MsgConnectError`  | 连不上、连接断开、发送失败                                    |
| `MsgTimeout`       | 读写超时（同时继承内置 `TimeoutError`，`except OSError` 也能接住） |
| `MsgProtocolError` | 对端字节不符合协议（长度前缀错、msgpack 解不开、结构不对）     |
| `MsgServerError`   | 服务端明确回了 `ok=false`（map 不存在、cmd 不支持）            |

都继承自 `MsgClientError`。另外 `host` 为空、`port` 不在 1~65535、`keys` 里有非字符串，
会抛 `ValueError` / `TypeError`。

## 二、SharedMsgClient

`client2.py` 里的最小客户端，一个方法一次往返，不走 `MsgClient` 那一套封装。
依赖第三方 `msgpack` 包。

```python
from msgclient import SharedMsgClient

with SharedMsgClient("127.0.0.1", 5099) as client:
    client.get_data("symbols", "600000")             # [True, ['中国平安']]
    client.get_datas("symbols", ["600000", "nope"])  # [True, ['中国平安', None]]
    client.get_data("没有这张map", "a")               # [False, None]
```

| 方法                                       | 返回              | 说明                                     |
| ------------------------------------------ | ----------------- | ---------------------------------------- |
| `SharedMsgClient(host="127.0.0.1", port=5555, timeout=5.0)` |   | 构造时立即连接                           |
| `get_data(mapname, key)`                   | 原始响应          | 查单个 key，参数是字符串                 |
| `get_datas(mapname, keys)`                 | 原始响应          | 查一批 key，参数是 `list[str]`           |
| `close()`                                  |                   | 也支持 `with`                            |

注意几点：

- **默认端口是 5555**，和服务端的 5099 对不上，连真服务端要显式写 `port=5099`。
- 返回的是 msgpack 解出来的原始结构 `[ok, value]`，**不做任何包装**：缺失的 key 是
  `None`，map 不存在是 `[False, None]`，要不要当错误处理由你决定（`MsgClient.try_get`
  就是把这个判断做掉的版本）。
- 不做自动重连；没有锁，**同一个实例别给多个线程共用**，要多线程就每个线程一个实例。
- `timeout` 直接设在 socket 上，同时管连接和之后的读写；超时抛的是
  `socket.timeout`，不是 `MsgTimeout`。
- 单条消息有 1 MiB 上限，超过抛 `ValueError`（`MsgClient` 的同类上限是 64 MiB）。
- `get_data` / `get_datas` 转发的是 `{"cmd": "GET", "mapname": ..., "key": [...]}`，
  协议上与 `MsgClient` 完全一致，可以混用。

## 该用哪个

| 场景                                             | 选              |
| ------------------------------------------------ | --------------- |
| 正常写业务代码                                   | `MsgClient`     |
| 要异常分类、要自动重连、要多线程共用一个实例     | `MsgClient`     |
| 想省掉装 `msgpack`（`MsgClient` 只用标准库）     | `MsgClient`     |
| 要查大 value（超过 1 MiB）                       | `MsgClient`     |
| 只想原样拿到 `[ok, value]` 自己判断              | `SharedMsgClient` |
| 已有的小脚本 / 就是喜欢极简                      | `SharedMsgClient` |

## 兼容性

- Python 3.9+（`pyproject.toml` 里 `requires-python = ">=3.9"`；本机 3.9 实测通过）
- Windows / macOS / Linux 都能跑（只用 `socket` + `struct`，无平台相关代码）
- 一条连接最长消息体 64 MiB 上限保护（`MsgClient`），超过报 `MsgProtocolError`
