nowcastingcli 0.6.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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: nowcastingcli
3
+ Version: 0.6.1
4
+ Summary: Terminal weather nowcasting dashboard
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: rich>=13.0
7
+ Requires-Dist: python-json-logger
8
+ Provides-Extra: docs
9
+ Requires-Dist: mkdocs<2.0,>=1.5; extra == "docs"
10
+ Requires-Dist: mkdocs-material; extra == "docs"
11
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-cov; extra == "dev"
15
+ Requires-Dist: setuptools; extra == "dev"
16
+ Requires-Dist: wheel; extra == "dev"
@@ -0,0 +1,625 @@
1
+ # NowcastingCLI
2
+
3
+ [![Smoke Tests](https://github.com/juaquiro/CCPD-nowcastingcli/actions/workflows/smoke-tests.yml/badge.svg?branch=develop)](https://github.com/juaquiro/CCPD-nowcastingcli/actions/workflows/smoke-tests.yml)
4
+
5
+ A terminal-based weather nowcasting CLI built with Python and [Rich](https://github.com/Textualize/rich).
6
+
7
+ ---
8
+
9
+ ## Branching Model
10
+
11
+ This repo uses a two-branch model:
12
+
13
+ | Branch | Role | Notes |
14
+ |---|---|---|
15
+ | `develop` | Default branch — everyday feature integration | All feature work and PRs target this branch |
16
+ | `main` | Stable / production-ready branch | Updated only via PR from `develop` at release time; protected — no direct pushes, no force-pushes, no deletion |
17
+
18
+ Everyday workflow: branch off `develop`, open a PR back into `develop`. When
19
+ `develop` is stable and ready to ship, open a PR from `develop` into `main`
20
+ to cut a release.
21
+
22
+ ---
23
+
24
+ ## Project Structure
25
+
26
+ ```
27
+ NOWCASTINGCLI/
28
+ ├── nowcastingcli/ # installable package
29
+ │ ├── __init__.py # package marker
30
+ │ ├── main.py # CLI entry point — cli(), run(), get_float(), edit_observation()
31
+ │ ├── models.py # Observation dataclass
32
+ │ ├── physics.py # barometric QNH normalisation
33
+ │ ├── heuristics.py # worsening / stable / improving logic
34
+ │ ├── display.py # Rich dashboard, sparkline, trend arrows
35
+ │ └── logging_config.py # dictConfig setup — rotating JSON file + stderr handlers
36
+ ├── tests/ # pytest test suite
37
+ │ ├── __init__.py
38
+ │ ├── test_display.py # tests for sparkline, trend_arrow, render_dashboard
39
+ │ ├── test_heuristics.py # tests for assess_conditions()
40
+ │ ├── test_main.py # tests for run() and get_float()
41
+ │ ├── test_models.py # tests for Observation dataclass
42
+ │ └── test_physics.py # tests for normalize_pressure()
43
+ ├── scripts/ # standalone helper scripts
44
+ │ ├── Init_observation.py # quick manual smoke-test for Observation
45
+ │ ├── test_observations.csv # sample CSV for --input and logging smoke-tests
46
+ │ ├── test_logging.sh # logging smoke-test (Bash / Git Bash / macOS)
47
+ │ ├── test_logging.bat # logging smoke-test (Windows CMD)
48
+ │ ├── sync_env.bat # sync conda environment between machines
49
+ │ └── update_lock.bat # regenerate environment.lock.yml
50
+ ├── docs/ # project documentation
51
+ │ ├── index.md # landing page
52
+ │ ├── architecture.md # module map and data-flow diagram
53
+ │ ├── usage.md # input loop, valid ranges, dashboard reference
54
+ │ └── api/ # per-module API reference (mkdocstrings stubs)
55
+ │ ├── physics.md
56
+ │ ├── models.md
57
+ │ ├── heuristics.md
58
+ │ └── display.md
59
+ ├── logs/ # auto-created at runtime — rotating JSON log files
60
+ ├── .vscode/
61
+ │ ├── launch.json # pytest debug configuration
62
+ │ └── settings.json
63
+ ├── environment.lock.yml # pinned conda environment snapshot
64
+ ├── pyproject.toml # build, dependencies, and pytest config
65
+ ├── TODO.md # pending improvements
66
+ ├── SESSION_SUMMARY.md # per-session change log
67
+ ├── README.md
68
+ ├── course_notes/ # course notes, split per module
69
+ │ ├── Course_Notes_Index.md
70
+ │ ├── Module1_Course_Notes.md
71
+ │ ├── Module2_Course_Notes.md
72
+ │ ├── Module3_Course_Notes.md
73
+ │ ├── Module4_Course_Notes.md
74
+ │ └── Module5_Course_Notes.md
75
+ └── README_CONDA_ENV_SYNC.md # guide for syncing conda envs across machines
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Documentation
81
+
82
+ Human-readable docs live in `docs/`:
83
+
84
+ | File | Contents |
85
+ |------|----------|
86
+ | [`docs/index.md`](docs/index.md) | Project overview and quick-start |
87
+ | [`docs/architecture.md`](docs/architecture.md) | Module map and data-flow diagram |
88
+ | [`docs/usage.md`](docs/usage.md) | Full input-loop reference, valid ranges, dashboard column guide |
89
+ | [`docs/api/physics.md`](docs/api/physics.md) | `normalize_pressure()` API reference |
90
+ | [`docs/api/models.md`](docs/api/models.md) | `Observation` dataclass field reference |
91
+ | [`docs/api/heuristics.md`](docs/api/heuristics.md) | `assess_conditions()` API reference |
92
+ | [`docs/api/display.md`](docs/api/display.md) | Dashboard rendering functions API reference |
93
+
94
+ Course notes live in [`course_notes/`](course_notes/), split per module and
95
+ indexed in [`course_notes/Course_Notes_Index.md`](course_notes/Course_Notes_Index.md).
96
+
97
+ To build and serve the docs site locally:
98
+
99
+ ```bash
100
+ pip install -e ".[docs]"
101
+ mkdocs serve
102
+ ```
103
+
104
+ To install every optional extra (`docs` + `dev`) alongside the runtime
105
+ dependencies in one shot:
106
+
107
+ ```bash
108
+ pip install -e ".[docs,dev]"
109
+ ```
110
+
111
+ ---
112
+
113
+ ## `pyproject.toml`
114
+
115
+ ```toml
116
+ [build-system]
117
+ requires = ["setuptools"]
118
+ build-backend = "setuptools.build_meta"
119
+
120
+ [project]
121
+ name = "nowcastingcli"
122
+ version = "0.1.0"
123
+ description = "Terminal weather nowcasting dashboard"
124
+ requires-python = ">=3.11"
125
+ dependencies = ["rich>=13.0", "python-json-logger"]
126
+
127
+ [project.scripts]
128
+ nowcastingcli = "nowcastingcli.main:cli"
129
+
130
+ [tool.setuptools.packages.find]
131
+ where = ["."]
132
+
133
+ [tool.pytest.ini_options]
134
+ testpaths = ["tests"]
135
+ addopts = "--cov=nowcastingcli --cov-fail-under=80"
136
+
137
+ [project.optional-dependencies]
138
+ docs = [
139
+ "mkdocs",
140
+ "mkdocs-material",
141
+ "mkdocstrings[python]",
142
+ ]
143
+ dev = [
144
+ "pytest",
145
+ "pytest-cov",
146
+ "setuptools",
147
+ "wheel",
148
+ ]
149
+ ```
150
+
151
+ Key sections explained:
152
+
153
+ - **`[build-system]`** — tells pip to use `setuptools` to build the package.
154
+ - **`[project]`** — package metadata: name, version, Python version constraint, and runtime dependencies (`rich`, `python-json-logger`).
155
+ - **`[project.scripts]`** — registers the `nowcastingcli` shell command, pointing it at the `cli()` entry point in `main.py`. `cli()` parses `--input` from `sys.argv` and delegates to `run()`. Available anywhere in the active environment after `pip install -e .`.
156
+ - **`[project.optional-dependencies]`** — extra dependency groups. `docs` (MkDocs, the Material theme, `mkdocstrings`) installs with `pip install -e ".[docs]"`; `dev` (`pytest`, `pytest-cov`, `setuptools`, `wheel`) installs with `pip install -e ".[dev]"`. Install both together, on top of the required dependencies, with `pip install -e ".[docs,dev]"`.
157
+ - **`[tool.setuptools.packages.find]`** — tells setuptools to auto-discover the `nowcastingcli` package from the project root.
158
+ - **`[tool.pytest.ini_options]`** — pytest configuration baked into `pyproject.toml` so no separate `pytest.ini` is needed:
159
+ - `testpaths` tells pytest to look for tests only in `tests/`.
160
+ - `addopts` automatically adds coverage flags to every `pytest` run: `--cov=nowcastingcli` measures coverage of the source package, and `--cov-fail-under=80` fails the run if total coverage drops below 80 %.
161
+
162
+ ---
163
+
164
+ ## Building a Distributable Package
165
+
166
+ Full detail, verification steps, and rationale live in
167
+ [`course_notes/Module6_Course_Notes.md`](course_notes/Module6_Course_Notes.md).
168
+ Five ways to hand off a build, depending on what the target machine has:
169
+
170
+ | Path | Output | Target needs | Command |
171
+ |---|---|---|---|
172
+ | **1. PyPI / TestPyPI** | Published package, installable by name | Python + pip, network access | `python -m build` then `twine upload dist/*` (or `--repository testpypi`) |
173
+ | **2. conda packaging** | conda package | conda | Not used for this project yet (no compiled/Qt deps) — see notes for a `meta.yaml` sketch |
174
+ | **3. Local wheel / editable install** | `.whl` file or a git clone | Python + pip (no PyPI account, no network needed for the wheel option) | `pip install nowcastingcli-<version>-py3-none-any.whl`, or `git clone` + `pip install -e .` |
175
+ | **4. Standalone `.exe` (PyInstaller)** | Single self-contained executable | **Nothing** — no Python required at all | `pyinstaller nowcastingcli.spec` (rebuilds from the committed spec; see `launcher.py` and `nowcastingcli.spec`) |
176
+ | **5. Unix / Raspberry Pi (`pipx`)** | Isolated CLI install, no conda needed | Python + pip on a Unix target (e.g. Raspberry Pi OS) | `pipx install nowcastingcli` (or `pipx install --index-url https://test.pypi.org/simple/ --pip-args="--extra-index-url https://pypi.org/simple/" nowcastingcli` for TestPyPI) |
177
+
178
+ Paths 1–3 and 5 all assume a Python interpreter is already on the target
179
+ machine (a "framework-dependent" deployment); Path 4 bundles the
180
+ interpreter itself (a "self-contained" deployment) and is distributed
181
+ directly — zipped, attached to a release, handed over on a USB stick —
182
+ never uploaded to PyPI. Path 5 is the Unix/Raspberry Pi counterpart to
183
+ conda on Windows: NowcastingCLI's wheel is pure Python (`py3-none-any`),
184
+ so the same wheel installs unmodified on ARM — no PyInstaller rebuild or
185
+ conda/miniforge overhead needed, and `pipx` avoids Debian's PEP 668
186
+ `externally-managed-environment` guard that blocks a bare `pip install`.
187
+
188
+ ---
189
+
190
+ ## Setup
191
+
192
+ ### 1. Create and activate the conda environment
193
+
194
+ ```bash
195
+ conda create -n nowcastingcli python=3.11
196
+ conda activate nowcastingcli
197
+ ```
198
+
199
+ ### 2. Install setuptools
200
+
201
+ ```bash
202
+ conda install setuptools -c conda-forge
203
+ ```
204
+
205
+ ### 3. Install the package in editable mode
206
+
207
+ From the project root (`NOWCASTINGCLI/`):
208
+
209
+ ```bash
210
+ pip install -e .
211
+ ```
212
+
213
+ This installs the required runtime dependencies (`rich`, `python-json-logger`)
214
+ and registers the `nowcastingcli` console script.
215
+
216
+ To also pull in the documentation toolchain (`docs` extra) and development
217
+ tooling (`dev` extra — `pytest`, `pytest-cov`, `setuptools`, `wheel`):
218
+
219
+ ```bash
220
+ pip install -e ".[docs,dev]"
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Verifying the CLI
226
+
227
+ ### Step 1 — Launch
228
+
229
+ ```bash
230
+ nowcastingcli
231
+ ```
232
+
233
+ You should see:
234
+
235
+ ```
236
+ NowcastingCLI v1.0 — type 'q' at any prompt to quit
237
+ ```
238
+
239
+ ### Step 2 — Enter 3 observations that simulate a worsening scenario
240
+
241
+ Respond to each prompt as shown below.
242
+
243
+ **Reading 1 — baseline**
244
+
245
+ ```
246
+ Enter pressure (hPa), or 'q' to quit: 1013
247
+ Temperature (°C): 18
248
+ Relative Humidity (%): 60
249
+ GPS Altitude (m): 340
250
+ ```
251
+
252
+ **Reading 2 — slight drop**
253
+
254
+ ```
255
+ Enter pressure (hPa), or 'q' to quit: 1011.5
256
+ Temperature (°C): 17
257
+ Relative Humidity (%): 72
258
+ GPS Altitude (m): 340
259
+ ```
260
+
261
+ **Reading 3 — accelerating drop + high humidity**
262
+
263
+ ```
264
+ Enter pressure (hPa), or 'q' to quit: 1009.8
265
+ Temperature (°C): 17
266
+ Relative Humidity (%): 86
267
+ GPS Altitude (m): 340
268
+ ```
269
+
270
+ ### Step 3 — Confirm expected dashboard output
271
+
272
+ After Reading 3 the dashboard panel should show:
273
+
274
+ | What to check | Expected |
275
+ |---|---|
276
+ | Nowcast verdict | `🔴 CONDITIONS WORSENING` |
277
+ | Reason | `Rapid pressure fall … + High humidity (86%)` |
278
+ | Pressure sparkline | characters tracking a downward trend (e.g. `█▅▂`) with a negative total delta |
279
+
280
+ The worsening verdict is triggered because:
281
+ - QNH pressure dropped more than 1 hPa between consecutive readings **and/or**
282
+ - humidity exceeded 85 % (Reading 3 = 86 %)
283
+
284
+ ---
285
+
286
+ ## Input Modes
287
+
288
+ ### Interactive mode (default)
289
+
290
+ ```bash
291
+ nowcastingcli
292
+ ```
293
+
294
+ The CLI prompts for each field one at a time. Type `q` at any pressure prompt to quit, or `e` to edit a past reading.
295
+
296
+ ### File input mode (`--input`)
297
+
298
+ ```bash
299
+ nowcastingcli --input path/to/observations.csv
300
+ ```
301
+
302
+ Reads observations from a CSV file and runs the full session non-interactively. Useful for automated testing, replaying scenarios, and log verification.
303
+
304
+ ### CSV file format
305
+
306
+ The file must have exactly these four columns (order matters, header required):
307
+
308
+ | Column | Unit | Valid range |
309
+ |---|---|---|
310
+ | `pressure_hpa` | hPa | 0.1 – 1100.0 |
311
+ | `temperature_c` | °C | -60 – 60 |
312
+ | `humidity_pct` | % | 0 – 100 |
313
+ | `altitude_m` | m | -500 – 5000 |
314
+
315
+ Example — `scripts/test_observations.csv`:
316
+
317
+ ```csv
318
+ pressure_hpa,temperature_c,humidity_pct,altitude_m
319
+ 1013,18,60,340
320
+ 1011.5,17,72,340
321
+ 1009.8,17,86,340
322
+ ```
323
+
324
+ Validation errors are reported with the row number and field name before the session starts:
325
+
326
+ ```
327
+ Input file error: Row 3: temperature = 99.0 out of range [-60, 60]
328
+ ```
329
+
330
+ ### Logging smoke-test scripts
331
+
332
+ Both scripts run `test_observations.csv` through the CLI and pretty-print the JSON log to verify that `DEBUG`, `INFO`, and `WARNING` events were written in the correct order.
333
+
334
+ **Bash (Git Bash / Linux / macOS):**
335
+
336
+ ```bash
337
+ bash scripts/test_logging.sh
338
+ ```
339
+
340
+ **Windows CMD:**
341
+
342
+ ```bat
343
+ scripts\test_logging.bat
344
+ ```
345
+
346
+ Expected log events per observation cycle, in order:
347
+
348
+ | Event | Level | Source |
349
+ |---|---|---|
350
+ | Session start | `INFO` | `main` |
351
+ | Raw sensor input | `DEBUG` | `main` |
352
+ | Observation recorded | `INFO` | `display` |
353
+ | Verdict change (when it occurs) | `WARNING` | `main` |
354
+
355
+ ---
356
+
357
+ ## Running scripts from VS Code
358
+
359
+ Once installed in editable mode the VS Code **▶ play button** works on any
360
+ script without a `launch.json`:
361
+
362
+ ```python
363
+ # test_scripts/Init_observation.py
364
+ from datetime import datetime
365
+ from nowcastingcli.models import Observation
366
+
367
+ obs = Observation(
368
+ timestamp = datetime.now(),
369
+ pressure_raw = 1013.25,
370
+ pressure_qnh = 1015.80,
371
+ temperature = 18.5,
372
+ humidity = 62.0,
373
+ altitude = 340.0
374
+ )
375
+
376
+ print(obs)
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Naming Conventions
382
+
383
+ This project follows **PEP 8** — the official Python style guide
384
+ (<https://peps.python.org/pep-0008/>). PEP 8 defines naming rules per
385
+ identifier type:
386
+
387
+ | Identifier type | Convention | Examples from this project |
388
+ |---|---|---|
389
+ | Functions | `snake_case` | `get_float`, `run`, `render_dashboard`, `normalize_pressure` |
390
+ | Variables | `snake_case` | `pressure_raw`, `pressure_qnh`, `min_val`, `pressure_delta` |
391
+ | Dataclass fields | `snake_case` | `timestamp`, `pressure_raw`, `pressure_qnh`, `humidity` |
392
+ | Classes | `PascalCase` | `Observation` |
393
+ | Module-level constants | `UPPER_SNAKE_CASE` | `WORSENING`, `STABLE`, `SPARKLINE_CHARS`, `VERDICT_STYLE` |
394
+ | Module-level singletons | `snake_case` | `console` (a `Console()` instance, not a true constant) |
395
+ | Private / internal helpers | `_snake_case` | `_obs` in test files (leading underscore signals "not public") |
396
+ | Test functions | `test_snake_case` | `test_run_quits_on_lowercase_q`, `test_valid_observation_is_created` |
397
+ | pytest fixtures | `snake_case` | `silence_console`, `make_obs` |
398
+
399
+ ### Why these rules matter
400
+
401
+ - **`snake_case` for functions and variables** is the most visible rule and
402
+ the one that most clearly separates Python from languages like Java or
403
+ JavaScript which use `camelCase` for the same things.
404
+ - **`PascalCase` for classes** makes it immediately obvious at the call site
405
+ that `Observation(...)` constructs an object, not calls a function.
406
+ - **`UPPER_SNAKE_CASE` for constants** signals "this value is fixed at module
407
+ load time and should not be reassigned".
408
+ - **Leading `_` for private helpers** is a convention, not enforcement — Python
409
+ does not block access, but it tells readers (and tools like pytest) that the
410
+ identifier is an implementation detail.
411
+
412
+ ### Type hints (PEP 484 / PEP 604)
413
+
414
+ Type hints use the modern syntax available from Python 3.10+:
415
+
416
+ ```python
417
+ def get_float(prompt: str, min_val: float, max_val: float) -> float: ...
418
+ def run() -> None: ...
419
+ observations: list[Observation] = [] # lowercase generic, not List[Observation]
420
+ float | None # union with | instead of Union[float, None]
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Unit Testing
426
+
427
+ ### Tools and conventions
428
+
429
+ This project uses **pytest** as the test runner and **`unittest.mock`** from
430
+ the Python standard library for mocking. They are used together — this is the
431
+ standard convention in the Python ecosystem:
432
+
433
+ | Tool | Role |
434
+ |---|---|
435
+ | `pytest` | Test runner, assertions (`assert`, `pytest.raises`, `pytest.approx`), fixtures |
436
+ | `unittest.mock` | Mocking (`patch`, `MagicMock`, `side_effect`) |
437
+
438
+ `unittest.mock` is part of the standard library — no extra install is needed.
439
+ The key rule is to avoid mixing **`unittest.TestCase`** (the class-based style)
440
+ with pytest, as that conflicts with fixtures and other pytest features.
441
+
442
+ ### Running tests
443
+
444
+ ```bash
445
+ # all tests
446
+ pytest -v
447
+
448
+ # one file
449
+ pytest tests/test_models.py -v
450
+
451
+ # one test
452
+ pytest tests/test_main.py::test_run_one_full_observation_cycle -v
453
+ ```
454
+
455
+ ### Mocking interactive input
456
+
457
+ `main.py` uses `rich.prompt.Prompt.ask` to read user input. In tests, that
458
+ call is replaced with a mock so tests run non-interactively:
459
+
460
+ ```python
461
+ from unittest.mock import patch
462
+
463
+ # Single scripted answer
464
+ with patch("nowcastingcli.main.Prompt.ask", return_value="20.0"):
465
+ result = get_float("Temperature", -60, 60)
466
+
467
+ # Queue of scripted answers (consumed in order)
468
+ with patch("nowcastingcli.main.Prompt.ask", side_effect=["abc", "20.0"]):
469
+ result = get_float("Temperature", -60, 60)
470
+ ```
471
+
472
+ `side_effect` with a list makes the mock return each value in turn, which lets
473
+ you script retry loops and multi-prompt sequences without touching the terminal.
474
+
475
+ ### Test helper pattern (`_obs`)
476
+
477
+ Test files use a factory helper prefixed with `_` to build valid objects with
478
+ sensible defaults. The leading underscore tells pytest not to collect it as a
479
+ test:
480
+
481
+ ```python
482
+ def _obs(**overrides):
483
+ defaults = dict(pressure_raw=1013.25, humidity=50.0, ...)
484
+ defaults.update(overrides) # caller overrides only what it needs
485
+ return Observation(**defaults) # unpack dict as keyword arguments
486
+
487
+ # Each test changes only the one field it cares about
488
+ def test_humidity_above_100_raises():
489
+ with pytest.raises(ValueError):
490
+ _obs(humidity=100.1)
491
+ ```
492
+
493
+ ---
494
+
495
+ ## `normalize_pressure()` — Barometric QNH Formula
496
+
497
+ ### What it computes
498
+
499
+ A weather station sits at some altitude. Its raw reading is **station pressure** (what the air actually weighs at that height). To compare stations at different elevations — and to produce the sea-level pressure shown on synoptic charts — you need **QNH**, the pressure the station *would* read if it were at sea level. `normalize_pressure()` does that conversion.
500
+
501
+ ### The math, step by step
502
+
503
+ The underlying physics is the **hypsometric (barometric) formula**, derived from hydrostatic equilibrium and the ideal-gas law:
504
+
505
+ ```
506
+ P₀ = P_station × (T₀ / T_station) ^ (g·M / R·L)
507
+ ```
508
+
509
+ Rearranging so T₀ appears only once:
510
+
511
+ ```
512
+ P₀ = P_station × (1 − L·h / T₀) ^ −(g·M / R·L)
513
+ ```
514
+
515
+ The code substitutes each piece:
516
+
517
+ **`0.0065` — the temperature lapse rate, L (K/m)**
518
+
519
+ The International Standard Atmosphere (ISA) assumes temperature falls by **6.5 K per 1000 m** of altitude. This is `L = 0.0065 K/m`.
520
+
521
+ **`temperature_c + 0.0065 * altitude_m + 273.15` — sea-level temperature, T₀ (K)**
522
+
523
+ The station temperature is measured at `altitude_m` above sea level. To get what the temperature *would be* at sea level, you add back the lapse-rate warming for the full column:
524
+
525
+ ```
526
+ T₀ = T_station_Kelvin + L × h
527
+ = (temperature_c + 273.15) + 0.0065 × altitude_m
528
+ ```
529
+
530
+ The `+ 273.15` converts Celsius to Kelvin.
531
+
532
+ **`(0.0065 * altitude_m) / T₀` — the fractional temperature drop**
533
+
534
+ This ratio is `L·h / T₀`, the fraction by which the temperature column contracts between station and sea level. It is always < 1 for realistic inputs.
535
+
536
+ **`(1 − L·h/T₀) ^ −5.257` — the pressure correction factor**
537
+
538
+ The exponent **5.257** is `g·M / (R·L)`:
539
+
540
+ | Symbol | Meaning | Value |
541
+ |--------|---------|-------|
542
+ | g | gravitational acceleration | 9.80665 m/s² |
543
+ | M | molar mass of dry air | 0.028964 kg/mol |
544
+ | R | universal gas constant | 8.31446 J/(mol·K) |
545
+ | L | lapse rate | 0.0065 K/m |
546
+
547
+ ```
548
+ g·M / (R·L) = (9.80665 × 0.028964) / (8.31446 × 0.0065) ≈ 5.257
549
+ ```
550
+
551
+ The **negative** exponent flips the ratio: because pressure decreases with altitude, correcting upward to sea level requires multiplying by something **greater than 1**, which `(fraction < 1)^−5.257` delivers.
552
+
553
+ **Final multiplication**
554
+
555
+ ```
556
+ QNH = P_station × correction_factor
557
+ ```
558
+
559
+ For a typical mountain station at 1500 m, 15 °C, 850 hPa, the correction factor is ≈ 1.196, giving QNH ≈ 1016 hPa — a plausible sea-level pressure.
560
+
561
+ ### Approximations being made
562
+
563
+ | Approximation | What it assumes | Reality |
564
+ |--------------|-----------------|---------|
565
+ | Constant lapse rate | Temperature always drops at 6.5 K/km | Varies with weather: inversions, convective instability, fronts |
566
+ | Dry air molar mass | Uses M = 0.02896 kg/mol | Humid air is lighter (M_water = 0.018). Error scales with humidity and altitude |
567
+ | Ideal gas | PV = nRT exactly | Small correction at high pressures, negligible here |
568
+ | Hydrostatic equilibrium | No vertical accelerations | Breaks in strong convection or turbulence |
569
+ | Constant gravity | g does not change with altitude | g decreases by ~0.03% per 100 m — negligible below 5 km |
570
+
571
+ ### When it breaks down
572
+
573
+ - **Above ~5000 m**: the ISA lapse rate diverges from the actual atmosphere; the tropopause (~11 km) has L ≈ 0 and the formula is simply wrong above it.
574
+ - **Temperature inversions**: when temperature *increases* with altitude (common at night, in fog, near fronts), the real pressure correction can differ substantially from what the 6.5 K/km constant predicts.
575
+ - **High-humidity environments**: using dry-air molar mass underestimates the correction slightly; significant in tropical boundary layers.
576
+ - **Precision aviation/meteorology**: ICAO QNH procedures tolerate this approximation, but scientific reanalysis systems use more sophisticated vertical integration.
577
+
578
+ For the intended use case — surface weather station normalization below 5000 m in mid-latitudes — the error is typically < 1–2 hPa, which is within observational uncertainty.
579
+
580
+ ---
581
+
582
+ ## Logging
583
+
584
+ ### Strategy
585
+
586
+ The app uses Python's standard `logging` module, configured once at startup via `logging_config.py` using `dictConfig`. Two handlers run in parallel:
587
+
588
+ | Handler | Destination | Level | Format |
589
+ |---------|-------------|-------|--------|
590
+ | `console` | `stderr` | `WARNING` and above | Plain text with timestamp |
591
+ | `file` | `logs/nowcastingcli.log` | `DEBUG` and above | JSON (via `python-json-logger`) |
592
+
593
+ The `logs/` directory is created automatically on first run. The file handler rotates at 1 MB and keeps 3 backups.
594
+
595
+ ### Log levels in use
596
+
597
+ | Level | Where | What is logged |
598
+ |-------|-------|----------------|
599
+ | `DEBUG` | `main.py` | Raw sensor input per observation (pressure, temperature, humidity, altitude) |
600
+ | `INFO` | `display.py` | Each observation recorded, with `pressure_qnh` and the current verdict |
601
+ | `WARNING` | `main.py` | Verdict transitions (e.g. `stable → worsening`) |
602
+ | `INFO` | `main.py` | Session start |
603
+
604
+ ### Implementation
605
+
606
+ `logging_config.py` was built with the following design goals:
607
+
608
+ - Use the `dictConfig` pattern to declare the full logging topology in one place as a plain dictionary, keeping configuration separate from application code.
609
+ - A `RotatingFileHandler` writes JSON-formatted logs to `logs/nowcastingcli.log` at `DEBUG` level, capturing all events for post-session analysis.
610
+ - A `StreamHandler` to `stderr` is set to `WARNING` only, so the terminal stays clean during normal operation.
611
+ - `setup_logging()` creates the `logs/` directory if it does not exist before applying the config, avoiding a `FileNotFoundError` on first run.
612
+
613
+ ### Wiring
614
+
615
+ `setup_logging()` is called once in `main.py` before any `getLogger()` call. Other modules (`display.py`, `heuristics.py`) obtain a logger with `logging.getLogger(__name__)` and rely on the handlers already being registered by the time they are imported.
616
+
617
+ ---
618
+
619
+ ## Interactive REPL
620
+
621
+ ```bash
622
+ python -i test_scripts/Init_observation.py
623
+ ```
624
+
625
+ Executes the script and leaves the interpreter open with all variables defined.
@@ -0,0 +1,12 @@
1
+ """NowcastingCLI package root.
2
+
3
+ Exposes ``__version__``, read from installed package metadata rather than
4
+ hardcoded, so it always matches what was installed.
5
+ """
6
+
7
+ from importlib.metadata import version
8
+
9
+ # Must match [project] name in pyproject.toml exactly ("nowcastingcli", not
10
+ # the module path: "nowcastingcli.main:cli"); only resolves once the package is installed.
11
+ __version__ = version("nowcastingcli")
12
+