nl-voting-data-scraper 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nl_voting_data_scraper-0.1.0/.github/workflows/publish.yml +37 -0
- nl_voting_data_scraper-0.1.0/.gitignore +19 -0
- nl_voting_data_scraper-0.1.0/LICENSE +21 -0
- nl_voting_data_scraper-0.1.0/PKG-INFO +178 -0
- nl_voting_data_scraper-0.1.0/README.md +149 -0
- nl_voting_data_scraper-0.1.0/examples/scrape_all_municipal.py +29 -0
- nl_voting_data_scraper-0.1.0/examples/scrape_national.py +27 -0
- nl_voting_data_scraper-0.1.0/examples/scrape_single.py +28 -0
- nl_voting_data_scraper-0.1.0/pyproject.toml +51 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/__init__.py +23 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/api_scraper.py +169 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/browser_scraper.py +205 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/cache.py +82 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/cli.py +167 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/config.py +115 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/decoder.py +98 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/models.py +108 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/output.py +111 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/rate_limiter.py +39 -0
- nl_voting_data_scraper-0.1.0/src/nl_voting_data_scraper/scraper.py +196 -0
- nl_voting_data_scraper-0.1.0/tests/__init__.py +0 -0
- nl_voting_data_scraper-0.1.0/tests/conftest.py +30 -0
- nl_voting_data_scraper-0.1.0/tests/fixtures/sample_index.json +42 -0
- nl_voting_data_scraper-0.1.0/tests/fixtures/sample_municipality.json +162 -0
- nl_voting_data_scraper-0.1.0/tests/test_cache.py +47 -0
- nl_voting_data_scraper-0.1.0/tests/test_cli.py +33 -0
- nl_voting_data_scraper-0.1.0/tests/test_decoder.py +68 -0
- nl_voting_data_scraper-0.1.0/tests/test_models.py +80 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
- name: Install build tools
|
|
17
|
+
run: pip install build
|
|
18
|
+
- name: Build package
|
|
19
|
+
run: python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
name: Publish to PyPI
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rehan Fazal
|
|
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,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nl-voting-data-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scrape Dutch voting advice (StemWijzer) data for any election
|
|
5
|
+
Author: Rehan Fazal
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: dutch,elections,scraper,stemwijzer,votematch
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: click>=8.3
|
|
16
|
+
Requires-Dist: httpx>=0.28
|
|
17
|
+
Requires-Dist: pycryptodome>=3.23
|
|
18
|
+
Requires-Dist: pydantic>=2.12
|
|
19
|
+
Requires-Dist: rich>=14.0
|
|
20
|
+
Requires-Dist: tenacity>=9.1
|
|
21
|
+
Provides-Extra: browser
|
|
22
|
+
Requires-Dist: playwright>=1.58; extra == 'browser'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=1.3; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-httpx>=0.36; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# nl-voting-data-scraper
|
|
31
|
+
|
|
32
|
+
Scrape Dutch voting advice ([StemWijzer](https://stemwijzer.nl)) data for any election — municipal, national, European, or provincial.
|
|
33
|
+
|
|
34
|
+
Outputs structured JSON with party positions, policy statements, and metadata. Reusable across election cycles.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nl-voting-data-scraper
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For browser automation fallback (optional):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install "nl-voting-data-scraper[browser]"
|
|
46
|
+
playwright install chromium
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# List known elections
|
|
55
|
+
nl-voting-data-scraper list-elections
|
|
56
|
+
|
|
57
|
+
# Scrape all municipalities for 2026 municipal elections
|
|
58
|
+
nl-voting-data-scraper scrape gr2026 -o ./output
|
|
59
|
+
|
|
60
|
+
# Scrape a specific municipality
|
|
61
|
+
nl-voting-data-scraper scrape gr2026 -m GM0014 -o ./output
|
|
62
|
+
|
|
63
|
+
# Scrape national election
|
|
64
|
+
nl-voting-data-scraper scrape tk2025 -o ./output
|
|
65
|
+
|
|
66
|
+
# List municipalities for an election
|
|
67
|
+
nl-voting-data-scraper list-municipalities gr2026
|
|
68
|
+
|
|
69
|
+
# Discover API endpoints
|
|
70
|
+
nl-voting-data-scraper discover gr2026
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Python Library
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from nl_voting_data_scraper import StemwijzerScraper
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with StemwijzerScraper("gr2026") as scraper:
|
|
81
|
+
# Scrape a single municipality
|
|
82
|
+
data = await scraper.scrape_one("GM0014")
|
|
83
|
+
print(f"{data.votematch.name}: {len(data.parties)} parties, {len(data.statements)} statements")
|
|
84
|
+
|
|
85
|
+
# Scrape all
|
|
86
|
+
results = await scraper.scrape()
|
|
87
|
+
print(f"Scraped {len(results)} entries")
|
|
88
|
+
|
|
89
|
+
asyncio.run(main())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Supported Elections
|
|
93
|
+
|
|
94
|
+
| Slug | Type | Year | Description |
|
|
95
|
+
|------|------|------|-------------|
|
|
96
|
+
| `gr2026` | Municipal | 2026 | Gemeenteraadsverkiezingen 2026 |
|
|
97
|
+
| `tk2025` | National | 2025 | Tweede Kamerverkiezingen 2025 |
|
|
98
|
+
| `tk2023` | National | 2023 | Tweede Kamerverkiezingen 2023 |
|
|
99
|
+
| `eu2024` | European | 2024 | Europees Parlement 2024 |
|
|
100
|
+
| `ps2023` | Provincial | 2023 | Provinciale Staten 2023 |
|
|
101
|
+
|
|
102
|
+
New elections are auto-detected from URL patterns. You can also pass custom election slugs.
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
**Hybrid approach:**
|
|
107
|
+
|
|
108
|
+
1. **API-first (fast):** Tries to fetch data from StemWijzer data endpoints via HTTP. Handles base64-encoded responses and optional AES decryption.
|
|
109
|
+
2. **Browser fallback:** If the API fails, uses Playwright to load the frontend, intercept network requests, and capture the data. Falls back to DOM extraction as a last resort.
|
|
110
|
+
|
|
111
|
+
## Output Format
|
|
112
|
+
|
|
113
|
+
Each municipality/election produces a JSON file:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"parties": [
|
|
118
|
+
{
|
|
119
|
+
"id": 206919,
|
|
120
|
+
"name": "Party Name",
|
|
121
|
+
"fullName": "Full Party Name",
|
|
122
|
+
"website": "https://...",
|
|
123
|
+
"hasSeats": true,
|
|
124
|
+
"statements": [
|
|
125
|
+
{ "id": 206987, "position": "agree", "explanation": "..." }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"statements": [
|
|
130
|
+
{
|
|
131
|
+
"id": 206987,
|
|
132
|
+
"theme": "Housing",
|
|
133
|
+
"title": "The municipality should build more affordable housing.",
|
|
134
|
+
"index": 1
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"shootoutStatements": [...],
|
|
138
|
+
"votematch": {
|
|
139
|
+
"id": 206918,
|
|
140
|
+
"name": "Municipality Name",
|
|
141
|
+
"context": "2026GR",
|
|
142
|
+
"remote_id": "GM0014",
|
|
143
|
+
"langcode": "nl"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## CLI Options
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
nl-voting-data-scraper scrape ELECTION [OPTIONS]
|
|
152
|
+
|
|
153
|
+
Options:
|
|
154
|
+
-m, --municipality TEXT Specific GM codes (repeatable)
|
|
155
|
+
-l, --language TEXT Languages to scrape (default: nl)
|
|
156
|
+
-o, --output TEXT Output directory (default: ./output)
|
|
157
|
+
--combined Also write combined.json
|
|
158
|
+
--rate-limit FLOAT Requests per second (default: 2.0)
|
|
159
|
+
--no-cache Disable caching
|
|
160
|
+
--resume Resume interrupted scrape
|
|
161
|
+
--browser-only Only use browser scraping
|
|
162
|
+
--api-only Only use API scraping
|
|
163
|
+
-v, --verbose Verbose output
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git clone https://github.com/rhnfzl/nl-voting-data-scraper.git
|
|
170
|
+
cd nl-voting-data-scraper
|
|
171
|
+
pip install -e ".[dev,browser]"
|
|
172
|
+
playwright install chromium
|
|
173
|
+
pytest
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# nl-voting-data-scraper
|
|
2
|
+
|
|
3
|
+
Scrape Dutch voting advice ([StemWijzer](https://stemwijzer.nl)) data for any election — municipal, national, European, or provincial.
|
|
4
|
+
|
|
5
|
+
Outputs structured JSON with party positions, policy statements, and metadata. Reusable across election cycles.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nl-voting-data-scraper
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For browser automation fallback (optional):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install "nl-voting-data-scraper[browser]"
|
|
17
|
+
playwright install chromium
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### CLI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# List known elections
|
|
26
|
+
nl-voting-data-scraper list-elections
|
|
27
|
+
|
|
28
|
+
# Scrape all municipalities for 2026 municipal elections
|
|
29
|
+
nl-voting-data-scraper scrape gr2026 -o ./output
|
|
30
|
+
|
|
31
|
+
# Scrape a specific municipality
|
|
32
|
+
nl-voting-data-scraper scrape gr2026 -m GM0014 -o ./output
|
|
33
|
+
|
|
34
|
+
# Scrape national election
|
|
35
|
+
nl-voting-data-scraper scrape tk2025 -o ./output
|
|
36
|
+
|
|
37
|
+
# List municipalities for an election
|
|
38
|
+
nl-voting-data-scraper list-municipalities gr2026
|
|
39
|
+
|
|
40
|
+
# Discover API endpoints
|
|
41
|
+
nl-voting-data-scraper discover gr2026
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Python Library
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from nl_voting_data_scraper import StemwijzerScraper
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
async with StemwijzerScraper("gr2026") as scraper:
|
|
52
|
+
# Scrape a single municipality
|
|
53
|
+
data = await scraper.scrape_one("GM0014")
|
|
54
|
+
print(f"{data.votematch.name}: {len(data.parties)} parties, {len(data.statements)} statements")
|
|
55
|
+
|
|
56
|
+
# Scrape all
|
|
57
|
+
results = await scraper.scrape()
|
|
58
|
+
print(f"Scraped {len(results)} entries")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Supported Elections
|
|
64
|
+
|
|
65
|
+
| Slug | Type | Year | Description |
|
|
66
|
+
|------|------|------|-------------|
|
|
67
|
+
| `gr2026` | Municipal | 2026 | Gemeenteraadsverkiezingen 2026 |
|
|
68
|
+
| `tk2025` | National | 2025 | Tweede Kamerverkiezingen 2025 |
|
|
69
|
+
| `tk2023` | National | 2023 | Tweede Kamerverkiezingen 2023 |
|
|
70
|
+
| `eu2024` | European | 2024 | Europees Parlement 2024 |
|
|
71
|
+
| `ps2023` | Provincial | 2023 | Provinciale Staten 2023 |
|
|
72
|
+
|
|
73
|
+
New elections are auto-detected from URL patterns. You can also pass custom election slugs.
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
**Hybrid approach:**
|
|
78
|
+
|
|
79
|
+
1. **API-first (fast):** Tries to fetch data from StemWijzer data endpoints via HTTP. Handles base64-encoded responses and optional AES decryption.
|
|
80
|
+
2. **Browser fallback:** If the API fails, uses Playwright to load the frontend, intercept network requests, and capture the data. Falls back to DOM extraction as a last resort.
|
|
81
|
+
|
|
82
|
+
## Output Format
|
|
83
|
+
|
|
84
|
+
Each municipality/election produces a JSON file:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"parties": [
|
|
89
|
+
{
|
|
90
|
+
"id": 206919,
|
|
91
|
+
"name": "Party Name",
|
|
92
|
+
"fullName": "Full Party Name",
|
|
93
|
+
"website": "https://...",
|
|
94
|
+
"hasSeats": true,
|
|
95
|
+
"statements": [
|
|
96
|
+
{ "id": 206987, "position": "agree", "explanation": "..." }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"statements": [
|
|
101
|
+
{
|
|
102
|
+
"id": 206987,
|
|
103
|
+
"theme": "Housing",
|
|
104
|
+
"title": "The municipality should build more affordable housing.",
|
|
105
|
+
"index": 1
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"shootoutStatements": [...],
|
|
109
|
+
"votematch": {
|
|
110
|
+
"id": 206918,
|
|
111
|
+
"name": "Municipality Name",
|
|
112
|
+
"context": "2026GR",
|
|
113
|
+
"remote_id": "GM0014",
|
|
114
|
+
"langcode": "nl"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## CLI Options
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
nl-voting-data-scraper scrape ELECTION [OPTIONS]
|
|
123
|
+
|
|
124
|
+
Options:
|
|
125
|
+
-m, --municipality TEXT Specific GM codes (repeatable)
|
|
126
|
+
-l, --language TEXT Languages to scrape (default: nl)
|
|
127
|
+
-o, --output TEXT Output directory (default: ./output)
|
|
128
|
+
--combined Also write combined.json
|
|
129
|
+
--rate-limit FLOAT Requests per second (default: 2.0)
|
|
130
|
+
--no-cache Disable caching
|
|
131
|
+
--resume Resume interrupted scrape
|
|
132
|
+
--browser-only Only use browser scraping
|
|
133
|
+
--api-only Only use API scraping
|
|
134
|
+
-v, --verbose Verbose output
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
git clone https://github.com/rhnfzl/nl-voting-data-scraper.git
|
|
141
|
+
cd nl-voting-data-scraper
|
|
142
|
+
pip install -e ".[dev,browser]"
|
|
143
|
+
playwright install chromium
|
|
144
|
+
pytest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Example: Scrape all municipalities for the 2026 municipal elections."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from nl_voting_data_scraper import StemwijzerScraper
|
|
7
|
+
from nl_voting_data_scraper.output import write_all
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
async with StemwijzerScraper("gr2026", rate_limit=2.0) as scraper:
|
|
12
|
+
# Fetch index first to see how many municipalities
|
|
13
|
+
index = await scraper.fetch_index()
|
|
14
|
+
print(f"Found {len(index)} entries")
|
|
15
|
+
|
|
16
|
+
# Scrape all
|
|
17
|
+
results = await scraper.scrape()
|
|
18
|
+
print(f"Scraped {len(results)} entries")
|
|
19
|
+
|
|
20
|
+
# Write to output directory
|
|
21
|
+
output_dir = Path("output") / "gr2026"
|
|
22
|
+
paths = write_all(results, output_dir, write_combined=True)
|
|
23
|
+
print(f"\nWritten to {output_dir}/")
|
|
24
|
+
for name, path in paths.items():
|
|
25
|
+
print(f" {name}: {path}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Example: Scrape national (Tweede Kamer) election data."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from nl_voting_data_scraper import StemwijzerScraper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
async with StemwijzerScraper("tk2025") as scraper:
|
|
11
|
+
results = await scraper.scrape()
|
|
12
|
+
|
|
13
|
+
for data in results:
|
|
14
|
+
print(f"Election: {data.votematch.name}")
|
|
15
|
+
print(f"Context: {data.votematch.context}")
|
|
16
|
+
print(f"Parties: {len(data.parties)}")
|
|
17
|
+
print(f"Statements: {len(data.statements)}")
|
|
18
|
+
|
|
19
|
+
# Save to file
|
|
20
|
+
filename = f"{data.votematch.context}.json"
|
|
21
|
+
with open(filename, "w") as f:
|
|
22
|
+
json.dump(data.model_dump(by_alias=True), f, ensure_ascii=False, indent=2)
|
|
23
|
+
print(f"Saved to {filename}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Example: Scrape a single municipality."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from nl_voting_data_scraper import StemwijzerScraper
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
async with StemwijzerScraper("gr2026") as scraper:
|
|
10
|
+
# Scrape Groningen
|
|
11
|
+
data = await scraper.scrape_one("GM0014")
|
|
12
|
+
print(f"Municipality: {data.votematch.name}")
|
|
13
|
+
print(f"Parties: {len(data.parties)}")
|
|
14
|
+
print(f"Statements: {len(data.statements)}")
|
|
15
|
+
print(f"Shootout statements: {len(data.shootoutStatements)}")
|
|
16
|
+
|
|
17
|
+
# Print first party and first statement
|
|
18
|
+
party = data.parties[0]
|
|
19
|
+
print(f"\nFirst party: {party.name}")
|
|
20
|
+
print(f" Positions: {len(party.statements)}")
|
|
21
|
+
|
|
22
|
+
stmt = data.statements[0]
|
|
23
|
+
print(f"\nFirst statement: {stmt.title}")
|
|
24
|
+
print(f" Theme: {stmt.theme}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nl-voting-data-scraper"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Scrape Dutch voting advice (StemWijzer) data for any election"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Rehan Fazal" }]
|
|
9
|
+
keywords = ["stemwijzer", "dutch", "elections", "scraper", "votematch"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"httpx>=0.28",
|
|
19
|
+
"pydantic>=2.12",
|
|
20
|
+
"click>=8.3",
|
|
21
|
+
"rich>=14.0",
|
|
22
|
+
"pycryptodome>=3.23",
|
|
23
|
+
"tenacity>=9.1",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
browser = ["playwright>=1.58"]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=9.0",
|
|
30
|
+
"pytest-asyncio>=1.3",
|
|
31
|
+
"pytest-httpx>=0.36",
|
|
32
|
+
"ruff>=0.15",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
nl-voting-data-scraper = "nl_voting_data_scraper.cli:cli"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/nl_voting_data_scraper"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
target-version = "py311"
|
|
47
|
+
line-length = 100
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
asyncio_mode = "auto"
|
|
51
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""nl-voting-data-scraper: Scrape Dutch voting advice (StemWijzer) data for any election."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from nl_voting_data_scraper.models import (
|
|
6
|
+
ElectionData,
|
|
7
|
+
ElectionIndexEntry,
|
|
8
|
+
Party,
|
|
9
|
+
PartyPosition,
|
|
10
|
+
Statement,
|
|
11
|
+
VotematchMeta,
|
|
12
|
+
)
|
|
13
|
+
from nl_voting_data_scraper.scraper import StemwijzerScraper
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"StemwijzerScraper",
|
|
17
|
+
"ElectionData",
|
|
18
|
+
"ElectionIndexEntry",
|
|
19
|
+
"Party",
|
|
20
|
+
"PartyPosition",
|
|
21
|
+
"Statement",
|
|
22
|
+
"VotematchMeta",
|
|
23
|
+
]
|