fd-cn-gov 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 (30) hide show
  1. fd_cn_gov-0.1.1/LICENSE +21 -0
  2. fd_cn_gov-0.1.1/PKG-INFO +152 -0
  3. fd_cn_gov-0.1.1/README.md +124 -0
  4. fd_cn_gov-0.1.1/fd_cn_gov/__init__.py +6 -0
  5. fd_cn_gov-0.1.1/fd_cn_gov/build_registry.py +185 -0
  6. fd_cn_gov-0.1.1/fd_cn_gov/cli.py +119 -0
  7. fd_cn_gov-0.1.1/fd_cn_gov/registry/__init__.py +82 -0
  8. fd_cn_gov-0.1.1/fd_cn_gov/registry/registry.db +0 -0
  9. fd_cn_gov-0.1.1/fd_cn_gov/registry/registry.json +2195 -0
  10. fd_cn_gov-0.1.1/fd_cn_gov/scraw_contract.py +147 -0
  11. fd_cn_gov-0.1.1/fd_cn_gov/scripts/__init__.py +0 -0
  12. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mee_gsgg_archive.py +158 -0
  13. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mem_tzgg_archive.py +142 -0
  14. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mnr_tzgg_archive.py +156 -0
  15. fd_cn_gov-0.1.1/fd_cn_gov/scripts/moa_govpublic_archive.py +170 -0
  16. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mof_gkml_archive.py +191 -0
  17. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mofcom_xwfb_archive.py +128 -0
  18. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mohurd_xinwen_archive.py +189 -0
  19. fd_cn_gov-0.1.1/fd_cn_gov/scripts/mot_shuju_archive.py +182 -0
  20. fd_cn_gov-0.1.1/fd_cn_gov/scripts/ndrc_tzgg_archive.py +157 -0
  21. fd_cn_gov-0.1.1/fd_cn_gov/scripts/pbc_xinwen_archive.py +169 -0
  22. fd_cn_gov-0.1.1/fd_cn_gov/scripts/safe_whxw_archive.py +148 -0
  23. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/PKG-INFO +152 -0
  24. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/SOURCES.txt +28 -0
  25. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/dependency_links.txt +1 -0
  26. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/entry_points.txt +2 -0
  27. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/requires.txt +2 -0
  28. fd_cn_gov-0.1.1/fd_cn_gov.egg-info/top_level.txt +1 -0
  29. fd_cn_gov-0.1.1/pyproject.toml +48 -0
  30. fd_cn_gov-0.1.1/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cli-anything
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.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: fd-cn-gov
3
+ Version: 0.1.1
4
+ Summary: Scrapers + datasource registry for Chinese central-government ministry open-information archives.
5
+ Author: cli-anything
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/FindDataOfficial/cn-goverment-datasource
8
+ Project-URL: Repository, https://github.com/FindDataOfficial/cn-goverment-datasource
9
+ Project-URL: Changelog, https://github.com/FindDataOfficial/cn-goverment-datasource/releases
10
+ Project-URL: Issues, https://github.com/FindDataOfficial/cn-goverment-datasource/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: scrapling[fetchers]
26
+ Requires-Dist: sqlalchemy>=2.0
27
+ Dynamic: license-file
28
+
29
+ # fd-cn-gov
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/fd-cn-gov.svg)](https://pypi.org/project/fd-cn-gov/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/fd-cn-gov.svg)](https://pypi.org/project/fd-cn-gov/)
33
+ [![License: MIT](https://img.shields.io/pypi/l/fd-cn-gov.svg)](https://github.com/FindDataOfficial/cn-goverment-datasource/blob/main/LICENSE)
34
+
35
+ Scrapers + a self-contained datasource registry for **Chinese central-government ministry open-information archives**. Catalog-crawls the public notice / news / data archives of 11 ministries (MOF, PBC, NDRC, MOFCOM, MOHURD, MOT, MOA, SAFE, MNR, MEE, MEM), emitting one JSON record per listed document, and ships a SQLite + JSON registry describing every datasource and its column schema.
36
+
37
+ Standalone: no monorepo, no MCP server, no `daas.db` dependency. The `MANIFEST` at the top of each scraper is the single source of truth; the bundled `registry.db` / `registry.json` are derived artifacts.
38
+
39
+ ## Ministries
40
+
41
+ | Name | Label | Seed URL |
42
+ |------|-------|----------|
43
+ | `mee_gsgg_archive` | MEE Notice Archive (生态环境部公示公告) | https://www.mee.gov.cn/ywdt/gsgg/ |
44
+ | `mem_tzgg_archive` | MEM Notice Archive (应急管理部通知公告) | https://www.mem.gov.cn/gk/tzgg/ |
45
+ | `mnr_tzgg_archive` | MNR Notice Archive (自然资源部通知公告) | https://www.mnr.gov.cn/gk/tzgg/ |
46
+ | `moa_govpublic_archive` | MOA GovPublic Archive (农业农村部 机构分类) | https://www.moa.gov.cn/govpublic/ |
47
+ | `mof_gkml_archive` | MOF gkml Archive (财政部信息公开) | https://www.mof.gov.cn/gkml/ |
48
+ | `mofcom_xwfb_archive` | MOFCOM News Preview (商务部新闻发布) | https://www.mofcom.gov.cn/xwfb/index.html |
49
+ | `mohurd_xinwen_archive` | MOHURD Xinwen Archive (住建部新闻动态) | https://www.mohurd.gov.cn/xinwen/ |
50
+ | `mot_shuju_archive` | MOT Data Hub Archive (交通运输部数据) | https://www.mot.gov.cn/shuju/index.html |
51
+ | `ndrc_tzgg_archive` | NDRC Notice Archive (发改委通知公告) | https://www.ndrc.gov.cn/xwdt/tzgg/ |
52
+ | `pbc_xinwen_archive` | PBC News Archive (人民银行新闻发布) | https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html |
53
+ | `safe_whxw_archive` | SAFE News Archive (外汇局外汇新闻) | https://www.safe.gov.cn/safe/whxw/index.html |
54
+
55
+ Each scraper emits a JSON array of records to stdout with at least `title`, `date` (`YYYY-MM-DD`, from the URL `t<YYYYMMDD>_` token with a `<span>` fallback), and `url` (absolute, the primary key). Some add `section` / `subsection` / `doc_type` / `department`. See `fd-cn-gov describe <name>` for the exact columns of any source.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install fd-cn-gov
61
+ ```
62
+
63
+ Requires Python ≥3.10. Dependencies: `scrapling` (HTTP + adaptive parsing), `sqlalchemy`.
64
+
65
+ ### From source
66
+
67
+ ```bash
68
+ pip install git+https://github.com/FindDataOfficial/cn-goverment-datasource.git
69
+ ```
70
+
71
+ ## CLI
72
+
73
+ ```bash
74
+ # List the 11 registered sources
75
+ fd-cn-gov list
76
+
77
+ # Show one source's identity + full column schema
78
+ fd-cn-gov describe mof_gkml_archive
79
+
80
+ # Crawl one archive (default: 50 pages per sub-archive; prints JSON to stdout)
81
+ fd-cn-gov crawl mof_gkml_archive --max-pages 2 > records.json
82
+
83
+ # Full crawl, no page cap (use sparingly — see Polite crawling below)
84
+ fd-cn-gov crawl mof_gkml_archive --all > records.json
85
+
86
+ # Regenerate the bundled registry from the scripts' MANIFESTs
87
+ fd-cn-gov build-registry
88
+ ```
89
+
90
+ ## Python API
91
+
92
+ ```python
93
+ import fd_cn_gov
94
+
95
+ # Discover datasources
96
+ for s in fd_cn_gov.list_sources():
97
+ print(s.name, s.label, s.url)
98
+
99
+ # One source + its column schema
100
+ src = fd_cn_gov.get_source("mof_gkml_archive")
101
+ cols = fd_cn_gov.get_columns("mof_gkml_archive") # -> list[Column]
102
+ # Column(name='url', type='string', primary_key=True, nullable=False,
103
+ # description='absolute document URL (.htm/.html/.pdf)',
104
+ # source_field='a@href', semantic_type='url', ...)
105
+ ```
106
+
107
+ ## Registry schema
108
+
109
+ The bundled `fd_cn_gov/registry/registry.db` is a 3-table SQLite, mirroring the column shapes of the originating `daas.db` for these sources (no foreign keys, no stale-FK footgun):
110
+
111
+ - **`sources`** — one row per ministry: `id, name, label, url, description, category, category_label, config_json`
112
+ - **`datasource_columns`** — one row per output column: `datasource_id, table_name, column_name, column_type, is_primary_key, is_nullable, description, source_field, unit, semantic_type`
113
+ - **`scraw_configs`** — one row per scraper: `name, url, columns_json`
114
+
115
+ `registry.json` is a deterministic full dump of the same three tables (`sort_keys`), so the registry is diff-friendly in code review.
116
+
117
+ To regenerate after editing a `MANIFEST`:
118
+
119
+ ```bash
120
+ fd-cn-gov build-registry
121
+ ```
122
+
123
+ The build is **logically idempotent**: re-running produces an identical `.dump` and a byte-identical `registry.json`. (Only SQLite's file-change-counter header byte differs on each write — unavoidable on any SQLite write; compare via `.dump` or `registry.json` for equality.)
124
+
125
+ ## Polite crawling
126
+
127
+ These are `.gov.cn` hosts. Each scraper paces requests (`SLEEP ≈ 0.3s` between pages) and defaults to a **50-page cap per sub-archive**. Use `--max-pages N` to bound a crawl further; reserve `--all` for when you genuinely need full history and can afford the time. Do not run multiple `--all` crawls in parallel against the same ministry.
128
+
129
+ ## Project layout
130
+
131
+ ```
132
+ fd-cn-gov/
133
+ ├── pyproject.toml
134
+ ├── README.md
135
+ ├── LICENSE
136
+ └── fd_cn_gov/
137
+ ├── __init__.py # public read API: list_sources / get_source / get_columns
138
+ ├── cli.py # fd-cn-gov CLI
139
+ ├── scraw_contract.py # ScrawManifest / ScrawColumn dataclasses (vendored, trimmed)
140
+ ├── build_registry.py # regenerates registry.db + registry.json from MANIFESTs
141
+ ├── scripts/ # 11 ministry scrapers, each with a module-level MANIFEST
142
+ │ ├── mof_gkml_archive.py
143
+ │ └── ...
144
+ └── registry/
145
+ ├── __init__.py # read API over the bundled DB
146
+ ├── registry.db # generated — checked in
147
+ └── registry.json # generated — checked in
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,124 @@
1
+ # fd-cn-gov
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/fd-cn-gov.svg)](https://pypi.org/project/fd-cn-gov/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/fd-cn-gov.svg)](https://pypi.org/project/fd-cn-gov/)
5
+ [![License: MIT](https://img.shields.io/pypi/l/fd-cn-gov.svg)](https://github.com/FindDataOfficial/cn-goverment-datasource/blob/main/LICENSE)
6
+
7
+ Scrapers + a self-contained datasource registry for **Chinese central-government ministry open-information archives**. Catalog-crawls the public notice / news / data archives of 11 ministries (MOF, PBC, NDRC, MOFCOM, MOHURD, MOT, MOA, SAFE, MNR, MEE, MEM), emitting one JSON record per listed document, and ships a SQLite + JSON registry describing every datasource and its column schema.
8
+
9
+ Standalone: no monorepo, no MCP server, no `daas.db` dependency. The `MANIFEST` at the top of each scraper is the single source of truth; the bundled `registry.db` / `registry.json` are derived artifacts.
10
+
11
+ ## Ministries
12
+
13
+ | Name | Label | Seed URL |
14
+ |------|-------|----------|
15
+ | `mee_gsgg_archive` | MEE Notice Archive (生态环境部公示公告) | https://www.mee.gov.cn/ywdt/gsgg/ |
16
+ | `mem_tzgg_archive` | MEM Notice Archive (应急管理部通知公告) | https://www.mem.gov.cn/gk/tzgg/ |
17
+ | `mnr_tzgg_archive` | MNR Notice Archive (自然资源部通知公告) | https://www.mnr.gov.cn/gk/tzgg/ |
18
+ | `moa_govpublic_archive` | MOA GovPublic Archive (农业农村部 机构分类) | https://www.moa.gov.cn/govpublic/ |
19
+ | `mof_gkml_archive` | MOF gkml Archive (财政部信息公开) | https://www.mof.gov.cn/gkml/ |
20
+ | `mofcom_xwfb_archive` | MOFCOM News Preview (商务部新闻发布) | https://www.mofcom.gov.cn/xwfb/index.html |
21
+ | `mohurd_xinwen_archive` | MOHURD Xinwen Archive (住建部新闻动态) | https://www.mohurd.gov.cn/xinwen/ |
22
+ | `mot_shuju_archive` | MOT Data Hub Archive (交通运输部数据) | https://www.mot.gov.cn/shuju/index.html |
23
+ | `ndrc_tzgg_archive` | NDRC Notice Archive (发改委通知公告) | https://www.ndrc.gov.cn/xwdt/tzgg/ |
24
+ | `pbc_xinwen_archive` | PBC News Archive (人民银行新闻发布) | https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html |
25
+ | `safe_whxw_archive` | SAFE News Archive (外汇局外汇新闻) | https://www.safe.gov.cn/safe/whxw/index.html |
26
+
27
+ Each scraper emits a JSON array of records to stdout with at least `title`, `date` (`YYYY-MM-DD`, from the URL `t<YYYYMMDD>_` token with a `<span>` fallback), and `url` (absolute, the primary key). Some add `section` / `subsection` / `doc_type` / `department`. See `fd-cn-gov describe <name>` for the exact columns of any source.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install fd-cn-gov
33
+ ```
34
+
35
+ Requires Python ≥3.10. Dependencies: `scrapling` (HTTP + adaptive parsing), `sqlalchemy`.
36
+
37
+ ### From source
38
+
39
+ ```bash
40
+ pip install git+https://github.com/FindDataOfficial/cn-goverment-datasource.git
41
+ ```
42
+
43
+ ## CLI
44
+
45
+ ```bash
46
+ # List the 11 registered sources
47
+ fd-cn-gov list
48
+
49
+ # Show one source's identity + full column schema
50
+ fd-cn-gov describe mof_gkml_archive
51
+
52
+ # Crawl one archive (default: 50 pages per sub-archive; prints JSON to stdout)
53
+ fd-cn-gov crawl mof_gkml_archive --max-pages 2 > records.json
54
+
55
+ # Full crawl, no page cap (use sparingly — see Polite crawling below)
56
+ fd-cn-gov crawl mof_gkml_archive --all > records.json
57
+
58
+ # Regenerate the bundled registry from the scripts' MANIFESTs
59
+ fd-cn-gov build-registry
60
+ ```
61
+
62
+ ## Python API
63
+
64
+ ```python
65
+ import fd_cn_gov
66
+
67
+ # Discover datasources
68
+ for s in fd_cn_gov.list_sources():
69
+ print(s.name, s.label, s.url)
70
+
71
+ # One source + its column schema
72
+ src = fd_cn_gov.get_source("mof_gkml_archive")
73
+ cols = fd_cn_gov.get_columns("mof_gkml_archive") # -> list[Column]
74
+ # Column(name='url', type='string', primary_key=True, nullable=False,
75
+ # description='absolute document URL (.htm/.html/.pdf)',
76
+ # source_field='a@href', semantic_type='url', ...)
77
+ ```
78
+
79
+ ## Registry schema
80
+
81
+ The bundled `fd_cn_gov/registry/registry.db` is a 3-table SQLite, mirroring the column shapes of the originating `daas.db` for these sources (no foreign keys, no stale-FK footgun):
82
+
83
+ - **`sources`** — one row per ministry: `id, name, label, url, description, category, category_label, config_json`
84
+ - **`datasource_columns`** — one row per output column: `datasource_id, table_name, column_name, column_type, is_primary_key, is_nullable, description, source_field, unit, semantic_type`
85
+ - **`scraw_configs`** — one row per scraper: `name, url, columns_json`
86
+
87
+ `registry.json` is a deterministic full dump of the same three tables (`sort_keys`), so the registry is diff-friendly in code review.
88
+
89
+ To regenerate after editing a `MANIFEST`:
90
+
91
+ ```bash
92
+ fd-cn-gov build-registry
93
+ ```
94
+
95
+ The build is **logically idempotent**: re-running produces an identical `.dump` and a byte-identical `registry.json`. (Only SQLite's file-change-counter header byte differs on each write — unavoidable on any SQLite write; compare via `.dump` or `registry.json` for equality.)
96
+
97
+ ## Polite crawling
98
+
99
+ These are `.gov.cn` hosts. Each scraper paces requests (`SLEEP ≈ 0.3s` between pages) and defaults to a **50-page cap per sub-archive**. Use `--max-pages N` to bound a crawl further; reserve `--all` for when you genuinely need full history and can afford the time. Do not run multiple `--all` crawls in parallel against the same ministry.
100
+
101
+ ## Project layout
102
+
103
+ ```
104
+ fd-cn-gov/
105
+ ├── pyproject.toml
106
+ ├── README.md
107
+ ├── LICENSE
108
+ └── fd_cn_gov/
109
+ ├── __init__.py # public read API: list_sources / get_source / get_columns
110
+ ├── cli.py # fd-cn-gov CLI
111
+ ├── scraw_contract.py # ScrawManifest / ScrawColumn dataclasses (vendored, trimmed)
112
+ ├── build_registry.py # regenerates registry.db + registry.json from MANIFESTs
113
+ ├── scripts/ # 11 ministry scrapers, each with a module-level MANIFEST
114
+ │ ├── mof_gkml_archive.py
115
+ │ └── ...
116
+ └── registry/
117
+ ├── __init__.py # read API over the bundled DB
118
+ ├── registry.db # generated — checked in
119
+ └── registry.json # generated — checked in
120
+ ```
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,6 @@
1
+ """fd_cn_gov — ministry open-information scrapers + datasource registry."""
2
+
3
+ from fd_cn_gov.registry import list_sources, get_source, get_columns
4
+
5
+ __all__ = ["list_sources", "get_source", "get_columns"]
6
+ __version__ = "0.1.1"
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+ """Regenerate the bundled registry.db + registry.json from the scripts' MANIFESTs.
3
+
4
+ The MANIFEST is the source of truth; the DB is derived. Idempotent: re-running
5
+ produces a byte-identical DB (delete-then-insert per source, deterministic
6
+ JSON sort_keys).
7
+
8
+ Usage: `python -m fd_cn_gov.build_registry` or `fd-cn-gov build-registry`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import importlib
13
+ import json
14
+ import sqlite3
15
+ from pathlib import Path
16
+
17
+ # The 11 ministry scrapers, alphabetical by module name (deterministic order).
18
+ SCRIPT_NAMES = [
19
+ "mee_gsgg_archive",
20
+ "mem_tzgg_archive",
21
+ "mnr_tzgg_archive",
22
+ "moa_govpublic_archive",
23
+ "mof_gkml_archive",
24
+ "mofcom_xwfb_archive",
25
+ "mohurd_xinwen_archive",
26
+ "mot_shuju_archive",
27
+ "ndrc_tzgg_archive",
28
+ "pbc_xinwen_archive",
29
+ "safe_whxw_archive",
30
+ ]
31
+
32
+ REGISTRY_DIR = Path(__file__).resolve().parent / "registry"
33
+ DB_PATH = REGISTRY_DIR / "registry.db"
34
+ JSON_PATH = REGISTRY_DIR / "registry.json"
35
+
36
+
37
+ def _load_manifests() -> list:
38
+ manifests = []
39
+ for name in SCRIPT_NAMES:
40
+ mod = importlib.import_module(f"fd_cn_gov.scripts.{name}")
41
+ m = getattr(mod, "MANIFEST", None)
42
+ if m is None or m.name != name:
43
+ raise RuntimeError(f"{name}: MANIFEST missing or name mismatch")
44
+ manifests.append(m)
45
+ return manifests
46
+
47
+
48
+ def _create_schema(conn: sqlite3.Connection) -> None:
49
+ conn.executescript(
50
+ """
51
+ DROP TABLE IF EXISTS datasource_columns;
52
+ DROP TABLE IF EXISTS scraw_configs;
53
+ DROP TABLE IF EXISTS sources;
54
+
55
+ CREATE TABLE sources (
56
+ id INTEGER PRIMARY KEY,
57
+ name TEXT NOT NULL UNIQUE,
58
+ label TEXT NOT NULL,
59
+ url TEXT NOT NULL,
60
+ description TEXT NOT NULL DEFAULT '',
61
+ category TEXT NOT NULL DEFAULT '',
62
+ category_label TEXT NOT NULL DEFAULT '',
63
+ config_json TEXT NOT NULL DEFAULT '{}'
64
+ );
65
+
66
+ CREATE TABLE datasource_columns (
67
+ id INTEGER PRIMARY KEY,
68
+ datasource_id INTEGER NOT NULL,
69
+ table_name TEXT NOT NULL,
70
+ column_name TEXT NOT NULL,
71
+ column_type TEXT NOT NULL,
72
+ is_primary_key INTEGER NOT NULL DEFAULT 0,
73
+ is_nullable INTEGER NOT NULL DEFAULT 1,
74
+ description TEXT NOT NULL DEFAULT '',
75
+ source_field TEXT NOT NULL DEFAULT '',
76
+ unit TEXT NOT NULL DEFAULT '',
77
+ semantic_type TEXT NOT NULL DEFAULT ''
78
+ );
79
+
80
+ CREATE TABLE scraw_configs (
81
+ id INTEGER PRIMARY KEY,
82
+ name TEXT NOT NULL UNIQUE,
83
+ url TEXT NOT NULL,
84
+ columns_json TEXT NOT NULL DEFAULT '[]'
85
+ );
86
+ """
87
+ )
88
+
89
+
90
+ def _write_source(conn: sqlite3.Connection, source_id: int, m) -> None:
91
+ conn.execute(
92
+ """INSERT INTO sources
93
+ (id, name, label, url, description, category, category_label, config_json)
94
+ VALUES (?,?,?,?,?,?,?,?)""",
95
+ (
96
+ source_id, m.name, m.label, m.url, m.description,
97
+ m.category, m.category_label, m.to_config_json(),
98
+ ),
99
+ )
100
+ cols = m.to_columns_json()
101
+ for c in cols:
102
+ conn.execute(
103
+ """INSERT INTO datasource_columns
104
+ (datasource_id, table_name, column_name, column_type,
105
+ is_primary_key, is_nullable, description, source_field,
106
+ unit, semantic_type)
107
+ VALUES (?,?,?,?,?,?,?,?,?,?)""",
108
+ (
109
+ source_id, c["table_name"], c["column_name"], c["column_type"],
110
+ c["is_primary_key"], c["is_nullable"], c["description"],
111
+ c["source_field"], c["unit"], c["semantic_type"],
112
+ ),
113
+ )
114
+ conn.execute(
115
+ "INSERT INTO scraw_configs (name, url, columns_json) VALUES (?,?,?)",
116
+ (m.name, m.url, json.dumps(m.to_scraw_columns(), ensure_ascii=False)),
117
+ )
118
+
119
+
120
+ def _dump_json(conn: sqlite3.Connection, manifests: list) -> dict:
121
+ """Deterministic full dump for registry.json (sort_keys, ensure_ascii=False)."""
122
+ sources = [
123
+ {
124
+ "id": i + 1,
125
+ "name": m.name,
126
+ "label": m.label,
127
+ "url": m.url,
128
+ "description": m.description,
129
+ "category": m.category,
130
+ "category_label": m.category_label,
131
+ "config": m.to_config(),
132
+ }
133
+ for i, m in enumerate(manifests)
134
+ ]
135
+ cols = []
136
+ with conn:
137
+ for row in conn.execute(
138
+ """SELECT datasource_id, table_name, column_name, column_type,
139
+ is_primary_key, is_nullable, description, source_field,
140
+ unit, semantic_type
141
+ FROM datasource_columns ORDER BY datasource_id, rowid"""
142
+ ):
143
+ cols.append(dict(row))
144
+ scraw = []
145
+ with conn:
146
+ for row in conn.execute(
147
+ "SELECT name, url, columns_json FROM scraw_configs ORDER BY name"
148
+ ):
149
+ d = dict(row)
150
+ d["columns_json"] = json.loads(d["columns_json"])
151
+ scraw.append(d)
152
+ return {"sources": sources, "datasource_columns": cols, "scraw_configs": scraw}
153
+
154
+
155
+ def build() -> str:
156
+ manifests = _load_manifests()
157
+ REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
158
+ conn = sqlite3.connect(DB_PATH)
159
+ conn.row_factory = sqlite3.Row
160
+ try:
161
+ with conn:
162
+ _create_schema(conn)
163
+ for i, m in enumerate(manifests, start=1):
164
+ _write_source(conn, i, m)
165
+ dump = _dump_json(conn, manifests)
166
+ finally:
167
+ conn.close()
168
+ JSON_PATH.write_text(
169
+ json.dumps(dump, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
170
+ encoding="utf-8",
171
+ )
172
+ n_cols = sum(len(m.to_columns_json()) for m in manifests)
173
+ return (
174
+ f"built {DB_PATH} + {JSON_PATH}: "
175
+ f"{len(manifests)} sources, {n_cols} columns"
176
+ )
177
+
178
+
179
+ def main() -> int:
180
+ print(build())
181
+ return 0
182
+
183
+
184
+ if __name__ == "__main__":
185
+ raise SystemExit(main())
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """fd-cn-gov CLI: crawl, list, describe, build-registry.
3
+
4
+ Usage:
5
+ fd-cn-gov crawl <name> [--max-pages N | --all] # run a scraper
6
+ fd-cn-gov list # list registered sources
7
+ fd-cn-gov describe <name> # show a source + its columns
8
+ fd-cn-gov build-registry # regenerate registry.db/json
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import importlib
14
+ import sys
15
+ from typing import Optional
16
+
17
+ from fd_cn_gov.registry import list_sources, get_source, get_columns
18
+
19
+ SCRIPT_NAMES = [
20
+ "mee_gsgg_archive", "mem_tzgg_archive", "mnr_tzgg_archive",
21
+ "moa_govpublic_archive", "mof_gkml_archive", "mofcom_xwfb_archive",
22
+ "mohurd_xinwen_archive", "mot_shuju_archive", "ndrc_tzgg_archive",
23
+ "pbc_xinwen_archive", "safe_whxw_archive",
24
+ ]
25
+
26
+
27
+ def _cmd_crawl(args) -> int:
28
+ name = args.name
29
+ if name not in SCRIPT_NAMES:
30
+ print(
31
+ f"error: unknown scraper {name!r}\navailable: {' '.join(SCRIPT_NAMES)}",
32
+ file=sys.stderr,
33
+ )
34
+ return 2
35
+ # Forward the page-cap flags to the script's argparse via synthesized argv.
36
+ script_argv = [name]
37
+ if args.all:
38
+ script_argv.append("--all")
39
+ elif args.max_pages is not None:
40
+ script_argv += ["--max-pages", str(args.max_pages)]
41
+ mod = importlib.import_module(f"fd_cn_gov.scripts.{name}")
42
+ old = sys.argv
43
+ sys.argv = script_argv
44
+ try:
45
+ mod.main()
46
+ finally:
47
+ sys.argv = old
48
+ return 0
49
+
50
+
51
+ def _cmd_list(args) -> int:
52
+ for s in list_sources():
53
+ print(f"{s.name}\t{s.label}\t{s.url}")
54
+ return 0
55
+
56
+
57
+ def _cmd_describe(args) -> int:
58
+ try:
59
+ s = get_source(args.name)
60
+ except KeyError as e:
61
+ print(f"error: {e}", file=sys.stderr)
62
+ return 2
63
+ print(f"name: {s.name}")
64
+ print(f"label: {s.label}")
65
+ print(f"url: {s.url}")
66
+ print(f"category: {s.category}")
67
+ print(f"description: {s.description}")
68
+ print("columns:")
69
+ for c in get_columns(args.name):
70
+ pk = " [PK]" if c.primary_key else ""
71
+ nn = "" if c.nullable else " NOT NULL"
72
+ print(f" - {c.name}: {c.type}{pk}{nn} ({c.semantic_type or '-'})")
73
+ if c.description:
74
+ print(f" {c.description}")
75
+ if c.source_field:
76
+ print(f" source: {c.source_field}")
77
+ return 0
78
+
79
+
80
+ def _cmd_build_registry(args) -> int:
81
+ from fd_cn_gov.build_registry import build
82
+ print(build())
83
+ return 0
84
+
85
+
86
+ def build_parser() -> argparse.ArgumentParser:
87
+ ap = argparse.ArgumentParser(
88
+ prog="fd-cn-gov",
89
+ description="Chinese ministry open-information scrapers + registry.",
90
+ )
91
+ sub = ap.add_subparsers(dest="cmd", required=True)
92
+
93
+ p_crawl = sub.add_parser("crawl", help="run a scraper; prints JSON records to stdout")
94
+ p_crawl.add_argument("name", choices=None, help="scraper name (e.g. mof_gkml_archive)")
95
+ pg = p_crawl.add_mutually_exclusive_group()
96
+ pg.add_argument("--max-pages", type=int, help="pages per archive (default: script default, usually 50)")
97
+ pg.add_argument("--all", action="store_true", help="full crawl, no page cap")
98
+ p_crawl.set_defaults(func=_cmd_crawl)
99
+
100
+ p_list = sub.add_parser("list", help="list registered sources")
101
+ p_list.set_defaults(func=_cmd_list)
102
+
103
+ p_desc = sub.add_parser("describe", help="show a source + its column schema")
104
+ p_desc.add_argument("name", help="scraper name")
105
+ p_desc.set_defaults(func=_cmd_describe)
106
+
107
+ p_build = sub.add_parser("build-registry", help="regenerate registry.db + registry.json from MANIFESTs")
108
+ p_build.set_defaults(func=_cmd_build_registry)
109
+ return ap
110
+
111
+
112
+ def main(argv: Optional[list[str]] = None) -> int:
113
+ ap = build_parser()
114
+ args = ap.parse_args(argv)
115
+ return args.func(args)
116
+
117
+
118
+ if __name__ == "__main__":
119
+ raise SystemExit(main())