birdrecord-cli 0.1.1__tar.gz → 0.1.3__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 (40) hide show
  1. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/PKG-INFO +5 -5
  2. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/README.md +2 -2
  3. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli/adcode/__init__.py +6 -2
  4. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli/core.py +2 -2
  5. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli/report/__init__.py +7 -1
  6. birdrecord_cli-0.1.3/birdrecord_cli/cli/search/__init__.py +282 -0
  7. birdrecord_cli-0.1.3/birdrecord_cli/cli/search/report_map.py +433 -0
  8. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli/taxon/__init__.py +7 -1
  9. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/client.py +26 -10
  10. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/i18n.py +1 -0
  11. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/cli/stdout.py +7 -0
  12. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/cli/unified_search.py +3 -1
  13. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/activity_payloads.py +2 -6
  14. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/activity_requests.py +3 -1
  15. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/taxon.py +4 -4
  16. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/PKG-INFO +5 -5
  17. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/SOURCES.txt +1 -0
  18. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/requires.txt +1 -1
  19. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/pyproject.toml +4 -3
  20. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/tests/test_birdrecord_client.py +46 -19
  21. birdrecord_cli-0.1.1/birdrecord_cli/cli/search/__init__.py +0 -143
  22. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/LICENSE +0 -0
  23. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/__init__.py +0 -0
  24. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli/__init__.py +0 -0
  25. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/cli_main.py +0 -0
  26. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/constants.py +0 -0
  27. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/crypto.py +1 -1
  28. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/__init__.py +0 -0
  29. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/cli/__init__.py +0 -0
  30. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/__init__.py +0 -0
  31. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/adcode.py +0 -0
  32. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/chart_payloads.py +0 -0
  33. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/chart_requests.py +0 -0
  34. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/envelopes.py +0 -0
  35. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/report_payloads.py +0 -0
  36. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli/models/client/report_requests.py +0 -0
  37. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/dependency_links.txt +0 -0
  38. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/entry_points.txt +0 -0
  39. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/birdrecord_cli.egg-info/top_level.txt +0 -0
  40. {birdrecord_cli-0.1.1 → birdrecord_cli-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: birdrecord-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: CLI for China Bird Record (birdreport.cn); default API host weixin.birdrecord.cn.
5
5
  Author: yoshino-s
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ Project-URL: Documentation, https://github.com/yoshino-s/birdrecord-cli#readme
10
10
  Project-URL: Issues, https://github.com/yoshino-s/birdrecord-cli/issues
11
11
  Project-URL: Changelog, https://github.com/yoshino-s/birdrecord-cli/releases
12
12
  Project-URL: PyPI, https://pypi.org/project/birdrecord-cli/
13
- Keywords: birdrecord,birding,cli,weixin,miniprogram,httpx,click
13
+ Keywords: birdrecord,birding,cli,weixin,miniprogram,requests,click
14
14
  Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Environment :: Console
16
16
  Classifier: Intended Audience :: Developers
@@ -22,7 +22,7 @@ Requires-Python: >=3.12
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: click>=8.1.0
25
- Requires-Dist: httpx>=0.28.1
25
+ Requires-Dist: requests>=2.32.0
26
26
  Requires-Dist: pycryptodome>=3.21.0
27
27
  Requires-Dist: pydantic>=2.10.0
28
28
  Requires-Dist: pypinyin>=0.53.0
@@ -69,8 +69,8 @@ Package index: [pypi.org/project/birdrecord-cli](https://pypi.org/project/birdre
69
69
  [uv](https://docs.astral.sh/uv/) downloads the package into an ephemeral environment. Pin the version for reproducible behavior:
70
70
 
71
71
  ```bash
72
- uvx --from 'birdrecord-cli==0.1.1' birdrecord-cli --help
73
- uvx --from 'birdrecord-cli==0.1.1' birdrecord-cli provinces --pretty
72
+ uvx --from 'birdrecord-cli==0.1.3' birdrecord-cli --help
73
+ uvx --from 'birdrecord-cli==0.1.3' birdrecord-cli provinces --pretty
74
74
  ```
75
75
 
76
76
  Use the latest release version from PyPI if it differs from the example above.
@@ -39,8 +39,8 @@ Package index: [pypi.org/project/birdrecord-cli](https://pypi.org/project/birdre
39
39
  [uv](https://docs.astral.sh/uv/) downloads the package into an ephemeral environment. Pin the version for reproducible behavior:
40
40
 
41
41
  ```bash
42
- uvx --from 'birdrecord-cli==0.1.1' birdrecord-cli --help
43
- uvx --from 'birdrecord-cli==0.1.1' birdrecord-cli provinces --pretty
42
+ uvx --from 'birdrecord-cli==0.1.3' birdrecord-cli --help
43
+ uvx --from 'birdrecord-cli==0.1.3' birdrecord-cli provinces --pretty
44
44
  ```
45
45
 
46
46
  Use the latest release version from PyPI if it differs from the example above.
@@ -68,7 +68,9 @@ def register_adcode_commands(group: click.Group) -> None:
68
68
  filtered = filter_region_rows_by_query(
69
69
  list(raw.payload), query, label_attr="province_name"
70
70
  )
71
- emit_call(cfg, _standard_list_call_after_query_filter(raw, filtered, query=query))
71
+ emit_call(
72
+ cfg, _standard_list_call_after_query_filter(raw, filtered, query=query)
73
+ )
72
74
 
73
75
  @group.command(
74
76
  "cities",
@@ -127,4 +129,6 @@ def register_adcode_commands(group: click.Group) -> None:
127
129
  filtered = filter_region_rows_by_query(
128
130
  list(raw.payload), query, label_attr="city_name"
129
131
  )
130
- emit_call(cfg, _standard_list_call_after_query_filter(raw, filtered, query=query))
132
+ emit_call(
133
+ cfg, _standard_list_call_after_query_filter(raw, filtered, query=query)
134
+ )
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
  from typing import Any, Callable, Mapping, Type
9
9
 
10
10
  import click
11
- import httpx
11
+ import requests
12
12
  from pydantic import BaseModel
13
13
 
14
14
  from birdrecord_cli.client import BirdrecordApiError, BirdrecordCall, BirdrecordClient
@@ -119,7 +119,7 @@ class BirdrecordGroup(click.Group):
119
119
  if e.envelope is not None:
120
120
  emit_json(e.envelope, pretty=pretty)
121
121
  raise click.exceptions.Exit(1) from e
122
- except httpx.HTTPError as e:
122
+ except requests.exceptions.RequestException as e:
123
123
  click.echo(
124
124
  f"{_cli_txt('HTTP error:', 'HTTP 错误:')} {e}",
125
125
  err=True,
@@ -4,7 +4,13 @@ from __future__ import annotations
4
4
 
5
5
  import click
6
6
 
7
- from birdrecord_cli.cli.core import CliConfig, client_from_cfg, emit_enveloped_model, json_schema_text, with_client_config
7
+ from birdrecord_cli.cli.core import (
8
+ CliConfig,
9
+ client_from_cfg,
10
+ emit_enveloped_model,
11
+ json_schema_text,
12
+ with_client_config,
13
+ )
8
14
  from birdrecord_cli.i18n import _cli_txt
9
15
  from birdrecord_cli.models.client import ReportBundleResult
10
16
 
@@ -0,0 +1,282 @@
1
+ """CLI: chart search statistic and optional activity drill-down."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ import click
8
+ import requests
9
+ from click._utils import FLAG_NEEDS_VALUE
10
+
11
+ from birdrecord_cli.cli.core import (
12
+ CliConfig,
13
+ client_from_cfg,
14
+ emit_json,
15
+ json_schema_text_object,
16
+ parse_cli_body_json,
17
+ with_client_config,
18
+ )
19
+ from birdrecord_cli.i18n import _cli_txt
20
+ from birdrecord_cli.models.client import (
21
+ ChartActivityReportRow,
22
+ ChartActivityTaxonRow,
23
+ build_common_list_taxon_request,
24
+ build_common_page_activity_request,
25
+ )
26
+ from birdrecord_cli.models.cli import (
27
+ UnifiedSearchRequest,
28
+ UnifiedSearchResult,
29
+ coerce_unified_search_request,
30
+ unified_search_to_common_activity,
31
+ unified_search_to_region_chart,
32
+ )
33
+ from birdrecord_cli.cli.search.report_map import (
34
+ render_report_map_html,
35
+ upload_report_map_html,
36
+ write_report_map_html,
37
+ )
38
+
39
+
40
+ class OptionalValueFlagOption(click.Option):
41
+ """Flag option that optionally consumes a following value token."""
42
+
43
+ _previous_parser_process: Callable[[Any, Any], None]
44
+
45
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
46
+ self.optional_value = kwargs.pop("optional_value", None)
47
+ super().__init__(*args, **kwargs)
48
+
49
+ def add_to_parser(self, parser: Any, ctx: click.Context) -> None:
50
+ super().add_to_parser(parser, ctx)
51
+
52
+ def parser_process(value: Any, state: Any) -> None:
53
+ if value is FLAG_NEEDS_VALUE:
54
+ value = self.optional_value
55
+ self._previous_parser_process(value, state)
56
+
57
+ for opt in self.opts:
58
+ option = parser._long_opt.get(opt) or parser._short_opt.get(opt)
59
+ if option is not None:
60
+ self._previous_parser_process = option.process
61
+ option.process = parser_process
62
+ break
63
+
64
+
65
+ def register_search_commands(group: click.Group) -> None:
66
+ @group.command(
67
+ "search",
68
+ short_help=_cli_txt(
69
+ "Chart search statistic; optional activity --taxon / --report.",
70
+ "图表检索统计;可选活动下钻 --taxon / --report。",
71
+ ),
72
+ help=_cli_txt(
73
+ (
74
+ "Chart search: per-month breakdown and rolled-up totals (--body-json). "
75
+ "Add --taxon for species ranking and/or --report for paged cards; omit both to skip those calls."
76
+ ),
77
+ (
78
+ "图表检索:按月拆分与汇总(--body-json)。"
79
+ "需要活动下钻时加 --taxon(鸟种排行)和/或 --report(分页记录);两者都不传则不请求这两项。"
80
+ ),
81
+ ),
82
+ )
83
+ @click.option(
84
+ "--taxon",
85
+ "want_taxon",
86
+ is_flag=True,
87
+ help=_cli_txt(
88
+ "Include per-species record counts for the chart month (common/list).",
89
+ "包含图表月份内各鸟种记录数(common/list)。",
90
+ ),
91
+ )
92
+ @click.option(
93
+ "--report",
94
+ "want_report",
95
+ is_flag=True,
96
+ help=_cli_txt(
97
+ "Include paged observation report cards (common/page).",
98
+ "包含分页观鸟记录卡片(common/page)。",
99
+ ),
100
+ )
101
+ @click.option(
102
+ "--report-map",
103
+ "report_map",
104
+ cls=OptionalValueFlagOption,
105
+ is_flag=False,
106
+ required=False,
107
+ type=str,
108
+ optional_value="output/report_map.html",
109
+ metavar="[OUTPUT_HTML]",
110
+ help=_cli_txt(
111
+ "Generate REPORT MAP: local HTML path (default output/report_map.html) or ONLINE to upload and return URL.",
112
+ "生成 REPORT MAP:本地 HTML 路径(默认 output/report_map.html)或 ONLINE(上传并返回 URL)。",
113
+ ),
114
+ )
115
+ @click.option(
116
+ "--report-limit",
117
+ "report_limit",
118
+ default=None,
119
+ type=int,
120
+ help=_cli_txt(
121
+ "Max total report rows to fetch across pages (stops paging once reached); default = fetch all pages.",
122
+ "跨分页最多获取的记录条数上限(达到后停止翻页);默认获取全部分页。",
123
+ ),
124
+ )
125
+ @click.option(
126
+ "--body-json",
127
+ default=None,
128
+ help=_cli_txt(
129
+ "Unified filter JSON (UnifiedSearchRequest): chart fields plus optional taxon_month, report_month, outside_type for drill-down.",
130
+ "统一筛选 JSON(UnifiedSearchRequest):图表字段 + 下钻可选 taxon_month、report_month、outside_type。",
131
+ ),
132
+ )
133
+ @click.option(
134
+ "--schema",
135
+ is_flag=True,
136
+ help=_cli_txt(
137
+ "Print JSON Schemas for request (UnifiedSearchRequest) and response (UnifiedSearchResult) only (no HTTP).",
138
+ "仅打印请求(UnifiedSearchRequest)与响应(UnifiedSearchResult)的 JSON Schema(不发起 HTTP)。",
139
+ ),
140
+ )
141
+ @click.pass_context
142
+ @with_client_config
143
+ def cmd_search(
144
+ ctx: click.Context,
145
+ want_taxon: bool,
146
+ want_report: bool,
147
+ report_map: str | None,
148
+ report_limit: int | None,
149
+ body_json: str | None,
150
+ schema: bool,
151
+ ) -> None:
152
+ if report_map and not want_report:
153
+ raise click.UsageError(
154
+ _cli_txt(
155
+ "--report-map requires --report.",
156
+ "--report-map 需要与 --report 一起使用。",
157
+ )
158
+ )
159
+ if schema:
160
+ click.echo(
161
+ json_schema_text_object(
162
+ {
163
+ "request": UnifiedSearchRequest,
164
+ "response": UnifiedSearchResult,
165
+ }
166
+ )
167
+ )
168
+ return
169
+ cfg = ctx.obj
170
+ assert isinstance(cfg, CliConfig)
171
+ raw_body = parse_cli_body_json(body_json)
172
+ unified = coerce_unified_search_request(raw_body)
173
+ with client_from_cfg(cfg) as client:
174
+ stat_fetch = client.fetch_search_statistic(
175
+ unified_search_to_region_chart(unified),
176
+ collect_envelopes=cfg.envelope,
177
+ )
178
+ taxon_rows: list[ChartActivityTaxonRow] | None = None
179
+ report_rows: list[ChartActivityReportRow] | None = None
180
+ envelopes = dict(stat_fetch.envelopes)
181
+ if want_taxon or want_report:
182
+ base = unified_search_to_common_activity(unified)
183
+ if want_taxon:
184
+ tcall = client.common_list_activity_taxon(
185
+ build_common_list_taxon_request(base)
186
+ )
187
+ taxon_rows = tcall.payload
188
+ if cfg.envelope:
189
+ envelopes["taxon"] = tcall.envelope.model_dump()
190
+ if want_report:
191
+ report_rows = []
192
+ page = 1
193
+ while True:
194
+ rcall = client.common_page_activity(
195
+ build_common_page_activity_request(
196
+ base,
197
+ report_month=unified.report_month,
198
+ start=page,
199
+ )
200
+ )
201
+ batch = rcall.payload or []
202
+ report_rows.extend(batch)
203
+ if cfg.envelope:
204
+ envelopes[f"report_page_{page}"] = (
205
+ rcall.envelope.model_dump()
206
+ )
207
+ # Stop if no rows returned
208
+ if not batch:
209
+ break
210
+ # Stop if reached the limit
211
+ if (
212
+ report_limit is not None
213
+ and len(report_rows) >= report_limit
214
+ ):
215
+ report_rows = report_rows[:report_limit]
216
+ break
217
+ # Stop if we've consumed all available pages
218
+ env = rcall.envelope
219
+ total = getattr(env, "total", None)
220
+ size = getattr(env, "size", None) or len(batch)
221
+ if total is not None and len(report_rows) >= total:
222
+ break
223
+ if len(batch) < size:
224
+ break
225
+ page += 1
226
+ report_map_ref: str | None = None
227
+ if report_map and report_rows is not None:
228
+ if report_map.strip().casefold() == "online":
229
+ html = render_report_map_html(
230
+ report_rows,
231
+ province=unified.province,
232
+ city=unified.city,
233
+ district=unified.district,
234
+ )
235
+ try:
236
+ report_map_ref = upload_report_map_html(
237
+ html,
238
+ timeout=cfg.timeout,
239
+ )
240
+ except (requests.RequestException, ValueError) as exc:
241
+ raise click.ClickException(
242
+ _cli_txt(
243
+ f"Failed to upload REPORT MAP ONLINE: {exc}",
244
+ f"REPORT MAP ONLINE 上传失败:{exc}",
245
+ )
246
+ ) from exc
247
+ click.echo(
248
+ _cli_txt(
249
+ f"Generated REPORT MAP URL: {report_map_ref}",
250
+ f"已生成 REPORT MAP URL:{report_map_ref}",
251
+ ),
252
+ err=True,
253
+ )
254
+ else:
255
+ out_path = write_report_map_html(
256
+ report_rows,
257
+ output_path=report_map,
258
+ province=unified.province,
259
+ city=unified.city,
260
+ district=unified.district,
261
+ )
262
+ report_map_ref = str(out_path)
263
+ click.echo(
264
+ _cli_txt(
265
+ f"Generated REPORT MAP HTML: {out_path}",
266
+ f"已生成 REPORT MAP HTML:{out_path}",
267
+ ),
268
+ err=True,
269
+ )
270
+ out = UnifiedSearchResult(
271
+ statistic=stat_fetch.result,
272
+ taxon=taxon_rows,
273
+ report=report_rows,
274
+ report_map=report_map_ref,
275
+ )
276
+ if cfg.envelope:
277
+ emit_json(
278
+ {"envelope": envelopes, "payload": out.model_dump(mode="json")},
279
+ pretty=cfg.pretty,
280
+ )
281
+ else:
282
+ emit_json(out.model_dump(mode="json"), pretty=cfg.pretty)