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.
Files changed (114) hide show
  1. qwx-0.1.1/.gitignore +12 -0
  2. qwx-0.1.1/LICENSE +21 -0
  3. qwx-0.1.1/PKG-INFO +460 -0
  4. qwx-0.1.1/README.md +424 -0
  5. qwx-0.1.1/README_EN.md +426 -0
  6. qwx-0.1.1/examples/basic_async.py +60 -0
  7. qwx-0.1.1/examples/basic_sync.py +61 -0
  8. qwx-0.1.1/examples/cache_redis.py +75 -0
  9. qwx-0.1.1/examples/jwt_auth.py +81 -0
  10. qwx-0.1.1/live/conftest.py +81 -0
  11. qwx-0.1.1/live/test_live_console.py +97 -0
  12. qwx-0.1.1/live/test_live_geo.py +124 -0
  13. qwx-0.1.1/live/test_live_v1.py +156 -0
  14. qwx-0.1.1/live/test_live_v7.py +153 -0
  15. qwx-0.1.1/pyproject.toml +117 -0
  16. qwx-0.1.1/src/qwx/__init__.py +90 -0
  17. qwx-0.1.1/src/qwx/_cachekey.py +193 -0
  18. qwx-0.1.1/src/qwx/_coord.py +24 -0
  19. qwx-0.1.1/src/qwx/_models.py +35 -0
  20. qwx-0.1.1/src/qwx/_service.py +32 -0
  21. qwx-0.1.1/src/qwx/auth.py +162 -0
  22. qwx-0.1.1/src/qwx/cache.py +81 -0
  23. qwx-0.1.1/src/qwx/client.py +601 -0
  24. qwx-0.1.1/src/qwx/console/__init__.py +35 -0
  25. qwx-0.1.1/src/qwx/console/client.py +168 -0
  26. qwx-0.1.1/src/qwx/console/types.py +271 -0
  27. qwx-0.1.1/src/qwx/errors.py +166 -0
  28. qwx-0.1.1/src/qwx/geo/__init__.py +39 -0
  29. qwx-0.1.1/src/qwx/geo/client.py +326 -0
  30. qwx-0.1.1/src/qwx/geo/types.py +263 -0
  31. qwx-0.1.1/src/qwx/lang.py +171 -0
  32. qwx-0.1.1/src/qwx/location.py +116 -0
  33. qwx-0.1.1/src/qwx/logger.py +89 -0
  34. qwx-0.1.1/src/qwx/py.typed +0 -0
  35. qwx-0.1.1/src/qwx/redis_cache.py +116 -0
  36. qwx-0.1.1/src/qwx/refer.py +32 -0
  37. qwx-0.1.1/src/qwx/request.py +50 -0
  38. qwx-0.1.1/src/qwx/timeutil.py +56 -0
  39. qwx-0.1.1/src/qwx/ttl.py +95 -0
  40. qwx-0.1.1/src/qwx/v1/__init__.py +12 -0
  41. qwx-0.1.1/src/qwx/v1/airquality/__init__.py +53 -0
  42. qwx-0.1.1/src/qwx/v1/airquality/client.py +271 -0
  43. qwx-0.1.1/src/qwx/v1/airquality/types.py +285 -0
  44. qwx-0.1.1/src/qwx/v1/alert/__init__.py +30 -0
  45. qwx-0.1.1/src/qwx/v1/alert/client.py +98 -0
  46. qwx-0.1.1/src/qwx/v1/alert/types.py +166 -0
  47. qwx-0.1.1/src/qwx/v1/solarradiation/__init__.py +33 -0
  48. qwx-0.1.1/src/qwx/v1/solarradiation/client.py +188 -0
  49. qwx-0.1.1/src/qwx/v1/solarradiation/types.py +188 -0
  50. qwx-0.1.1/src/qwx/v1/types.py +65 -0
  51. qwx-0.1.1/src/qwx/v1/weather/__init__.py +57 -0
  52. qwx-0.1.1/src/qwx/v1/weather/client.py +255 -0
  53. qwx-0.1.1/src/qwx/v1/weather/types.py +397 -0
  54. qwx-0.1.1/src/qwx/v7/__init__.py +7 -0
  55. qwx-0.1.1/src/qwx/v7/astronomy/__init__.py +31 -0
  56. qwx-0.1.1/src/qwx/v7/astronomy/client.py +336 -0
  57. qwx-0.1.1/src/qwx/v7/astronomy/types.py +187 -0
  58. qwx-0.1.1/src/qwx/v7/historical/__init__.py +25 -0
  59. qwx-0.1.1/src/qwx/v7/historical/client.py +126 -0
  60. qwx-0.1.1/src/qwx/v7/historical/types.py +184 -0
  61. qwx-0.1.1/src/qwx/v7/indices/__init__.py +27 -0
  62. qwx-0.1.1/src/qwx/v7/indices/client.py +225 -0
  63. qwx-0.1.1/src/qwx/v7/indices/types.py +102 -0
  64. qwx-0.1.1/src/qwx/v7/minutely/__init__.py +21 -0
  65. qwx-0.1.1/src/qwx/v7/minutely/client.py +105 -0
  66. qwx-0.1.1/src/qwx/v7/minutely/types.py +76 -0
  67. qwx-0.1.1/src/qwx/v7/ocean/__init__.py +23 -0
  68. qwx-0.1.1/src/qwx/v7/ocean/client.py +125 -0
  69. qwx-0.1.1/src/qwx/v7/ocean/types.py +107 -0
  70. qwx-0.1.1/src/qwx/v7/storm/__init__.py +40 -0
  71. qwx-0.1.1/src/qwx/v7/storm/client.py +219 -0
  72. qwx-0.1.1/src/qwx/v7/storm/types.py +256 -0
  73. qwx-0.1.1/tests/test_auth.py +150 -0
  74. qwx-0.1.1/tests/test_cache.py +31 -0
  75. qwx-0.1.1/tests/test_cachekey.py +213 -0
  76. qwx-0.1.1/tests/test_client.py +431 -0
  77. qwx-0.1.1/tests/test_console_client.py +228 -0
  78. qwx-0.1.1/tests/test_console_types.py +229 -0
  79. qwx-0.1.1/tests/test_contract_baseline.py +270 -0
  80. qwx-0.1.1/tests/test_coord.py +35 -0
  81. qwx-0.1.1/tests/test_endpoints_doc.py +220 -0
  82. qwx-0.1.1/tests/test_errors.py +93 -0
  83. qwx-0.1.1/tests/test_geo_client.py +405 -0
  84. qwx-0.1.1/tests/test_geo_types.py +202 -0
  85. qwx-0.1.1/tests/test_lang.py +58 -0
  86. qwx-0.1.1/tests/test_location.py +62 -0
  87. qwx-0.1.1/tests/test_logger.py +25 -0
  88. qwx-0.1.1/tests/test_redis_cache.py +143 -0
  89. qwx-0.1.1/tests/test_timeutil.py +38 -0
  90. qwx-0.1.1/tests/test_ttl.py +44 -0
  91. qwx-0.1.1/tests/test_v1_airquality_client.py +324 -0
  92. qwx-0.1.1/tests/test_v1_airquality_types.py +222 -0
  93. qwx-0.1.1/tests/test_v1_alert_client.py +215 -0
  94. qwx-0.1.1/tests/test_v1_alert_types.py +113 -0
  95. qwx-0.1.1/tests/test_v1_solarradiation_client.py +376 -0
  96. qwx-0.1.1/tests/test_v1_solarradiation_types.py +149 -0
  97. qwx-0.1.1/tests/test_v1_weather_client.py +293 -0
  98. qwx-0.1.1/tests/test_v1_weather_types.py +264 -0
  99. qwx-0.1.1/tests/test_v7_astronomy_client.py +432 -0
  100. qwx-0.1.1/tests/test_v7_astronomy_types.py +159 -0
  101. qwx-0.1.1/tests/test_v7_historical_client.py +220 -0
  102. qwx-0.1.1/tests/test_v7_historical_types.py +149 -0
  103. qwx-0.1.1/tests/test_v7_indices_client.py +299 -0
  104. qwx-0.1.1/tests/test_v7_indices_types.py +86 -0
  105. qwx-0.1.1/tests/test_v7_minutely_client.py +194 -0
  106. qwx-0.1.1/tests/test_v7_minutely_types.py +60 -0
  107. qwx-0.1.1/tests/test_v7_ocean_client.py +189 -0
  108. qwx-0.1.1/tests/test_v7_ocean_types.py +80 -0
  109. qwx-0.1.1/tests/test_v7_storm_client.py +343 -0
  110. qwx-0.1.1/tests/test_v7_storm_types.py +276 -0
  111. qwx-0.1.1/tests/test_version.py +23 -0
  112. qwx-0.1.1/tools/check_field_map.py +208 -0
  113. qwx-0.1.1/tools/extract_go_contract.py +1359 -0
  114. qwx-0.1.1/tools/gen_endpoints.py +233 -0
qwx-0.1.1/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .DS_Store
11
+ docs/
12
+ contract/
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 信息。