Skip to main content

PyWeatherEnriched

Real geocoding + real historical weather, wired into a Rust core with pandas/numpy climate feature engineering on top.

PyPI Python 3.10+ Tests License: Proprietary

Given a location name and a timestamp, PyWeatherEnriched geocodes the location (OpenStreetMap Nominatim) and looks up the real, genuinely observed historical weather for that place and hour (Open-Meteo's free Archive API) — no formula-generated or fabricated numbers. The lookup/geocoding core is Rust (via PyO3) for speed and a small memory footprint; a Python layer on top adds pandas/numpy feature engineering (rolling aggregates, heating/cooling degree-days, cyclical time encoding, anomaly z-scores) useful for feeding weather into an ML pipeline.

Install

pip install pyweatherenriched

Requires Python 3.10+. CI builds prebuilt wheels for Linux (x86_64/ARM64), macOS (Intel/Apple Silicon), and Windows (x86_64) on every release, but the CI→PyPI publish step is currently broken (PyPI rejects the upload with 403 Invalid or non-existent authentication information — a stale/invalid API token), so only a macOS ARM64 wheel is actually published on PyPI today. On any other platform, pip install pyweatherenriched will fall back to building the sdist from source, which does need a Rust toolchain (see Requirements below) despite the other-platform wheels existing as CI build artifacts.

Quick start

import pyweatherenriched as pwe

enricher = pwe.WeatherEnricher()
result = enricher.enrich_row("New York", "2024-06-15T12:00:00")

print(result)
# {'location': 'New York', 'latitude': 40.7127281, 'longitude': -74.0060152,
#  'temperature': 19.4, 'humidity': 74.0, 'condition': 'Clear',
#  'timestamp': '2024-06-15T12:00:00'}

enrich_row makes two real network calls the first time it sees a location/timestamp pair (one to Nominatim to geocode location, one to Open-Meteo for the historical weather at those coordinates); the in-process cache means repeating the same lookup doesn't hit the network again:

fresh = pwe.WeatherEnricher()
fresh.enrich_row("New York", "2024-06-15T12:00:00")  # network
fresh.enrich_row("New York", "2024-06-15T12:00:00")  # cache hit
print(fresh.cache_stats())  # {'hits': 1, 'misses': 1, 'size': 1}

Historical backfill

enrich_row/enrich_batch fetch one day of data per call. To backfill a date range for a location, enrich_range geocodes once and fetches every hourly observation across the whole range in a single Open-Meteo request:

enricher = pwe.WeatherEnricher()
rows = enricher.enrich_range("Chicago", "2024-06-01", "2024-06-07")
print(len(rows))  # ~168 (7 days x 24 hours, minus any hours Open-Meteo has no data for)

Hours Open-Meteo has no observation for (e.g. the tail of a range that runs past the latest available data) are omitted from the result rather than filled with a fabricated value.

Feature engineering on a DataFrame

enrich_dataframe bridges the Rust core to pandas — fetch real weather for every row of a DataFrame in one call — and the features functions build standard climate ML features on top of the result:

import pandas as pd
import pyweatherenriched as pwe

orders = pd.DataFrame({
    "order_id": ["A1", "A2", "A3"],
    "location": ["Chicago", "Miami", "Denver"],
    "timestamp": ["2024-01-15T12:00:00", "2024-06-15T12:00:00", "2024-03-15T12:00:00"],
})

enricher = pwe.WeatherEnricher()
enriched = pwe.enrich_dataframe(enricher, orders)     # + latitude/longitude/temperature/humidity/condition
features = pwe.build_features(enriched)                # + hdd/cdd, cyclical time, rolling stats, anomaly z-score

print(features[["order_id", "temperature", "hdd", "cdd", "temperature_zscore"]])

Rows whose lookup fails (unresolvable location, upstream error) get NaN in the new columns instead of raising or fabricating a value, so one bad row doesn't lose the batch.

build_features is a convenience pipeline; each step is also a standalone function you can call individually with more control:

Function What it adds
add_degree_days(df, temp_col, base_temp=18.0) hdd, cdd — heating/cooling degree-days (standard energy-demand/agriculture signal: max(0, base - t) / max(0, t - base))
add_cyclical_time_features(df, timestamp_col) hour_sin/cos, day_of_week_sin/cos, day_of_year_sin/cos — sin/cos encoding so e.g. 23:59 and 00:00 stay numerically adjacent
add_rolling_features(df, value_col, window, group_col=None, stats=(...)) trailing rolling aggregates (mean/std/min/max/...), optionally computed independently per group (e.g. per location)
add_anomaly_features(df, value_col, group_col=None, baseline="expanding"|"rolling"|"global") a z-score measuring how unusual each reading is relative to a chosen baseline

Caching

WeatherEnricher has a small built-in LRU cache. For larger workloads, EnhancedCache adds a second, SQLite-backed persistent tier with geospatial-proximity matching, TTL expiration, batch deduplication, and date-range queries:

from pyweatherenriched import EnhancedCache

cache = EnhancedCache(cache_size=5000, db_path="weather_cache.db")
cache.set_proximity_radius(10.0)  # treat lookups within 10km as cache hits
cache.set_ttl(72)                 # hours before an entry expires

cache.put("New York", 40.7128, -74.0060, 15.2, 65.0, "Partly Cloudy", "2024-01-15T12:00:00Z")
result = cache.get("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z")

# Deduplicate a batch before making any API calls.
batch = [
    ("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z"),
    ("New York", 40.7128, -74.0060, "2024-01-15T12:00:00Z"),  # duplicate
]
missing_indices, cache_hits = cache.deduplicate_batch(batch)

# Every observation in a date range near a point (requires db_path).
rows = cache.get_range(40.7128, -74.0060, "2024-01-01T00:00:00Z", "2024-01-31T23:59:59Z")

stats = cache.stats()
print(f"hit ratio: {stats['hit_ratio']:.1%}")

get() checks the memory tier, then the persistent tier (if db_path was given), then falls back to a proximity search; get_range only queries the persistent tier, so it always returns [] without db_path.

What's real vs. not (yet)

  • Real: forward geocoding (Nominatim), historical weather (Open-Meteo Archive API), in-memory LRU cache, SQLite-backed EnhancedCache (proximity matching, TTL, dedup, date-range queries), pandas/numpy feature engineering.
  • Not yet exposed to Python: the crate has additional Rust-side, independently unit-tested building blocks — elevation/lapse-rate adjustment (real SRTM GeoTIFF parsing), urban-heat-island modeling (real OSM building-density analysis), and OSM-based reverse geocoding — that aren't wired into the Python API yet. They live under src/geospatial/ if you want to build on them.
  • Deliberately unimplemented: vegetation/NDVI, soil, and flood-risk layers, plus additional commercial reverse-geocoding provider backends, are framework stubs (src/geospatial/optional.rs) that return a clear "not yet implemented" error rather than fake data.
  • Fixed (external critique, verified real gaps): src/enricher.rs and src/geocoder.rs now retry through src/http_retry.rs — exponential backoff with jitter on 429/5xx responses and transport errors, honoring a numeric Retry-After header when Nominatim/Open-Meteo send one — where every request previously went out once with no retry at all. Range-based historical backfill is real too: WeatherEnricher.enrich_range(location, start_date, end_date) (exposed to Python as enrich_range) geocodes once and fetches the entire date range in a single Open-Meteo request, instead of the one-call-per-day pattern enrich/enrich_batch still use for their single-timestamp use case. See "Historical backfill" above. (Note: the "no local caching layer" critique item never held — EnhancedCache is a real SQLite-backed persistent cache with TTL and proximity matching; it's just opt-in rather than auto-wired into WeatherEnricher.enrich(), which remains its own small, separate TODO.)

Development

git clone https://github.com/Mullassery/PyWeatherEnriched.git
cd PyWeatherEnriched

maturin develop --release   # build the Rust extension + install editable
cargo test --lib            # Rust unit tests (32 tests)
pip install -e ".[dev]"
pytest tests/ -v            # Python tests (33 tests, incl. live-network ones)

Note: plain cargo test (without --lib) currently fails to compile — tests/phase2_integration_test.rs references ParallelEnricher, BatchResolver, StreamingReader/StreamingWriter, and DatabaseConfig/ DatabaseType from src/parallel.rs, src/batch_resolver.rs, src/streaming_io.rs, and src/database.rs — source files that exist on disk but aren't declared as mods in src/lib.rs, so they're not part of the compiled crate and that integration test is stale. Use cargo test --lib to run the real, passing unit test suite.

cargo run --bin pyweatherenriched-validate is a small smoke-test CLI that exercises real geocoding + weather fetch end to end — useful to confirm a build/environment can actually reach Nominatim and Open-Meteo.

See BUILD.md for wheel-building/release details.

Requirements

  • Python 3.10+
  • Rust 1.75+ (only if building from source — not needed to pip install)
  • Network access to nominatim.openstreetmap.org and archive-api.open-meteo.com at call time (no API key needed for either)

License

Proprietary — see LICENSE. Source is public on GitHub for review; this isn't an open-source license, so redistribution/reuse isn't granted by default.

Support

Release files for pyweatherenriched 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyweatherenriched 0.7.0
File Size Uploaded
pyweatherenriched-0.7.0.tar.gz 98.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyweatherenriched 0.7.0
File Interpreter ABI Platform
pyweatherenriched-0.7.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details

Total release size: 2.6 MB

Release files / pyweatherenriched-0.7.0.tar.gz

Download URL pyweatherenriched-0.7.0.tar.gz
Size 98.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c72aca6e0940f3fdfabf9e81caa5808441aa61a2ea9ceae76b63e08c8e8d1abd
BLAKE2b-256 checksum
How to use checksums
569afce61d01fc27826f2718bca01a8d0b4d95bc545e9f25e4d5fa4e3d948b51
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / pyweatherenriched-0.7.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL pyweatherenriched-0.7.0-cp310-abi3-macosx_11_0_arm64.whl
Size 2.5 MB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
d121e1217bd2642c34b6506620aede150cc0f8aa5585d5381ecccbc5d104230d
BLAKE2b-256 checksum
How to use checksums
ccb5bc1a3ce06d16bf280af7e215d2047cbd2cac9a225e181f6fed90b5f76336
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

1 release file

0.4.0

1 release file

0.3.0

1 release file

0.2.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page