qwx 0.1.1__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.
- qwx-0.1.1/.gitignore +12 -0
- qwx-0.1.1/LICENSE +21 -0
- qwx-0.1.1/PKG-INFO +460 -0
- qwx-0.1.1/README.md +424 -0
- qwx-0.1.1/README_EN.md +426 -0
- qwx-0.1.1/examples/basic_async.py +60 -0
- qwx-0.1.1/examples/basic_sync.py +61 -0
- qwx-0.1.1/examples/cache_redis.py +75 -0
- qwx-0.1.1/examples/jwt_auth.py +81 -0
- qwx-0.1.1/live/conftest.py +81 -0
- qwx-0.1.1/live/test_live_console.py +97 -0
- qwx-0.1.1/live/test_live_geo.py +124 -0
- qwx-0.1.1/live/test_live_v1.py +156 -0
- qwx-0.1.1/live/test_live_v7.py +153 -0
- qwx-0.1.1/pyproject.toml +117 -0
- qwx-0.1.1/src/qwx/__init__.py +90 -0
- qwx-0.1.1/src/qwx/_cachekey.py +193 -0
- qwx-0.1.1/src/qwx/_coord.py +24 -0
- qwx-0.1.1/src/qwx/_models.py +35 -0
- qwx-0.1.1/src/qwx/_service.py +32 -0
- qwx-0.1.1/src/qwx/auth.py +162 -0
- qwx-0.1.1/src/qwx/cache.py +81 -0
- qwx-0.1.1/src/qwx/client.py +601 -0
- qwx-0.1.1/src/qwx/console/__init__.py +35 -0
- qwx-0.1.1/src/qwx/console/client.py +168 -0
- qwx-0.1.1/src/qwx/console/types.py +271 -0
- qwx-0.1.1/src/qwx/errors.py +166 -0
- qwx-0.1.1/src/qwx/geo/__init__.py +39 -0
- qwx-0.1.1/src/qwx/geo/client.py +326 -0
- qwx-0.1.1/src/qwx/geo/types.py +263 -0
- qwx-0.1.1/src/qwx/lang.py +171 -0
- qwx-0.1.1/src/qwx/location.py +116 -0
- qwx-0.1.1/src/qwx/logger.py +89 -0
- qwx-0.1.1/src/qwx/py.typed +0 -0
- qwx-0.1.1/src/qwx/redis_cache.py +116 -0
- qwx-0.1.1/src/qwx/refer.py +32 -0
- qwx-0.1.1/src/qwx/request.py +50 -0
- qwx-0.1.1/src/qwx/timeutil.py +56 -0
- qwx-0.1.1/src/qwx/ttl.py +95 -0
- qwx-0.1.1/src/qwx/v1/__init__.py +12 -0
- qwx-0.1.1/src/qwx/v1/airquality/__init__.py +53 -0
- qwx-0.1.1/src/qwx/v1/airquality/client.py +271 -0
- qwx-0.1.1/src/qwx/v1/airquality/types.py +285 -0
- qwx-0.1.1/src/qwx/v1/alert/__init__.py +30 -0
- qwx-0.1.1/src/qwx/v1/alert/client.py +98 -0
- qwx-0.1.1/src/qwx/v1/alert/types.py +166 -0
- qwx-0.1.1/src/qwx/v1/solarradiation/__init__.py +33 -0
- qwx-0.1.1/src/qwx/v1/solarradiation/client.py +188 -0
- qwx-0.1.1/src/qwx/v1/solarradiation/types.py +188 -0
- qwx-0.1.1/src/qwx/v1/types.py +65 -0
- qwx-0.1.1/src/qwx/v1/weather/__init__.py +57 -0
- qwx-0.1.1/src/qwx/v1/weather/client.py +255 -0
- qwx-0.1.1/src/qwx/v1/weather/types.py +397 -0
- qwx-0.1.1/src/qwx/v7/__init__.py +7 -0
- qwx-0.1.1/src/qwx/v7/astronomy/__init__.py +31 -0
- qwx-0.1.1/src/qwx/v7/astronomy/client.py +336 -0
- qwx-0.1.1/src/qwx/v7/astronomy/types.py +187 -0
- qwx-0.1.1/src/qwx/v7/historical/__init__.py +25 -0
- qwx-0.1.1/src/qwx/v7/historical/client.py +126 -0
- qwx-0.1.1/src/qwx/v7/historical/types.py +184 -0
- qwx-0.1.1/src/qwx/v7/indices/__init__.py +27 -0
- qwx-0.1.1/src/qwx/v7/indices/client.py +225 -0
- qwx-0.1.1/src/qwx/v7/indices/types.py +102 -0
- qwx-0.1.1/src/qwx/v7/minutely/__init__.py +21 -0
- qwx-0.1.1/src/qwx/v7/minutely/client.py +105 -0
- qwx-0.1.1/src/qwx/v7/minutely/types.py +76 -0
- qwx-0.1.1/src/qwx/v7/ocean/__init__.py +23 -0
- qwx-0.1.1/src/qwx/v7/ocean/client.py +125 -0
- qwx-0.1.1/src/qwx/v7/ocean/types.py +107 -0
- qwx-0.1.1/src/qwx/v7/storm/__init__.py +40 -0
- qwx-0.1.1/src/qwx/v7/storm/client.py +219 -0
- qwx-0.1.1/src/qwx/v7/storm/types.py +256 -0
- qwx-0.1.1/tests/test_auth.py +150 -0
- qwx-0.1.1/tests/test_cache.py +31 -0
- qwx-0.1.1/tests/test_cachekey.py +213 -0
- qwx-0.1.1/tests/test_client.py +431 -0
- qwx-0.1.1/tests/test_console_client.py +228 -0
- qwx-0.1.1/tests/test_console_types.py +229 -0
- qwx-0.1.1/tests/test_contract_baseline.py +270 -0
- qwx-0.1.1/tests/test_coord.py +35 -0
- qwx-0.1.1/tests/test_endpoints_doc.py +220 -0
- qwx-0.1.1/tests/test_errors.py +93 -0
- qwx-0.1.1/tests/test_geo_client.py +405 -0
- qwx-0.1.1/tests/test_geo_types.py +202 -0
- qwx-0.1.1/tests/test_lang.py +58 -0
- qwx-0.1.1/tests/test_location.py +62 -0
- qwx-0.1.1/tests/test_logger.py +25 -0
- qwx-0.1.1/tests/test_redis_cache.py +143 -0
- qwx-0.1.1/tests/test_timeutil.py +38 -0
- qwx-0.1.1/tests/test_ttl.py +44 -0
- qwx-0.1.1/tests/test_v1_airquality_client.py +324 -0
- qwx-0.1.1/tests/test_v1_airquality_types.py +222 -0
- qwx-0.1.1/tests/test_v1_alert_client.py +215 -0
- qwx-0.1.1/tests/test_v1_alert_types.py +113 -0
- qwx-0.1.1/tests/test_v1_solarradiation_client.py +376 -0
- qwx-0.1.1/tests/test_v1_solarradiation_types.py +149 -0
- qwx-0.1.1/tests/test_v1_weather_client.py +293 -0
- qwx-0.1.1/tests/test_v1_weather_types.py +264 -0
- qwx-0.1.1/tests/test_v7_astronomy_client.py +432 -0
- qwx-0.1.1/tests/test_v7_astronomy_types.py +159 -0
- qwx-0.1.1/tests/test_v7_historical_client.py +220 -0
- qwx-0.1.1/tests/test_v7_historical_types.py +149 -0
- qwx-0.1.1/tests/test_v7_indices_client.py +299 -0
- qwx-0.1.1/tests/test_v7_indices_types.py +86 -0
- qwx-0.1.1/tests/test_v7_minutely_client.py +194 -0
- qwx-0.1.1/tests/test_v7_minutely_types.py +60 -0
- qwx-0.1.1/tests/test_v7_ocean_client.py +189 -0
- qwx-0.1.1/tests/test_v7_ocean_types.py +80 -0
- qwx-0.1.1/tests/test_v7_storm_client.py +343 -0
- qwx-0.1.1/tests/test_v7_storm_types.py +276 -0
- qwx-0.1.1/tests/test_version.py +23 -0
- qwx-0.1.1/tools/check_field_map.py +208 -0
- qwx-0.1.1/tools/extract_go_contract.py +1359 -0
- qwx-0.1.1/tools/gen_endpoints.py +233 -0
qwx-0.1.1/.gitignore
ADDED
qwx-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LiuDeTao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
qwx-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: qwx
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: 和风天气(QWeather)开发服务的 Python 客户端
|
|
5
|
+
Project-URL: Homepage, https://github.com/KanoCifer/qwx-py
|
|
6
|
+
Project-URL: Source, https://github.com/KanoCifer/qwx-py
|
|
7
|
+
Project-URL: Documentation, https://github.com/KanoCifer/qwx-py/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/KanoCifer/qwx-py/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/KanoCifer/qwx-py/releases
|
|
10
|
+
Author: KanoCifer
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: air-quality,forecast,geo,jwt,qweather,sdk,tide,warning,weather
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: pydantic>=2.7
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Provides-Extra: redis
|
|
34
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://raw.githubusercontent.com/KanoCifer/qwx-py/main/assets/banner.png" width="100%" alt="qwx" />
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://github.com/KanoCifer/qwx-py/actions/workflows/ci.yml" target="_blank" rel="noopener"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/KanoCifer/qwx-py/ci.yml?branch=main" height="20"></a>
|
|
43
|
+
<a href="https://pypi.org/project/qwx/" target="_blank" rel="noopener"><img alt="PyPI" src="https://img.shields.io/pypi/v/qwx" height="20"></a>
|
|
44
|
+
<a href="https://pypi.org/project/qwx/" target="_blank" rel="noopener"><img alt="Python" src="https://img.shields.io/pypi/pyversions/qwx" height="20"></a>
|
|
45
|
+
<a href="https://github.com/KanoCifer/qwx-py/blob/main/LICENSE" target="_blank" rel="noopener"><img alt="License" src="https://img.shields.io/github/license/KanoCifer/qwx-py" height="20"></a>
|
|
46
|
+
<a href="https://github.com/KanoCifer/qwx-py#endpoints" target="_blank" rel="noopener"><img alt="端点数" src="https://img.shields.io/badge/%E7%AB%AF%E7%82%B9-26%20%E4%B8%AA-blue" height="20"></a>
|
|
47
|
+
<a href="https://github.com/KanoCifer/qwx-py#endpoints" target="_blank" rel="noopener"><img alt="模型" src="https://img.shields.io/badge/%E6%A8%A1%E5%9E%8B-pydantic%20v2-success" height="20"></a>
|
|
48
|
+
<a href="https://github.com/KanoCifer/qwx" target="_blank" rel="noopener"><img alt="Go 版" src="https://img.shields.io/badge/Go-%E5%8F%82%E8%80%83-00ADD8?logo=go&logoColor=white" height="20"></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<p align="center">
|
|
52
|
+
<a href="README.md"><b>简体中文</b></a> · <a href="README_EN.md">English</a>
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
qwx 是社区维护的 Python SDK,覆盖和风天气(QWeather)上游文档中的核心接口族,返回 pydantic v2 模型。
|
|
56
|
+
|
|
57
|
+
Python 版与 Go 版([KanoCifer/qwx](https://github.com/KanoCifer/qwx))语义对等:端点集合、缓存键字节、端点常量值、错误分类与 TTL 优先级完全一致。缓存值存原始 JSON bytes,两边共享同一个 Redis 时键和值都互通。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary><b>📖 目录</b></summary>
|
|
63
|
+
|
|
64
|
+
- [✨ 特性](#features)
|
|
65
|
+
- [📦 安装](#installation)
|
|
66
|
+
- [🚀 快速开始](#quick-start)
|
|
67
|
+
- [📖 使用指南](#guide)
|
|
68
|
+
- [🔑 认证](#authentication)
|
|
69
|
+
- [⚙️ 客户端选项](#client-options)
|
|
70
|
+
- [📍 位置](#location)
|
|
71
|
+
- [🗄️ 缓存与 TTL](#caching)
|
|
72
|
+
- [📝 日志](#logging)
|
|
73
|
+
- [🚨 错误处理](#errors)
|
|
74
|
+
- [⏱️ 并发与超时](#concurrency)
|
|
75
|
+
- [🧪 测试](#testing)
|
|
76
|
+
- [🔌 支持的接口](#endpoints)
|
|
77
|
+
- [📚 文档](#docs)
|
|
78
|
+
- [🔗 官方文档](#upstream-docs)
|
|
79
|
+
- [⚖️ 许可](#license)
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<a id="features"></a>
|
|
86
|
+
## ✨ 特性
|
|
87
|
+
|
|
88
|
+
- **26 个端点**,覆盖天气、空气质量、预警、太阳辐射、GeoAPI、控制台、天文、历史天气、生活指数、分钟级降水、台风、潮汐十二个资源族。
|
|
89
|
+
- **同步与异步双栈**(httpx):端点方法体只写一份,同步客户端返回模型,异步客户端返回协程。
|
|
90
|
+
- **pydantic v2 模型**:上游新增字段不会被丢弃,未知字段保留在 `model_extra` 里。
|
|
91
|
+
- **缓存键与 Go 版逐字相同**:字面量是已发布事实,改了会让已写入的缓存条目全部失效。
|
|
92
|
+
- **客户端先校验**:参数越界与位置形态非法在本地抛异常,不打上游。
|
|
93
|
+
- **错误分层**:按异常类型判断,不解析错误字符串。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
<a id="installation"></a>
|
|
98
|
+
## 📦 安装
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install qwx
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
要求 Python 3.10 及以上。依赖 `httpx`、`pydantic`、`cryptography`(JWT 签名)。
|
|
105
|
+
|
|
106
|
+
需要 Redis 缓存时装可选 extra:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install "qwx[redis]"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
从源码装:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git clone https://github.com/KanoCifer/qwx-py.git
|
|
116
|
+
cd qwx-py
|
|
117
|
+
pip install -e ".[dev]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
<a id="quick-start"></a>
|
|
123
|
+
## 🚀 快速开始
|
|
124
|
+
|
|
125
|
+
三步:创建认证、创建基础客户端、创建接口族的客户端。
|
|
126
|
+
|
|
127
|
+
### 同步
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import qwx
|
|
131
|
+
from qwx.v1.weather import WeatherService
|
|
132
|
+
|
|
133
|
+
# 1. 认证。JWT 用控制台分配的凭据,API KEY 用 qwx.APIKeyAuth。
|
|
134
|
+
auth = qwx.JWTAuth(
|
|
135
|
+
private_key_pem=open("ed25519-private.pem", encoding="utf-8").read(),
|
|
136
|
+
key_id="YOUR_KEY_ID",
|
|
137
|
+
project_id="YOUR_PROJECT_ID",
|
|
138
|
+
developer_id="YOUR_DEVELOPER_ID", # 必填,控制台「设置」页
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# 2. 基础客户端。api_host 是控制台「设置」页分配的专属 Host,必填。
|
|
142
|
+
with qwx.Client(auth, "abcxyz.qweatherapi.com") as client:
|
|
143
|
+
# 3. 接口族的客户端。
|
|
144
|
+
weather = WeatherService(client)
|
|
145
|
+
result = weather.current(qwx.Location(lat=39.92, lon=116.41))
|
|
146
|
+
print(result.condition.text, result.temperature.value, result.temperature.unit)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`Client` 是上下文管理器,`with` 退出时关闭底层 HTTP 连接。不需要上下文管理时自己调 `client.close()`。
|
|
150
|
+
|
|
151
|
+
### 异步
|
|
152
|
+
|
|
153
|
+
异步版方法同名,返回协程:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import asyncio
|
|
157
|
+
|
|
158
|
+
import qwx
|
|
159
|
+
from qwx.v1.weather import AsyncWeatherService
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def main() -> None:
|
|
163
|
+
auth = qwx.APIKeyAuth("YOUR_API_KEY")
|
|
164
|
+
async with qwx.AsyncClient(auth, "abcxyz.qweatherapi.com") as client:
|
|
165
|
+
weather = AsyncWeatherService(client)
|
|
166
|
+
result = await weather.current(qwx.Location(lat=39.92, lon=116.41))
|
|
167
|
+
print(result.condition.text)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
asyncio.run(main())
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 可运行示例
|
|
174
|
+
|
|
175
|
+
`examples/` 下有四个脚本,读环境变量后直接跑:
|
|
176
|
+
|
|
177
|
+
| 脚本 | 内容 |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `examples/basic_sync.py` | API KEY 认证,取实时天气 |
|
|
180
|
+
| `examples/basic_async.py` | 异步版 |
|
|
181
|
+
| `examples/jwt_auth.py` | JWT 认证 |
|
|
182
|
+
| `examples/cache_redis.py` | 接 Redis 缓存 |
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
QWX_API_KEY=YOUR_API_KEY QWX_API_HOST=your-host.qweatherapi.com python examples/basic_sync.py
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
<a id="guide"></a>
|
|
191
|
+
## 📖 使用指南
|
|
192
|
+
|
|
193
|
+
<a id="authentication"></a>
|
|
194
|
+
### 🔑 认证
|
|
195
|
+
|
|
196
|
+
两种认证任选其一,不能同时使用。
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
auth = qwx.APIKeyAuth("YOUR_API_KEY")
|
|
200
|
+
|
|
201
|
+
auth = qwx.JWTAuth(
|
|
202
|
+
private_key_pem=open("ed25519-private.pem", encoding="utf-8").read(),
|
|
203
|
+
key_id="YOUR_KEY_ID",
|
|
204
|
+
project_id="YOUR_PROJECT_ID",
|
|
205
|
+
developer_id="YOUR_DEVELOPER_ID",
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
JWT 用 Ed25519 私钥签 EdDSA 令牌,有效期 15 分钟,客户端在过期前自动换新。私钥生成:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
openssl genpkey -algorithm ED25519 -out ed25519-private.pem
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
官方已公告:从 2027 年 1 月 1 日起限制用 API KEY 认证的每日请求量,且 SDK 5+ 不再支持 API KEY。新项目建议用 JWT。配置与排障顺序见 [docs/auth.md](docs/auth.md)。
|
|
216
|
+
|
|
217
|
+
<a id="client-options"></a>
|
|
218
|
+
### ⚙️ 客户端选项
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
client = qwx.Client(
|
|
222
|
+
auth,
|
|
223
|
+
"abcxyz.qweatherapi.com",
|
|
224
|
+
timeout=30.0, # 单请求超时(秒)
|
|
225
|
+
cache=cache, # 缓存实现,默认 None
|
|
226
|
+
ttl_policy=policy, # 按端点决定缓存时长
|
|
227
|
+
logger=logger, # 标准库 logging 的 logger
|
|
228
|
+
http_client=httpx.Client(...), # 自定义 httpx 客户端
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`api_host` 没有默认值:公共域名已陆续停服,必须用控制台「设置」页分配的专属 Host。
|
|
233
|
+
|
|
234
|
+
<a id="location"></a>
|
|
235
|
+
### 📍 位置
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
qwx.Location(lat=39.92, lon=116.41) # 坐标对
|
|
239
|
+
qwx.Location(id="101010100") # LocationID
|
|
240
|
+
qwx.Location(name="杭州") # 地名,只有 GeoAPI 认
|
|
241
|
+
qwx.parse_location("116.41,39.92") # 从字符串解析:经度在前
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
新一代端点(`/weather/v1/*` 等)的路径参数是**纬度在前**;v7 与 GeoAPI 的查询参数是**经度在前**。写反了上游直接 400。位置形态按端点不同,详见 [docs/endpoints.md](docs/endpoints.md)。
|
|
245
|
+
|
|
246
|
+
<a id="caching"></a>
|
|
247
|
+
### 🗄️ 缓存与 TTL
|
|
248
|
+
|
|
249
|
+
不传 `cache` 时每次请求都打到上游。缓存接口只有两个方法,自己实现即可:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from datetime import timedelta
|
|
253
|
+
from typing import Protocol
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class Cache(Protocol):
|
|
257
|
+
def get(self, key: str) -> bytes | None: ...
|
|
258
|
+
def set(self, key: str, data: bytes, ttl: timedelta) -> None: ...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
内置 `qwx.MemoryCache` 只适合测试与单实例临时缓存(没有后台清理,长期运行内存只增不减)。生产环境用共享缓存:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from qwx.redis_cache import RedisCache
|
|
265
|
+
|
|
266
|
+
cache = RedisCache.from_url("redis://127.0.0.1:6379/0")
|
|
267
|
+
client = qwx.Client(auth, "abcxyz.qweatherapi.com", cache=cache)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
缓存值是原始 JSON bytes,与 Go 版共享同一个 Redis 时键和值都互通。
|
|
271
|
+
|
|
272
|
+
TTL 三级优先级:单次覆盖 > `TTLPolicy` > 端点默认时长。
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from datetime import timedelta
|
|
276
|
+
|
|
277
|
+
import qwx
|
|
278
|
+
from qwx.v1.weather import EndpointCurrent
|
|
279
|
+
|
|
280
|
+
policy = qwx.TTLMap({EndpointCurrent: timedelta(seconds=30)}) # 实况只缓存 30 秒
|
|
281
|
+
client = qwx.Client(auth, "abcxyz.qweatherapi.com", cache=cache, ttl_policy=policy)
|
|
282
|
+
|
|
283
|
+
with client.override_ttl(timedelta(0)): # 这一次不读也不写缓存
|
|
284
|
+
weather.current(loc)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
键的形状、TTL 表与接自己的缓存见 [docs/caching.md](docs/caching.md)。
|
|
288
|
+
|
|
289
|
+
<a id="logging"></a>
|
|
290
|
+
### 📝 日志
|
|
291
|
+
|
|
292
|
+
不注入 logger 时不输出任何日志。注入标准库 `logging` 的 logger:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
import logging
|
|
296
|
+
|
|
297
|
+
import qwx
|
|
298
|
+
|
|
299
|
+
logger = logging.getLogger("qwx")
|
|
300
|
+
logger.setLevel(logging.INFO)
|
|
301
|
+
|
|
302
|
+
client = qwx.Client(auth, "abcxyz.qweatherapi.com", logger=logger)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
字段名:`endpoint`、`method`、`path`、`status`、`duration_ms`、`cache_key`、`attempt`、`error`、`upstream_code`、`upstream_title`。日志里没有认证凭据与响应体,查询参数也不记录。详见 [docs/logging.md](docs/logging.md)。
|
|
306
|
+
|
|
307
|
+
<a id="errors"></a>
|
|
308
|
+
### 🚨 错误处理
|
|
309
|
+
|
|
310
|
+
异常层级:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
QwxError
|
|
314
|
+
├── InvalidConfigError 构造参数有问题
|
|
315
|
+
├── InvalidLocationError 位置形态非法
|
|
316
|
+
├── InvalidParameterError 参数越界
|
|
317
|
+
├── InvalidResponseError 响应不是合法 JSON
|
|
318
|
+
└── UpstreamError
|
|
319
|
+
├── UpstreamRejectedError 上游 4xx
|
|
320
|
+
├── QuotaExceededError 上游 429
|
|
321
|
+
├── UnauthorizedError 上游 401 / 403
|
|
322
|
+
└── UpstreamUnavailableError 上游 5xx 或网络错误
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
按类型判断,不解析错误字符串:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
import qwx
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
result = weather.current(loc)
|
|
332
|
+
except qwx.UnauthorizedError:
|
|
333
|
+
... # 查凭据,见 docs/auth.md
|
|
334
|
+
except qwx.QuotaExceededError:
|
|
335
|
+
... # 退避后重试
|
|
336
|
+
except qwx.InvalidParameterError:
|
|
337
|
+
... # 修请求
|
|
338
|
+
except qwx.UpstreamError as exc:
|
|
339
|
+
print(exc.status_code, exc.code, exc.title, exc.invalid_params, exc.body)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
前四类在本地抛出,不打上游。上游 5xx、429 与网络错误时客户端自己带退避重试(首次之外最多 3 次)。详见 [docs/errors.md](docs/errors.md)。
|
|
343
|
+
|
|
344
|
+
<a id="concurrency"></a>
|
|
345
|
+
### ⏱️ 并发与超时
|
|
346
|
+
|
|
347
|
+
`Client` 与 `AsyncClient` 无状态,可并发共享。默认单请求超时 30 秒;需要代理、连接池或自定义证书时注入自己的 httpx 客户端:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
import httpx
|
|
351
|
+
|
|
352
|
+
import qwx
|
|
353
|
+
|
|
354
|
+
http = httpx.Client(
|
|
355
|
+
timeout=httpx.Timeout(10.0, connect=3.0), limits=httpx.Limits(max_connections=50)
|
|
356
|
+
)
|
|
357
|
+
client = qwx.Client(auth, "abcxyz.qweatherapi.com", http_client=http)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
异步版同理,注入 `httpx.AsyncClient`。
|
|
361
|
+
|
|
362
|
+
<a id="testing"></a>
|
|
363
|
+
### 🧪 测试
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
pytest tests/ -q # 单元测试,不打上游
|
|
367
|
+
pytest live -q # 联网冒烟,需要真实凭据,缺凭据时整份跳过
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
联网冒烟要环境变量:`QWX_API_HOST` 必填,认证用 `QWX_API_KEY` 或 `QWX_JWT_PRIVATE_KEY` + `QWX_KEY_ID` + `QWX_PROJECT_ID` + `QWX_DEVELOPER_ID`。详见 [docs/verification.md](docs/verification.md)。
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
<a id="endpoints"></a>
|
|
375
|
+
## 🔌 支持的接口
|
|
376
|
+
|
|
377
|
+
26 个端点,与 [docs/endpoints.md](docs/endpoints.md) 一一对应(那里有位置形态、默认 TTL、缓存键模板)。
|
|
378
|
+
|
|
379
|
+
| 模块 | 方法 | 上游接口 | 端点常量 |
|
|
380
|
+
| --- | --- | --- | --- |
|
|
381
|
+
| `qwx.console` | `finance` | `GET /finance/v1/summary` | `qwx.console.EndpointFinance` |
|
|
382
|
+
| `qwx.console` | `stats` | `GET /metrics/v1/stats` | `qwx.console.EndpointStats` |
|
|
383
|
+
| `qwx.geo` | `lookup_city` | `GET /geo/v2/city/lookup` | `qwx.geo.EndpointCity` |
|
|
384
|
+
| `qwx.geo` | `lookup_poi` | `GET /geo/v2/poi/lookup` | `qwx.geo.EndpointPOI` |
|
|
385
|
+
| `qwx.geo` | `poi_range` | `GET /geo/v2/poi/range` | `qwx.geo.EndpointPOIRange` |
|
|
386
|
+
| `qwx.geo` | `top_cities` | `GET /geo/v2/city/top` | `qwx.geo.EndpointTopCities` |
|
|
387
|
+
| `qwx.v1.airquality` | `current` | `GET /airquality/v1/current/{lat}/{lon}` | `qwx.v1.airquality.EndpointCurrent` |
|
|
388
|
+
| `qwx.v1.airquality` | `daily` | `GET /airquality/v1/daily/{lat}/{lon}` | `qwx.v1.airquality.EndpointDaily` |
|
|
389
|
+
| `qwx.v1.airquality` | `hourly` | `GET /airquality/v1/hourly/{lat}/{lon}` | `qwx.v1.airquality.EndpointHourly` |
|
|
390
|
+
| `qwx.v1.airquality` | `station` | `GET /airquality/v1/stations/{stationID}` | `qwx.v1.airquality.EndpointStation` |
|
|
391
|
+
| `qwx.v1.alert` | `current` | `GET /weatheralert/v1/current/{lat}/{lon}` | `qwx.v1.alert.Endpoint` |
|
|
392
|
+
| `qwx.v1.solarradiation` | `forecast` | `GET /solarradiation/v1/forecast/{lat}/{lon}` | `qwx.v1.solarradiation.Endpoint` |
|
|
393
|
+
| `qwx.v1.weather` | `current` | `GET /weather/v1/current/{lat}/{lon}` | `qwx.v1.weather.EndpointCurrent` |
|
|
394
|
+
| `qwx.v1.weather` | `hourly` | `GET /weather/v1/hourly/{lat}/{lon}` | `qwx.v1.weather.EndpointHourly` |
|
|
395
|
+
| `qwx.v1.weather` | `daily` | `GET /weather/v1/daily/{lat}/{lon}` | `qwx.v1.weather.EndpointDaily` |
|
|
396
|
+
| `qwx.v7.astronomy` | `sun` | `GET /v7/astronomy/sun` | `qwx.v7.astronomy.EndpointSun` |
|
|
397
|
+
| `qwx.v7.astronomy` | `moon` | `GET /v7/astronomy/moon` | `qwx.v7.astronomy.EndpointMoon` |
|
|
398
|
+
| `qwx.v7.astronomy` | `solar_elevation_angle` | `GET /v7/astronomy/solar-elevation-angle` | `qwx.v7.astronomy.EndpointAngle` |
|
|
399
|
+
| `qwx.v7.historical` | `weather` | `GET /v7/historical/weather` | `qwx.v7.historical.Endpoint` |
|
|
400
|
+
| `qwx.v7.indices` | `indices` | `GET /v7/indices/{days}d` | `qwx.v7.indices.Endpoint` |
|
|
401
|
+
| `qwx.v7.indices` | `indices_multi` | `GET /v7/indices/{days}d` | `qwx.v7.indices.Endpoint` |
|
|
402
|
+
| `qwx.v7.minutely` | `minutely` | `GET /v7/minutely/5m` | `qwx.v7.minutely.Endpoint` |
|
|
403
|
+
| `qwx.v7.ocean` | `tide` | `GET /v7/ocean/tide` | `qwx.v7.ocean.Endpoint` |
|
|
404
|
+
| `qwx.v7.storm` | `list` | `GET /v7/tropical/storm-list` | `qwx.v7.storm.EndpointList` |
|
|
405
|
+
| `qwx.v7.storm` | `forecast` | `GET /v7/tropical/storm-forecast` | `qwx.v7.storm.EndpointForecast` |
|
|
406
|
+
| `qwx.v7.storm` | `track` | `GET /v7/tropical/storm-track` | `qwx.v7.storm.EndpointTrack` |
|
|
407
|
+
|
|
408
|
+
模块结构:
|
|
409
|
+
|
|
410
|
+
| 模块 | 承载 |
|
|
411
|
+
| --- | --- |
|
|
412
|
+
| `qwx` | 基础客户端、认证、缓存、`Location`、错误层级、两代共用的请求结构、`Refer`、`Metadata` |
|
|
413
|
+
| `qwx.v1` | 新一代跨端点族共用的基础类型(`Metadata`、`Value`、`Options`) |
|
|
414
|
+
| `qwx.v1.weather` | 新一代天气 `/weather/v1/*` |
|
|
415
|
+
| `qwx.v1.airquality` | 新一代空气质量 `/airquality/v1/*` |
|
|
416
|
+
| `qwx.v1.alert` | 预警 `/weatheralert/v1/*` |
|
|
417
|
+
| `qwx.v1.solarradiation` | 新一代太阳辐射预报 `/solarradiation/v1/*` |
|
|
418
|
+
| `qwx.v7.ocean` | 旧一代潮汐 `/v7/ocean/tide` |
|
|
419
|
+
| `qwx.v7.indices` | 旧一代生活指数 `/v7/indices/*` |
|
|
420
|
+
| `qwx.v7.minutely` | 旧一代分钟级降水 `/v7/minutely/*` |
|
|
421
|
+
| `qwx.v7.storm` | 台风 `/v7/tropical/*` |
|
|
422
|
+
| `qwx.v7.astronomy` | 天文 `/v7/astronomy/*` |
|
|
423
|
+
| `qwx.v7.historical` | 旧一代历史天气 `/v7/historical/weather` |
|
|
424
|
+
| `qwx.geo` | GeoAPI `/geo/v2/*` |
|
|
425
|
+
| `qwx.console` | 控制台 `/finance/v1/*`、`/metrics/v1/stats` |
|
|
426
|
+
| `qwx.redis_cache` | Redis 缓存实现(可选依赖 `qwx[redis]`) |
|
|
427
|
+
|
|
428
|
+
一个模块 = 一个上游资源族。两个例外:`geo` 的四个检索端点共享 `Place` 类型,`console` 的两个端点在上游归在同一页文档下、共享同一套响应骨架。
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
<a id="docs"></a>
|
|
433
|
+
## 📚 文档
|
|
434
|
+
|
|
435
|
+
| 场景 | 文档 |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| 接认证、配凭据、token 过期、401/403 | [docs/auth.md](docs/auth.md) |
|
|
438
|
+
| 写调用代码:参数上限、位置形态、返回类型、字段单位 | [docs/contract.md](docs/contract.md) |
|
|
439
|
+
| 上游行为与文档不符:400、空结果、时间对不上 | [docs/gotchas.md](docs/gotchas.md) |
|
|
440
|
+
| 决定重试与降级策略、读上游错误细节 | [docs/errors.md](docs/errors.md) |
|
|
441
|
+
| 接自己的缓存、调 TTL、排查缓存串味 | [docs/caching.md](docs/caching.md) |
|
|
442
|
+
| 接日志、排查请求记录 | [docs/logging.md](docs/logging.md) |
|
|
443
|
+
| 跑测试、联网冒烟 | [docs/verification.md](docs/verification.md) |
|
|
444
|
+
| 查全部端点:路径、位置形态、默认 TTL、缓存键模板 | [docs/endpoints.md](docs/endpoints.md) |
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
<a id="upstream-docs"></a>
|
|
449
|
+
## 🔗 官方文档
|
|
450
|
+
|
|
451
|
+
<https://dev.qweather.com/docs/>
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
<a id="license"></a>
|
|
456
|
+
## ⚖️ 许可
|
|
457
|
+
|
|
458
|
+
本包遵循 MIT 许可。
|
|
459
|
+
|
|
460
|
+
本包不是和风天气官方产品。使用上游数据需遵守[和风天气开发者许可协议](https://www.qweather.com/terms/developers-eula/),展示天气数据时需保留上游返回的 attribution 信息。
|