wz-opensdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wz_opensdk-0.1.0/PKG-INFO +104 -0
- wz_opensdk-0.1.0/README.md +92 -0
- wz_opensdk-0.1.0/pyproject.toml +25 -0
- wz_opensdk-0.1.0/setup.cfg +4 -0
- wz_opensdk-0.1.0/src/wz_opensdk.egg-info/PKG-INFO +104 -0
- wz_opensdk-0.1.0/src/wz_opensdk.egg-info/SOURCES.txt +26 -0
- wz_opensdk-0.1.0/src/wz_opensdk.egg-info/dependency_links.txt +1 -0
- wz_opensdk-0.1.0/src/wz_opensdk.egg-info/requires.txt +4 -0
- wz_opensdk-0.1.0/src/wz_opensdk.egg-info/top_level.txt +1 -0
- wz_opensdk-0.1.0/src/wzopensdk/__init__.py +13 -0
- wz_opensdk-0.1.0/src/wzopensdk/_internal/__init__.py +1 -0
- wz_opensdk-0.1.0/src/wzopensdk/_internal/http.py +207 -0
- wz_opensdk-0.1.0/src/wzopensdk/_internal/params.py +52 -0
- wz_opensdk-0.1.0/src/wzopensdk/_internal/signer.py +96 -0
- wz_opensdk-0.1.0/src/wzopensdk/callback.py +66 -0
- wz_opensdk-0.1.0/src/wzopensdk/charge.py +34 -0
- wz_opensdk-0.1.0/src/wzopensdk/client.py +71 -0
- wz_opensdk-0.1.0/src/wzopensdk/device.py +312 -0
- wz_opensdk-0.1.0/src/wzopensdk/endpoints.py +6 -0
- wz_opensdk-0.1.0/src/wzopensdk/errors.py +47 -0
- wz_opensdk-0.1.0/src/wzopensdk/invoice.py +84 -0
- wz_opensdk-0.1.0/src/wzopensdk/renew.py +61 -0
- wz_opensdk-0.1.0/tests/test_client.py +156 -0
- wz_opensdk-0.1.0/tests/test_contract_error.py +114 -0
- wz_opensdk-0.1.0/tests/test_device.py +128 -0
- wz_opensdk-0.1.0/tests/test_live_smoke.py +23 -0
- wz_opensdk-0.1.0/tests/test_service.py +32 -0
- wz_opensdk-0.1.0/tests/test_signer.py +87 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wz-opensdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 万桩开放平台 Python OpenSDK
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://gitee.com/wz-open-platform/wz-opensdk-python
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# wz-opensdk
|
|
14
|
+
|
|
15
|
+
万桩开放平台 Python SDK。覆盖 Apifox 项目 `wz-open` 中文目录下的公开 API(设备、充电、续费、开票、回调日志)。`inner/*` 内部接口不在本 SDK 中。SDK 只做 client,不解析平台推送到合作方的入站回调。
|
|
16
|
+
|
|
17
|
+
发行名:`wz-opensdk`。导入名:`wzopensdk`。Python **3.9+**。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install wz-opensdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
尚未发布到 PyPI 时,可从 Gitee 安装:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install git+https://gitee.com/wz-open-platform/wz-opensdk-python.git@v0.1.0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
本地开发:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
source .venv/bin/activate
|
|
36
|
+
pip install -e ".[dev]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 快速开始
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
from wzopensdk import WzOpenClient, Endpoints
|
|
44
|
+
from wzopensdk.device import DeviceNumRequest
|
|
45
|
+
|
|
46
|
+
client = WzOpenClient(
|
|
47
|
+
endpoint=Endpoints.PROD, # 测试环境用 Endpoints.TEST
|
|
48
|
+
app_id=os.environ["WZ_OPEN_APP_ID"],
|
|
49
|
+
app_secret=os.environ["WZ_OPEN_APP_SECRET"],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
device = client.device.find_one(DeviceNumRequest("18000001"))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
也可 `WzOpenClient.from_env()`(读 `WZ_OPEN_APP_ID` / `WZ_OPEN_APP_SECRET`,可选 `WZ_OPEN_ENDPOINT`,缺省测试环境)。
|
|
56
|
+
|
|
57
|
+
按域调用,方法不堆在 `WzOpenClient` 上:
|
|
58
|
+
|
|
59
|
+
- `client.device` 设备绑定、计费模式、状态、在线日志
|
|
60
|
+
- `client.charge.start` / `stop`
|
|
61
|
+
- `client.renew`
|
|
62
|
+
- `client.invoice`
|
|
63
|
+
- `client.callback.log` / `retry_finish_order`
|
|
64
|
+
|
|
65
|
+
默认请求超时 10s。
|
|
66
|
+
|
|
67
|
+
## 签名
|
|
68
|
+
|
|
69
|
+
SDK 按接入指引自动加 `app_id`、`timestamp`、`sign`(HMAC-SHA256 大写 hex)。调用方不要自己拼签,也不要传 `company_id`。金额单位为分。
|
|
70
|
+
|
|
71
|
+
契约快照:[../api/openapi.json](../api/openapi.json)(汽车桩 `saveMode`/`getMode` 在 SDK 中拆成独立方法;`/open/device/setting/sync` 属于 inner,本 SDK 不实现)。公开能力合计 40 条出站 API。
|
|
72
|
+
|
|
73
|
+
## 日志
|
|
74
|
+
|
|
75
|
+
使用标准库 `logging`,logger 名 `wzopensdk`。可传入自定义 logger。
|
|
76
|
+
|
|
77
|
+
成功请求的 info 日志会带 `method`、`path`;响应头有 `X-B3-TraceId` 时再带 `traceId`。失败时同一字段出现在 error 日志里,并可从 `WzOpenAPIError.trace_id` 读取。
|
|
78
|
+
|
|
79
|
+
## HTTP
|
|
80
|
+
|
|
81
|
+
可配 timeout(默认 10s)、retry_count(失败重试且每次重签)、retry_wait、user_agent、proxy_url,或直接传入 `httpx.Client`。
|
|
82
|
+
|
|
83
|
+
## 环境
|
|
84
|
+
|
|
85
|
+
| 环境 | Base URL |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| 测试 | `https://dev-api-open.wanzhuangkj.com` |
|
|
88
|
+
| 正式 | `https://api-open.wanzhuangkj.com` |
|
|
89
|
+
|
|
90
|
+
## 开发
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
python3 -m pytest -q
|
|
95
|
+
# 或 make test
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
现网验证(需要凭证):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export WZ_OPEN_APP_ID=...
|
|
102
|
+
export WZ_OPEN_APP_SECRET=...
|
|
103
|
+
make verify
|
|
104
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# wz-opensdk
|
|
2
|
+
|
|
3
|
+
万桩开放平台 Python SDK。覆盖 Apifox 项目 `wz-open` 中文目录下的公开 API(设备、充电、续费、开票、回调日志)。`inner/*` 内部接口不在本 SDK 中。SDK 只做 client,不解析平台推送到合作方的入站回调。
|
|
4
|
+
|
|
5
|
+
发行名:`wz-opensdk`。导入名:`wzopensdk`。Python **3.9+**。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install wz-opensdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
尚未发布到 PyPI 时,可从 Gitee 安装:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install git+https://gitee.com/wz-open-platform/wz-opensdk-python.git@v0.1.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
本地开发:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 -m venv .venv
|
|
23
|
+
source .venv/bin/activate
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import os
|
|
31
|
+
from wzopensdk import WzOpenClient, Endpoints
|
|
32
|
+
from wzopensdk.device import DeviceNumRequest
|
|
33
|
+
|
|
34
|
+
client = WzOpenClient(
|
|
35
|
+
endpoint=Endpoints.PROD, # 测试环境用 Endpoints.TEST
|
|
36
|
+
app_id=os.environ["WZ_OPEN_APP_ID"],
|
|
37
|
+
app_secret=os.environ["WZ_OPEN_APP_SECRET"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
device = client.device.find_one(DeviceNumRequest("18000001"))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
也可 `WzOpenClient.from_env()`(读 `WZ_OPEN_APP_ID` / `WZ_OPEN_APP_SECRET`,可选 `WZ_OPEN_ENDPOINT`,缺省测试环境)。
|
|
44
|
+
|
|
45
|
+
按域调用,方法不堆在 `WzOpenClient` 上:
|
|
46
|
+
|
|
47
|
+
- `client.device` 设备绑定、计费模式、状态、在线日志
|
|
48
|
+
- `client.charge.start` / `stop`
|
|
49
|
+
- `client.renew`
|
|
50
|
+
- `client.invoice`
|
|
51
|
+
- `client.callback.log` / `retry_finish_order`
|
|
52
|
+
|
|
53
|
+
默认请求超时 10s。
|
|
54
|
+
|
|
55
|
+
## 签名
|
|
56
|
+
|
|
57
|
+
SDK 按接入指引自动加 `app_id`、`timestamp`、`sign`(HMAC-SHA256 大写 hex)。调用方不要自己拼签,也不要传 `company_id`。金额单位为分。
|
|
58
|
+
|
|
59
|
+
契约快照:[../api/openapi.json](../api/openapi.json)(汽车桩 `saveMode`/`getMode` 在 SDK 中拆成独立方法;`/open/device/setting/sync` 属于 inner,本 SDK 不实现)。公开能力合计 40 条出站 API。
|
|
60
|
+
|
|
61
|
+
## 日志
|
|
62
|
+
|
|
63
|
+
使用标准库 `logging`,logger 名 `wzopensdk`。可传入自定义 logger。
|
|
64
|
+
|
|
65
|
+
成功请求的 info 日志会带 `method`、`path`;响应头有 `X-B3-TraceId` 时再带 `traceId`。失败时同一字段出现在 error 日志里,并可从 `WzOpenAPIError.trace_id` 读取。
|
|
66
|
+
|
|
67
|
+
## HTTP
|
|
68
|
+
|
|
69
|
+
可配 timeout(默认 10s)、retry_count(失败重试且每次重签)、retry_wait、user_agent、proxy_url,或直接传入 `httpx.Client`。
|
|
70
|
+
|
|
71
|
+
## 环境
|
|
72
|
+
|
|
73
|
+
| 环境 | Base URL |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| 测试 | `https://dev-api-open.wanzhuangkj.com` |
|
|
76
|
+
| 正式 | `https://api-open.wanzhuangkj.com` |
|
|
77
|
+
|
|
78
|
+
## 开发
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install -e ".[dev]"
|
|
82
|
+
python3 -m pytest -q
|
|
83
|
+
# 或 make test
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
现网验证(需要凭证):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
export WZ_OPEN_APP_ID=...
|
|
90
|
+
export WZ_OPEN_APP_SECRET=...
|
|
91
|
+
make verify
|
|
92
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wz-opensdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "万桩开放平台 Python OpenSDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
urls = { Homepage = "https://gitee.com/wz-open-platform/wz-opensdk-python" }
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = ["pytest>=8.0"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wz-opensdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 万桩开放平台 Python OpenSDK
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://gitee.com/wz-open-platform/wz-opensdk-python
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# wz-opensdk
|
|
14
|
+
|
|
15
|
+
万桩开放平台 Python SDK。覆盖 Apifox 项目 `wz-open` 中文目录下的公开 API(设备、充电、续费、开票、回调日志)。`inner/*` 内部接口不在本 SDK 中。SDK 只做 client,不解析平台推送到合作方的入站回调。
|
|
16
|
+
|
|
17
|
+
发行名:`wz-opensdk`。导入名:`wzopensdk`。Python **3.9+**。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install wz-opensdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
尚未发布到 PyPI 时,可从 Gitee 安装:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install git+https://gitee.com/wz-open-platform/wz-opensdk-python.git@v0.1.0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
本地开发:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
source .venv/bin/activate
|
|
36
|
+
pip install -e ".[dev]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 快速开始
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
from wzopensdk import WzOpenClient, Endpoints
|
|
44
|
+
from wzopensdk.device import DeviceNumRequest
|
|
45
|
+
|
|
46
|
+
client = WzOpenClient(
|
|
47
|
+
endpoint=Endpoints.PROD, # 测试环境用 Endpoints.TEST
|
|
48
|
+
app_id=os.environ["WZ_OPEN_APP_ID"],
|
|
49
|
+
app_secret=os.environ["WZ_OPEN_APP_SECRET"],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
device = client.device.find_one(DeviceNumRequest("18000001"))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
也可 `WzOpenClient.from_env()`(读 `WZ_OPEN_APP_ID` / `WZ_OPEN_APP_SECRET`,可选 `WZ_OPEN_ENDPOINT`,缺省测试环境)。
|
|
56
|
+
|
|
57
|
+
按域调用,方法不堆在 `WzOpenClient` 上:
|
|
58
|
+
|
|
59
|
+
- `client.device` 设备绑定、计费模式、状态、在线日志
|
|
60
|
+
- `client.charge.start` / `stop`
|
|
61
|
+
- `client.renew`
|
|
62
|
+
- `client.invoice`
|
|
63
|
+
- `client.callback.log` / `retry_finish_order`
|
|
64
|
+
|
|
65
|
+
默认请求超时 10s。
|
|
66
|
+
|
|
67
|
+
## 签名
|
|
68
|
+
|
|
69
|
+
SDK 按接入指引自动加 `app_id`、`timestamp`、`sign`(HMAC-SHA256 大写 hex)。调用方不要自己拼签,也不要传 `company_id`。金额单位为分。
|
|
70
|
+
|
|
71
|
+
契约快照:[../api/openapi.json](../api/openapi.json)(汽车桩 `saveMode`/`getMode` 在 SDK 中拆成独立方法;`/open/device/setting/sync` 属于 inner,本 SDK 不实现)。公开能力合计 40 条出站 API。
|
|
72
|
+
|
|
73
|
+
## 日志
|
|
74
|
+
|
|
75
|
+
使用标准库 `logging`,logger 名 `wzopensdk`。可传入自定义 logger。
|
|
76
|
+
|
|
77
|
+
成功请求的 info 日志会带 `method`、`path`;响应头有 `X-B3-TraceId` 时再带 `traceId`。失败时同一字段出现在 error 日志里,并可从 `WzOpenAPIError.trace_id` 读取。
|
|
78
|
+
|
|
79
|
+
## HTTP
|
|
80
|
+
|
|
81
|
+
可配 timeout(默认 10s)、retry_count(失败重试且每次重签)、retry_wait、user_agent、proxy_url,或直接传入 `httpx.Client`。
|
|
82
|
+
|
|
83
|
+
## 环境
|
|
84
|
+
|
|
85
|
+
| 环境 | Base URL |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| 测试 | `https://dev-api-open.wanzhuangkj.com` |
|
|
88
|
+
| 正式 | `https://api-open.wanzhuangkj.com` |
|
|
89
|
+
|
|
90
|
+
## 开发
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
python3 -m pytest -q
|
|
95
|
+
# 或 make test
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
现网验证(需要凭证):
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export WZ_OPEN_APP_ID=...
|
|
102
|
+
export WZ_OPEN_APP_SECRET=...
|
|
103
|
+
make verify
|
|
104
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/wz_opensdk.egg-info/PKG-INFO
|
|
4
|
+
src/wz_opensdk.egg-info/SOURCES.txt
|
|
5
|
+
src/wz_opensdk.egg-info/dependency_links.txt
|
|
6
|
+
src/wz_opensdk.egg-info/requires.txt
|
|
7
|
+
src/wz_opensdk.egg-info/top_level.txt
|
|
8
|
+
src/wzopensdk/__init__.py
|
|
9
|
+
src/wzopensdk/callback.py
|
|
10
|
+
src/wzopensdk/charge.py
|
|
11
|
+
src/wzopensdk/client.py
|
|
12
|
+
src/wzopensdk/device.py
|
|
13
|
+
src/wzopensdk/endpoints.py
|
|
14
|
+
src/wzopensdk/errors.py
|
|
15
|
+
src/wzopensdk/invoice.py
|
|
16
|
+
src/wzopensdk/renew.py
|
|
17
|
+
src/wzopensdk/_internal/__init__.py
|
|
18
|
+
src/wzopensdk/_internal/http.py
|
|
19
|
+
src/wzopensdk/_internal/params.py
|
|
20
|
+
src/wzopensdk/_internal/signer.py
|
|
21
|
+
tests/test_client.py
|
|
22
|
+
tests/test_contract_error.py
|
|
23
|
+
tests/test_device.py
|
|
24
|
+
tests/test_live_smoke.py
|
|
25
|
+
tests/test_service.py
|
|
26
|
+
tests/test_signer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wzopensdk
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""万桩开放平台 Python OpenSDK。"""
|
|
2
|
+
|
|
3
|
+
from wzopensdk.client import WzOpenClient
|
|
4
|
+
from wzopensdk.endpoints import Endpoints
|
|
5
|
+
from wzopensdk.errors import WzOpenAPIError, WzOpenConfigError, WzOpenError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Endpoints",
|
|
9
|
+
"WzOpenAPIError",
|
|
10
|
+
"WzOpenClient",
|
|
11
|
+
"WzOpenConfigError",
|
|
12
|
+
"WzOpenError",
|
|
13
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Sign, send, retry, and decode open-platform envelopes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Callable, TypeVar
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from wzopensdk._internal import params as paramx
|
|
13
|
+
from wzopensdk._internal import signer
|
|
14
|
+
from wzopensdk.errors import WzOpenAPIError, WzOpenError
|
|
15
|
+
|
|
16
|
+
CODE_SUCCESS = 1
|
|
17
|
+
HEADER_B3_TRACE_ID = "X-B3-TraceId"
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpInvoker:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
endpoint: str,
|
|
26
|
+
app_id: str,
|
|
27
|
+
app_secret: str,
|
|
28
|
+
timeout: float = 10.0,
|
|
29
|
+
retry_count: int = 0,
|
|
30
|
+
retry_wait: float = 0.2,
|
|
31
|
+
user_agent: str = "wz-opensdk/python",
|
|
32
|
+
proxy_url: str | None = None,
|
|
33
|
+
insecure_skip_verify: bool = False,
|
|
34
|
+
logger: logging.Logger | None = None,
|
|
35
|
+
http_client: httpx.Client | None = None,
|
|
36
|
+
clock: Callable[[], int] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.endpoint = endpoint.rstrip("/")
|
|
39
|
+
self.app_id = app_id
|
|
40
|
+
self.app_secret = app_secret
|
|
41
|
+
self.timeout = timeout if timeout > 0 else 10.0
|
|
42
|
+
self.retry_count = max(0, retry_count)
|
|
43
|
+
self.retry_wait = retry_wait if retry_wait > 0 else 0.2
|
|
44
|
+
self.user_agent = user_agent or "wz-opensdk/python"
|
|
45
|
+
self.log = logger or logging.getLogger("wzopensdk")
|
|
46
|
+
self.clock = clock or (lambda: int(time.time()))
|
|
47
|
+
if http_client is not None:
|
|
48
|
+
self.http = http_client
|
|
49
|
+
self._owns_client = False
|
|
50
|
+
else:
|
|
51
|
+
self.http = httpx.Client(
|
|
52
|
+
timeout=self.timeout,
|
|
53
|
+
proxy=proxy_url,
|
|
54
|
+
verify=not insecure_skip_verify,
|
|
55
|
+
)
|
|
56
|
+
self._owns_client = True
|
|
57
|
+
|
|
58
|
+
def invoke(
|
|
59
|
+
self,
|
|
60
|
+
method: str,
|
|
61
|
+
path: str,
|
|
62
|
+
operation: str,
|
|
63
|
+
incoming: Any,
|
|
64
|
+
decode: Callable[[Any], T],
|
|
65
|
+
) -> T:
|
|
66
|
+
business = paramx.to_params(incoming)
|
|
67
|
+
wait = self.retry_wait
|
|
68
|
+
attempts = self.retry_count + 1
|
|
69
|
+
last: WzOpenError | None = None
|
|
70
|
+
for attempt in range(attempts):
|
|
71
|
+
if attempt > 0:
|
|
72
|
+
time.sleep(wait)
|
|
73
|
+
wait *= 2
|
|
74
|
+
try:
|
|
75
|
+
return self._attempt_once(method, path, operation, business, decode)
|
|
76
|
+
except WzOpenError as exc:
|
|
77
|
+
last = exc
|
|
78
|
+
if not self._should_retry(exc) or attempt == attempts - 1:
|
|
79
|
+
raise
|
|
80
|
+
raise last or WzOpenError("request failed")
|
|
81
|
+
|
|
82
|
+
def invoke_map(self, method: str, path: str, operation: str, incoming: Any) -> dict[str, Any] | None:
|
|
83
|
+
return self.invoke(method, path, operation, incoming, lambda data: data)
|
|
84
|
+
|
|
85
|
+
def invoke_void(self, method: str, path: str, operation: str, incoming: Any) -> None:
|
|
86
|
+
self.invoke(method, path, operation, incoming, lambda data: data)
|
|
87
|
+
|
|
88
|
+
def _attempt_once(
|
|
89
|
+
self,
|
|
90
|
+
method: str,
|
|
91
|
+
path: str,
|
|
92
|
+
operation: str,
|
|
93
|
+
business: dict[str, Any],
|
|
94
|
+
decode: Callable[[Any], T],
|
|
95
|
+
) -> T:
|
|
96
|
+
payload = dict(business)
|
|
97
|
+
payload["app_id"] = self.app_id
|
|
98
|
+
payload["timestamp"] = self.clock()
|
|
99
|
+
payload["sign"] = signer.sign(payload, self.app_secret)
|
|
100
|
+
|
|
101
|
+
method = method.upper()
|
|
102
|
+
url = self.endpoint + path
|
|
103
|
+
headers = {
|
|
104
|
+
"Accept": "application/json",
|
|
105
|
+
"User-Agent": self.user_agent,
|
|
106
|
+
}
|
|
107
|
+
request_kwargs: dict[str, Any] = {
|
|
108
|
+
"headers": headers,
|
|
109
|
+
"timeout": self.timeout,
|
|
110
|
+
}
|
|
111
|
+
if method in {"GET", "HEAD"}:
|
|
112
|
+
qs = paramx.encode_query(payload)
|
|
113
|
+
if qs:
|
|
114
|
+
url = f"{url}?{qs}"
|
|
115
|
+
else:
|
|
116
|
+
request_kwargs["json"] = payload
|
|
117
|
+
headers["Content-Type"] = "application/json"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
resp = self.http.request(method, url, **request_kwargs)
|
|
121
|
+
except httpx.TimeoutException as exc:
|
|
122
|
+
self.log.error(
|
|
123
|
+
"open api request failed method=%s path=%s operation=%s err=%s",
|
|
124
|
+
method,
|
|
125
|
+
path,
|
|
126
|
+
operation,
|
|
127
|
+
exc,
|
|
128
|
+
)
|
|
129
|
+
raise WzOpenError(f"request timed out: {exc}") from exc
|
|
130
|
+
except httpx.HTTPError as exc:
|
|
131
|
+
self.log.error(
|
|
132
|
+
"open api request failed method=%s path=%s operation=%s err=%s",
|
|
133
|
+
method,
|
|
134
|
+
path,
|
|
135
|
+
operation,
|
|
136
|
+
exc,
|
|
137
|
+
)
|
|
138
|
+
raise WzOpenError(f"network error: {exc}") from exc
|
|
139
|
+
|
|
140
|
+
status = resp.status_code
|
|
141
|
+
trace_id = _response_trace_id(resp)
|
|
142
|
+
if status < 200 or status >= 300:
|
|
143
|
+
self._log_failure(method, path, operation, status, trace_id)
|
|
144
|
+
raise WzOpenAPIError.http_status(status, operation, trace_id)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
env = resp.json()
|
|
148
|
+
except json.JSONDecodeError as exc:
|
|
149
|
+
raise WzOpenError(f"decode envelope: {exc}") from exc
|
|
150
|
+
if not isinstance(env, dict):
|
|
151
|
+
raise WzOpenError("decode envelope: not an object")
|
|
152
|
+
|
|
153
|
+
code = int(env.get("code") or 0)
|
|
154
|
+
if code != CODE_SUCCESS:
|
|
155
|
+
raise WzOpenAPIError(status, code, str(env.get("message") or ""), operation, trace_id)
|
|
156
|
+
|
|
157
|
+
self._log_success(method, path, operation, status, trace_id)
|
|
158
|
+
data = env.get("data")
|
|
159
|
+
return decode(data)
|
|
160
|
+
|
|
161
|
+
def _log_success(self, method: str, path: str, operation: str, status: int, trace_id: str) -> None:
|
|
162
|
+
if trace_id:
|
|
163
|
+
self.log.info(
|
|
164
|
+
"open api request method=%s path=%s operation=%s status=%s traceId=%s",
|
|
165
|
+
method,
|
|
166
|
+
path,
|
|
167
|
+
operation,
|
|
168
|
+
status,
|
|
169
|
+
trace_id,
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
self.log.info(
|
|
173
|
+
"open api request method=%s path=%s operation=%s status=%s",
|
|
174
|
+
method,
|
|
175
|
+
path,
|
|
176
|
+
operation,
|
|
177
|
+
status,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _log_failure(self, method: str, path: str, operation: str, status: int, trace_id: str) -> None:
|
|
181
|
+
if trace_id:
|
|
182
|
+
self.log.error(
|
|
183
|
+
"open api request failed method=%s path=%s operation=%s status=%s traceId=%s",
|
|
184
|
+
method,
|
|
185
|
+
path,
|
|
186
|
+
operation,
|
|
187
|
+
status,
|
|
188
|
+
trace_id,
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
self.log.error(
|
|
192
|
+
"open api request failed method=%s path=%s operation=%s status=%s",
|
|
193
|
+
method,
|
|
194
|
+
path,
|
|
195
|
+
operation,
|
|
196
|
+
status,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _should_retry(self, err: WzOpenError) -> bool:
|
|
200
|
+
if isinstance(err, WzOpenAPIError):
|
|
201
|
+
return err.status >= 500
|
|
202
|
+
cause = err.__cause__
|
|
203
|
+
return isinstance(cause, httpx.HTTPError)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _response_trace_id(resp: httpx.Response) -> str:
|
|
207
|
+
return (resp.headers.get(HEADER_B3_TRACE_ID) or "").strip()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Convert request models to signed parameter maps and GET query strings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from wzopensdk._internal import signer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def to_params(value: Any) -> dict[str, Any]:
|
|
13
|
+
if value is None:
|
|
14
|
+
return {}
|
|
15
|
+
if isinstance(value, dict):
|
|
16
|
+
raw = value
|
|
17
|
+
elif is_dataclass(value) and not isinstance(value, type):
|
|
18
|
+
raw = asdict(value)
|
|
19
|
+
elif hasattr(value, "__dict__"):
|
|
20
|
+
raw = {k: v for k, v in vars(value).items() if not k.startswith("_")}
|
|
21
|
+
else:
|
|
22
|
+
raw = {}
|
|
23
|
+
out: dict[str, Any] = {}
|
|
24
|
+
for key, item in raw.items():
|
|
25
|
+
if signer.is_blank(item):
|
|
26
|
+
continue
|
|
27
|
+
out[str(key)] = item
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode_query(params: dict[str, Any]) -> str:
|
|
32
|
+
if not params:
|
|
33
|
+
return ""
|
|
34
|
+
parts: list[str] = []
|
|
35
|
+
for key in sorted(params.keys()):
|
|
36
|
+
value = params[key]
|
|
37
|
+
if signer.is_blank(value):
|
|
38
|
+
continue
|
|
39
|
+
items = _as_slice(value)
|
|
40
|
+
if items is not None:
|
|
41
|
+
q_key = key if key.endswith("[]") else key + "[]"
|
|
42
|
+
for item in items:
|
|
43
|
+
parts.append(f"{quote(q_key, safe='')}={quote(item, safe='')}")
|
|
44
|
+
continue
|
|
45
|
+
parts.append(f"{quote(str(key), safe='')}={quote(signer.stringify(value), safe='')}")
|
|
46
|
+
return "&".join(parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _as_slice(value: Any) -> list[str] | None:
|
|
50
|
+
if isinstance(value, (list, tuple)):
|
|
51
|
+
return [signer.stringify(item) for item in value if not signer.is_blank(item)]
|
|
52
|
+
return None
|