weatherlink-bridge 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.
Files changed (26) hide show
  1. weatherlink_bridge-0.1.0/PKG-INFO +243 -0
  2. weatherlink_bridge-0.1.0/README.md +227 -0
  3. weatherlink_bridge-0.1.0/pyproject.toml +132 -0
  4. weatherlink_bridge-0.1.0/src/weatherlink_bridge/__init__.py +5 -0
  5. weatherlink_bridge-0.1.0/src/weatherlink_bridge/__main__.py +5 -0
  6. weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/__init__.py +1 -0
  7. weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/base.py +4 -0
  8. weatherlink_bridge-0.1.0/src/weatherlink_bridge/collectors/weatherlink.py +215 -0
  9. weatherlink_bridge-0.1.0/src/weatherlink_bridge/exceptions.py +39 -0
  10. weatherlink_bridge-0.1.0/src/weatherlink_bridge/logger.py +60 -0
  11. weatherlink_bridge-0.1.0/src/weatherlink_bridge/main.py +385 -0
  12. weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/__init__.py +1 -0
  13. weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/mapper.py +101 -0
  14. weatherlink_bridge-0.1.0/src/weatherlink_bridge/mapping/transforms.py +70 -0
  15. weatherlink_bridge-0.1.0/src/weatherlink_bridge/metrics.py +150 -0
  16. weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/__init__.py +27 -0
  17. weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/observation.py +56 -0
  18. weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/sensor_map.py +105 -0
  19. weatherlink_bridge-0.1.0/src/weatherlink_bridge/models/weatherlink.py +90 -0
  20. weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/__init__.py +8 -0
  21. weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/base.py +58 -0
  22. weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/factory.py +123 -0
  23. weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/windy.py +181 -0
  24. weatherlink_bridge-0.1.0/src/weatherlink_bridge/publishers/wunderground.py +125 -0
  25. weatherlink_bridge-0.1.0/src/weatherlink_bridge/py.typed +0 -0
  26. weatherlink_bridge-0.1.0/src/weatherlink_bridge/settings.py +90 -0
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.3
2
+ Name: weatherlink-bridge
3
+ Version: 0.1.0
4
+ Summary: WeatherLink PWS bridge service forwarding to Weather Underground and Windy
5
+ Author: John Roden
6
+ Author-email: John Roden <rodenj@gmail.com>
7
+ License: MIT
8
+ Requires-Dist: pydantic>=2.7
9
+ Requires-Dist: pydantic-settings>=2.3
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: prometheus-client>=0.20
12
+ Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: structlog>=24.1
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+
17
+ # WeatherLink Bridge
18
+
19
+ A Python service that polls the **Davis WeatherLink v2 API** and forwards personal weather station observations to **Weather Underground** and **Windy**. It is a clean rewrite of the original Node.js app, adding robust configuration management, Prometheus metrics, and a production-ready container image.
20
+
21
+ ---
22
+
23
+ ## Architecture
24
+
25
+ ```
26
+ WeatherLink API
27
+ |
28
+ v
29
+ WeatherLinkCollector (httpx, X-Api-Secret header)
30
+ |
31
+ v
32
+ WeatherObservation (canonical imperial fields, Pydantic v2)
33
+ |
34
+ +----> FieldMapper(wunderground.yaml) --> WundergroundPublisher --> WU HTTPS API
35
+ |
36
+ +----> FieldMapper(windy.yaml) --> WindyPublisher --> Windy v2 API
37
+ (unit transforms: °F→°C, mph→m/s, inHg→Pa, in→mm)
38
+
39
+ Prometheus /metrics on METRICS_PORT (default 8080)
40
+ - wl_fetch_total, publish_total, collection_run_total
41
+ - observation_value (per-field gauge)
42
+ - last_successful_cycle_timestamp <-- liveness signal
43
+ ```
44
+
45
+ Publishers are registered via `PublisherFactory` — adding a new destination (CWOP, PWSWeather, …) requires only a new YAML sensor map and a publisher class.
46
+
47
+ ---
48
+
49
+ ## Quickstart
50
+
51
+ **Prerequisites:** Python 3.12+, [uv](https://docs.astral.sh/uv/).
52
+
53
+ ```bash
54
+ # Clone and install
55
+ git clone https://github.com/rodenj1/weatherlink-bridge.git
56
+ cd weatherlink-bridge
57
+
58
+ # Copy the example env file and fill in your credentials
59
+ cp .env.example .env
60
+ $EDITOR .env
61
+
62
+ # Run directly (reads .env automatically)
63
+ uv run weatherlink-bridge
64
+
65
+ # Or install into a venv and run the console script
66
+ uv sync
67
+ weatherlink-bridge --version
68
+ weatherlink-bridge
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Configuration Reference
74
+
75
+ All configuration is via environment variables (or a `.env` file in the working directory). Nested settings use the `__` delimiter.
76
+
77
+ | Variable | Required | Default | Description |
78
+ |---|---|---|---|
79
+ | `WEATHERLINK__API_KEY` | Yes | — | WeatherLink v2 API key |
80
+ | `WEATHERLINK__API_SECRET` | Yes | — | WeatherLink v2 API secret |
81
+ | `WEATHERLINK__STATION_ID` | Yes | — | WeatherLink station ID |
82
+ | `WUNDERGROUND__ENABLED` | No | `false` | Enable Weather Underground publishing |
83
+ | `WUNDERGROUND__STATION_ID` | No | `""` | WU PWS station ID (e.g. `KCASANDI123`) |
84
+ | `WUNDERGROUND__PASSWORD` | No | `""` | WU station key / password |
85
+ | `WINDY__ENABLED` | No | `false` | Enable Windy publishing |
86
+ | `WINDY__STATION_ID` | No | `""` | Windy station ID (numeric) |
87
+ | `WINDY__PASSWORD` | No | `""` | Windy station password (see note below) |
88
+ | `UPDATE_INTERVAL_MINS` | No | `5` | Poll interval in minutes (minimum 5) |
89
+ | `METRICS_PORT` | No | `8080` | TCP port for Prometheus `/metrics` |
90
+ | `LOG_LEVEL` | No | `INFO` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
91
+
92
+ ### Windy credentials note
93
+
94
+ Windy uses a **per-station password** for uploads, not the management API key.
95
+ Find it at [stations.windy.com](https://stations.windy.com/) under
96
+ **My Stations → Station settings → Key/Password**. Set this value in
97
+ `WINDY__PASSWORD`.
98
+
99
+ ### Weather Underground credentials note
100
+
101
+ WU needs the **station ID** (`WUNDERGROUND__STATION_ID`, e.g. `KCASANDI123`)
102
+ and the **station key / password** (`WUNDERGROUND__PASSWORD`). Both are found
103
+ in the WU device management dashboard.
104
+
105
+ ---
106
+
107
+ ## Running with Docker
108
+
109
+ ### Single container
110
+
111
+ ```bash
112
+ # Pull the latest image
113
+ docker pull ghcr.io/rodenj1/weatherlink-bridge:latest
114
+
115
+ # Run with a .env file
116
+ docker run --rm --env-file .env -p 8080:8080 ghcr.io/rodenj1/weatherlink-bridge:latest
117
+ ```
118
+
119
+ ### docker-compose
120
+
121
+ ```bash
122
+ cp .env.example .env
123
+ $EDITOR .env # fill in real credentials
124
+ docker compose up -d
125
+ # metrics available at http://localhost:8080/metrics
126
+ ```
127
+
128
+ The image is published to `ghcr.io/rodenj1/weatherlink-bridge` for both
129
+ `linux/amd64` and `linux/arm64` (suitable for Raspberry Pi / ARM homelab).
130
+
131
+ ---
132
+
133
+ ## Running on Kubernetes
134
+
135
+ Manifests are under `deploy/k8s/`.
136
+
137
+ ```bash
138
+ # 1. Create the namespace
139
+ kubectl create namespace weather
140
+
141
+ # 2. Copy the secret example, fill in real values, apply
142
+ cp deploy/k8s/secret.example.yaml deploy/k8s/secret.yaml
143
+ $EDITOR deploy/k8s/secret.yaml # add real credentials
144
+ kubectl apply -f deploy/k8s/secret.yaml
145
+
146
+ # 3. Apply the rest
147
+ kubectl apply -f deploy/k8s/deployment.yaml
148
+ kubectl apply -f deploy/k8s/service.yaml
149
+
150
+ # Verify
151
+ kubectl -n weather get pods
152
+ kubectl -n weather logs -f deployment/weatherlink-bridge
153
+ ```
154
+
155
+ > **Do not commit `secret.yaml` with real values.** Use
156
+ > [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) or a
157
+ > secrets manager to encrypt before committing.
158
+
159
+ The Deployment is intentionally `replicas: 1` — two replicas would
160
+ **double-upload** every observation to WU and Windy.
161
+
162
+ ### Liveness and readiness probes
163
+
164
+ Both probes hit `GET /metrics` on port 8080. The liveness probe also lets you
165
+ monitor `last_successful_cycle_timestamp` in Prometheus — if that gauge is
166
+ older than `2 * update_interval_seconds`, the service may be stalled.
167
+
168
+ ---
169
+
170
+ ## Metrics
171
+
172
+ The service exposes Prometheus metrics at `http://<host>:8080/metrics`.
173
+
174
+ | Metric | Type | Description |
175
+ |---|---|---|
176
+ | `wl_fetch_total` | Counter | WeatherLink fetch attempts (`status=success\|error`) |
177
+ | `wl_fetch_duration_seconds` | Histogram | WeatherLink API latency |
178
+ | `publish_total` | Counter | Publisher attempts (`publisher=wu\|windy`, `status=success\|failure\|skipped`) |
179
+ | `publish_duration_seconds` | Histogram | Publisher call latency |
180
+ | `collection_run_total` | Counter | Full cycle outcomes (`status=success\|partial\|error`) |
181
+ | `collection_run_duration_seconds` | Histogram | Full cycle duration |
182
+ | `observation_value` | Gauge | Latest numeric field value per field + station |
183
+ | `last_successful_cycle_timestamp` | Gauge | Unix timestamp of last successful fetch (liveness signal) |
184
+ | `update_interval_seconds` | Gauge | Configured poll interval |
185
+ | `weatherlink_bridge_info` | Info | App version |
186
+
187
+ ### Liveness vs freshness
188
+
189
+ - **Liveness** (`last_successful_cycle_timestamp`): advances after every
190
+ successful WeatherLink fetch, regardless of publisher outcomes. Alert if
191
+ `time() - last_successful_cycle_timestamp > 2 * update_interval_seconds`.
192
+ - **Publisher health**: monitor `publish_total{status="failure"}` separately.
193
+ Publisher failures do not gate liveness — a rate-limited or temporarily
194
+ unavailable WU/Windy should not restart the pod.
195
+
196
+ ---
197
+
198
+ ## Migration from the Node.js version
199
+
200
+ | Old environment variable | New environment variable | Notes |
201
+ |---|---|---|
202
+ | `WEATHERLINK_API_KEY` | `WEATHERLINK__API_KEY` | Delimiter changed (`_` → `__`) |
203
+ | `WEATHERLINK_API_SECRECT` | `WEATHERLINK__API_SECRET` | **Typo fixed** (`SECRECT` → `SECRET`) |
204
+ | `WEATHERLINK_STATION_ID` | `WEATHERLINK__STATION_ID` | Delimiter changed |
205
+ | `WUNDERGROUND_ID` | `WUNDERGROUND__STATION_ID` | Renamed for clarity |
206
+ | `WUNDERGROUND_KEY` | `WUNDERGROUND__PASSWORD` | Renamed; this is the station password |
207
+ | `UPDATE_INTERVAL_MINS` | `UPDATE_INTERVAL_MINS` | Unchanged |
208
+ | `PORT` | `METRICS_PORT` | Renamed; default is now 8080 |
209
+
210
+ **Sensor map:** `sensor_map.json` is replaced by YAML files in
211
+ `config/sensor_maps/` (`wunderground.yaml`, `windy.yaml`). The YAML format
212
+ supports field transforms (unit conversions) and is checked into version
213
+ control.
214
+
215
+ ---
216
+
217
+ ## Development
218
+
219
+ ```bash
220
+ # Install all deps including dev tools
221
+ uv sync
222
+
223
+ # Run tests with coverage
224
+ uv run pytest
225
+
226
+ # Lint + format check
227
+ uv run ruff check src tests
228
+ uv run ruff format --check src tests
229
+
230
+ # Type-check
231
+ uv run pyright src
232
+ uv run mypy src
233
+
234
+ # Pre-commit hooks (runs on every commit)
235
+ uv run pre-commit install
236
+ uv run pre-commit run --all-files
237
+ ```
238
+
239
+ ---
240
+
241
+ ## License
242
+
243
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,227 @@
1
+ # WeatherLink Bridge
2
+
3
+ A Python service that polls the **Davis WeatherLink v2 API** and forwards personal weather station observations to **Weather Underground** and **Windy**. It is a clean rewrite of the original Node.js app, adding robust configuration management, Prometheus metrics, and a production-ready container image.
4
+
5
+ ---
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ WeatherLink API
11
+ |
12
+ v
13
+ WeatherLinkCollector (httpx, X-Api-Secret header)
14
+ |
15
+ v
16
+ WeatherObservation (canonical imperial fields, Pydantic v2)
17
+ |
18
+ +----> FieldMapper(wunderground.yaml) --> WundergroundPublisher --> WU HTTPS API
19
+ |
20
+ +----> FieldMapper(windy.yaml) --> WindyPublisher --> Windy v2 API
21
+ (unit transforms: °F→°C, mph→m/s, inHg→Pa, in→mm)
22
+
23
+ Prometheus /metrics on METRICS_PORT (default 8080)
24
+ - wl_fetch_total, publish_total, collection_run_total
25
+ - observation_value (per-field gauge)
26
+ - last_successful_cycle_timestamp <-- liveness signal
27
+ ```
28
+
29
+ Publishers are registered via `PublisherFactory` — adding a new destination (CWOP, PWSWeather, …) requires only a new YAML sensor map and a publisher class.
30
+
31
+ ---
32
+
33
+ ## Quickstart
34
+
35
+ **Prerequisites:** Python 3.12+, [uv](https://docs.astral.sh/uv/).
36
+
37
+ ```bash
38
+ # Clone and install
39
+ git clone https://github.com/rodenj1/weatherlink-bridge.git
40
+ cd weatherlink-bridge
41
+
42
+ # Copy the example env file and fill in your credentials
43
+ cp .env.example .env
44
+ $EDITOR .env
45
+
46
+ # Run directly (reads .env automatically)
47
+ uv run weatherlink-bridge
48
+
49
+ # Or install into a venv and run the console script
50
+ uv sync
51
+ weatherlink-bridge --version
52
+ weatherlink-bridge
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Configuration Reference
58
+
59
+ All configuration is via environment variables (or a `.env` file in the working directory). Nested settings use the `__` delimiter.
60
+
61
+ | Variable | Required | Default | Description |
62
+ |---|---|---|---|
63
+ | `WEATHERLINK__API_KEY` | Yes | — | WeatherLink v2 API key |
64
+ | `WEATHERLINK__API_SECRET` | Yes | — | WeatherLink v2 API secret |
65
+ | `WEATHERLINK__STATION_ID` | Yes | — | WeatherLink station ID |
66
+ | `WUNDERGROUND__ENABLED` | No | `false` | Enable Weather Underground publishing |
67
+ | `WUNDERGROUND__STATION_ID` | No | `""` | WU PWS station ID (e.g. `KCASANDI123`) |
68
+ | `WUNDERGROUND__PASSWORD` | No | `""` | WU station key / password |
69
+ | `WINDY__ENABLED` | No | `false` | Enable Windy publishing |
70
+ | `WINDY__STATION_ID` | No | `""` | Windy station ID (numeric) |
71
+ | `WINDY__PASSWORD` | No | `""` | Windy station password (see note below) |
72
+ | `UPDATE_INTERVAL_MINS` | No | `5` | Poll interval in minutes (minimum 5) |
73
+ | `METRICS_PORT` | No | `8080` | TCP port for Prometheus `/metrics` |
74
+ | `LOG_LEVEL` | No | `INFO` | Log verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
75
+
76
+ ### Windy credentials note
77
+
78
+ Windy uses a **per-station password** for uploads, not the management API key.
79
+ Find it at [stations.windy.com](https://stations.windy.com/) under
80
+ **My Stations → Station settings → Key/Password**. Set this value in
81
+ `WINDY__PASSWORD`.
82
+
83
+ ### Weather Underground credentials note
84
+
85
+ WU needs the **station ID** (`WUNDERGROUND__STATION_ID`, e.g. `KCASANDI123`)
86
+ and the **station key / password** (`WUNDERGROUND__PASSWORD`). Both are found
87
+ in the WU device management dashboard.
88
+
89
+ ---
90
+
91
+ ## Running with Docker
92
+
93
+ ### Single container
94
+
95
+ ```bash
96
+ # Pull the latest image
97
+ docker pull ghcr.io/rodenj1/weatherlink-bridge:latest
98
+
99
+ # Run with a .env file
100
+ docker run --rm --env-file .env -p 8080:8080 ghcr.io/rodenj1/weatherlink-bridge:latest
101
+ ```
102
+
103
+ ### docker-compose
104
+
105
+ ```bash
106
+ cp .env.example .env
107
+ $EDITOR .env # fill in real credentials
108
+ docker compose up -d
109
+ # metrics available at http://localhost:8080/metrics
110
+ ```
111
+
112
+ The image is published to `ghcr.io/rodenj1/weatherlink-bridge` for both
113
+ `linux/amd64` and `linux/arm64` (suitable for Raspberry Pi / ARM homelab).
114
+
115
+ ---
116
+
117
+ ## Running on Kubernetes
118
+
119
+ Manifests are under `deploy/k8s/`.
120
+
121
+ ```bash
122
+ # 1. Create the namespace
123
+ kubectl create namespace weather
124
+
125
+ # 2. Copy the secret example, fill in real values, apply
126
+ cp deploy/k8s/secret.example.yaml deploy/k8s/secret.yaml
127
+ $EDITOR deploy/k8s/secret.yaml # add real credentials
128
+ kubectl apply -f deploy/k8s/secret.yaml
129
+
130
+ # 3. Apply the rest
131
+ kubectl apply -f deploy/k8s/deployment.yaml
132
+ kubectl apply -f deploy/k8s/service.yaml
133
+
134
+ # Verify
135
+ kubectl -n weather get pods
136
+ kubectl -n weather logs -f deployment/weatherlink-bridge
137
+ ```
138
+
139
+ > **Do not commit `secret.yaml` with real values.** Use
140
+ > [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) or a
141
+ > secrets manager to encrypt before committing.
142
+
143
+ The Deployment is intentionally `replicas: 1` — two replicas would
144
+ **double-upload** every observation to WU and Windy.
145
+
146
+ ### Liveness and readiness probes
147
+
148
+ Both probes hit `GET /metrics` on port 8080. The liveness probe also lets you
149
+ monitor `last_successful_cycle_timestamp` in Prometheus — if that gauge is
150
+ older than `2 * update_interval_seconds`, the service may be stalled.
151
+
152
+ ---
153
+
154
+ ## Metrics
155
+
156
+ The service exposes Prometheus metrics at `http://<host>:8080/metrics`.
157
+
158
+ | Metric | Type | Description |
159
+ |---|---|---|
160
+ | `wl_fetch_total` | Counter | WeatherLink fetch attempts (`status=success\|error`) |
161
+ | `wl_fetch_duration_seconds` | Histogram | WeatherLink API latency |
162
+ | `publish_total` | Counter | Publisher attempts (`publisher=wu\|windy`, `status=success\|failure\|skipped`) |
163
+ | `publish_duration_seconds` | Histogram | Publisher call latency |
164
+ | `collection_run_total` | Counter | Full cycle outcomes (`status=success\|partial\|error`) |
165
+ | `collection_run_duration_seconds` | Histogram | Full cycle duration |
166
+ | `observation_value` | Gauge | Latest numeric field value per field + station |
167
+ | `last_successful_cycle_timestamp` | Gauge | Unix timestamp of last successful fetch (liveness signal) |
168
+ | `update_interval_seconds` | Gauge | Configured poll interval |
169
+ | `weatherlink_bridge_info` | Info | App version |
170
+
171
+ ### Liveness vs freshness
172
+
173
+ - **Liveness** (`last_successful_cycle_timestamp`): advances after every
174
+ successful WeatherLink fetch, regardless of publisher outcomes. Alert if
175
+ `time() - last_successful_cycle_timestamp > 2 * update_interval_seconds`.
176
+ - **Publisher health**: monitor `publish_total{status="failure"}` separately.
177
+ Publisher failures do not gate liveness — a rate-limited or temporarily
178
+ unavailable WU/Windy should not restart the pod.
179
+
180
+ ---
181
+
182
+ ## Migration from the Node.js version
183
+
184
+ | Old environment variable | New environment variable | Notes |
185
+ |---|---|---|
186
+ | `WEATHERLINK_API_KEY` | `WEATHERLINK__API_KEY` | Delimiter changed (`_` → `__`) |
187
+ | `WEATHERLINK_API_SECRECT` | `WEATHERLINK__API_SECRET` | **Typo fixed** (`SECRECT` → `SECRET`) |
188
+ | `WEATHERLINK_STATION_ID` | `WEATHERLINK__STATION_ID` | Delimiter changed |
189
+ | `WUNDERGROUND_ID` | `WUNDERGROUND__STATION_ID` | Renamed for clarity |
190
+ | `WUNDERGROUND_KEY` | `WUNDERGROUND__PASSWORD` | Renamed; this is the station password |
191
+ | `UPDATE_INTERVAL_MINS` | `UPDATE_INTERVAL_MINS` | Unchanged |
192
+ | `PORT` | `METRICS_PORT` | Renamed; default is now 8080 |
193
+
194
+ **Sensor map:** `sensor_map.json` is replaced by YAML files in
195
+ `config/sensor_maps/` (`wunderground.yaml`, `windy.yaml`). The YAML format
196
+ supports field transforms (unit conversions) and is checked into version
197
+ control.
198
+
199
+ ---
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ # Install all deps including dev tools
205
+ uv sync
206
+
207
+ # Run tests with coverage
208
+ uv run pytest
209
+
210
+ # Lint + format check
211
+ uv run ruff check src tests
212
+ uv run ruff format --check src tests
213
+
214
+ # Type-check
215
+ uv run pyright src
216
+ uv run mypy src
217
+
218
+ # Pre-commit hooks (runs on every commit)
219
+ uv run pre-commit install
220
+ uv run pre-commit run --all-files
221
+ ```
222
+
223
+ ---
224
+
225
+ ## License
226
+
227
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,132 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.7,<0.12.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "weatherlink-bridge"
7
+ version = "0.1.0"
8
+ description = "WeatherLink PWS bridge service forwarding to Weather Underground and Windy"
9
+ readme = "README.md"
10
+ authors = [{ name = "John Roden", email = "rodenj@gmail.com" }]
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.12"
13
+ dependencies = [
14
+ "pydantic>=2.7",
15
+ "pydantic-settings>=2.3",
16
+ "httpx>=0.27",
17
+ "prometheus-client>=0.20",
18
+ "pyyaml>=6.0",
19
+ "structlog>=24.1",
20
+ ]
21
+
22
+ [project.scripts]
23
+ weatherlink-bridge = "weatherlink_bridge.main:main"
24
+
25
+ [dependency-groups]
26
+ dev = [
27
+ "pytest>=8",
28
+ "pytest-asyncio>=1",
29
+ "pytest-cov>=5",
30
+ "respx>=0.21",
31
+ "ruff>=0.6",
32
+ "mypy>=1.10",
33
+ "pyright>=1.1.400",
34
+ "pre-commit>=3",
35
+ "commitizen>=4",
36
+ "types-PyYAML",
37
+ ]
38
+
39
+ [tool.pytest.ini_options]
40
+ addopts = [
41
+ "--cov=src/weatherlink_bridge",
42
+ "--cov-report=term-missing",
43
+ "--cov-report=html:htmlcov",
44
+ "--cov-fail-under=95",
45
+ "--strict-markers",
46
+ "--strict-config",
47
+ ]
48
+ testpaths = ["tests"]
49
+ norecursedirs = ["manual"]
50
+ asyncio_mode = "auto"
51
+ markers = [
52
+ "integration: wired multi-component tests",
53
+ "live: hits the real WeatherLink API; skipped unless WLB_LIVE_TESTS=1",
54
+ ]
55
+
56
+ [tool.coverage.run]
57
+ source = ["src"]
58
+ branch = true
59
+ omit = ["*/tests/*", "*/test_*", "conftest.py"]
60
+
61
+ [tool.coverage.report]
62
+ exclude_lines = [
63
+ "pragma: no cover",
64
+ "def __repr__",
65
+ "raise AssertionError",
66
+ "raise NotImplementedError",
67
+ "if __name__ == .__main__.:",
68
+ "if TYPE_CHECKING:",
69
+ ]
70
+
71
+ [tool.mypy]
72
+ python_version = "3.12"
73
+ strict = true
74
+ warn_return_any = true
75
+ warn_unused_configs = true
76
+ disallow_untyped_defs = true
77
+ disallow_incomplete_defs = true
78
+ check_untyped_defs = true
79
+ disallow_untyped_decorators = true
80
+ no_implicit_optional = true
81
+ warn_redundant_casts = true
82
+ warn_unused_ignores = true
83
+ warn_no_return = true
84
+ warn_unreachable = true
85
+ strict_equality = true
86
+
87
+ [[tool.mypy.overrides]]
88
+ module = "prometheus_client.*"
89
+ ignore_missing_imports = true
90
+
91
+ [tool.ruff]
92
+ line-length = 88
93
+ target-version = "py312"
94
+ extend-exclude = ["docs", "htmlcov", "build", "dist", ".venv"]
95
+
96
+ [tool.ruff.lint]
97
+ select = ["E", "W", "F", "I", "B", "UP", "N", "SIM", "RUF"]
98
+ ignore = ["E501"]
99
+
100
+ [tool.ruff.lint.per-file-ignores]
101
+ "tests/**/*.py" = ["B", "N", "SIM"]
102
+ "conftest.py" = ["B", "N"]
103
+
104
+ [tool.ruff.format]
105
+ quote-style = "double"
106
+ indent-style = "space"
107
+ line-ending = "auto"
108
+
109
+ [tool.pyright]
110
+ include = ["src"]
111
+ typeCheckingMode = "strict"
112
+ deprecateTypingAliases = true
113
+ reportDeprecated = "warning"
114
+ extraPaths = ["src/"]
115
+
116
+ [tool.commitizen]
117
+ name = "cz_customize"
118
+ version = "0.1.0"
119
+ version_provider = "pep621"
120
+ version_files = ["src/weatherlink_bridge/__init__.py:__version__"]
121
+ tag_format = "v$version"
122
+ update_changelog_on_bump = true
123
+ major_version_zero = true
124
+ allowed_prefixes = ["Merge", "Revert", "Pull request", "fixup!", "squash!"]
125
+ bump_message = "release: bump $current_version -> $new_version"
126
+
127
+ [tool.commitizen.customize]
128
+ schema_pattern = "^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release|security)(\\([a-z0-9_\\-.,/]+\\))?!?: .+"
129
+ bump_pattern = "^(feat|fix|refactor|perf|security|build)"
130
+ bump_map = { "^feat" = "MINOR", "^fix" = "PATCH", "^refactor" = "PATCH", "^perf" = "PATCH", "^security" = "PATCH", "^build" = "PATCH" }
131
+ change_type_order = ["security", "feat", "fix", "refactor", "perf", "build", "ci", "docs", "style", "test", "chore"]
132
+ info = "WeatherLink Bridge follows Conventional Commits 1.0.0"
@@ -0,0 +1,5 @@
1
+ """WeatherLink Bridge: forwards PWS data to Weather Underground and Windy."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,5 @@
1
+ """Allow running the package as a module: python -m weatherlink_bridge."""
2
+
3
+ from weatherlink_bridge.main import main # pragma: no cover
4
+
5
+ main() # pragma: no cover
@@ -0,0 +1 @@
1
+ """Weather data collectors."""
@@ -0,0 +1,4 @@
1
+ """Abstract base class for weather data collectors.
2
+
3
+ TODO(Phase 2): Implement BaseCollector ABC.
4
+ """