Weather, space weather, and how they affect you — Python SDK with sync/async support, caching, and typed responses.
Project description
SkyPulse
Weather, space weather, and how they affect you — Python SDK with sync/async support, LRU caching, and typed responses.
Features
- Typed responses — all data returned as frozen dataclasses, not raw dicts
- Sync + async — identical API surface for both clients
- LRU caching — configurable TTL and max entries, thread-safe
- Air quality — AQI (1-5), 8 pollutant concentrations, 4-day hourly forecast
- UV index — real-time + 5-day forecast from CurrentUVIndex (no extra API key)
- Circadian light — day length, effective light hours, quality rating for wellness apps
- Geomagnetic storms — real-time Kp index and 3-day forecast from NOAA SWPC
- Health impact — Kp-based health risk assessment with latitude adjustment
- Storm alerts — location-aware alerts with aurora visibility prediction
- Auto-location — IP-based geolocation for hands-free weather and storm queries
- Translation — all labels available in English and Ukrainian
- Error hierarchy — distinct exceptions for auth, rate limit, not found, timeout, network
- Retry with backoff — automatic retry on 5xx and 429 with exponential backoff
- API key redaction — keys never appear in logs, exceptions, or repr output
Installation
pip install skypulse-weather
Requires Python 3.9 — 3.14. Dependencies: httpx, cachetools.
API Keys
| Feature | Key Required | Source |
|---|---|---|
| Weather, Forecast, Geocoding, Air Quality | OpenWeather API key | openweathermap.org |
| UV Index | None (free, no signup) | currentuvindex.com |
| Geomagnetic Storms, Forecast | None (free, no signup) | NOAA SWPC |
| IP Geolocation | None (free, no signup) | ip-api.com |
| Circadian Light | OpenWeather API key | Computed from weather data |
export SKYPULSE_API_KEY="your-openweather-api-key"
# or
export OPENWEATHER_API_KEY="your-openweather-api-key"
Quick Start
from skypulse import SkyPulseClient, Units
client = SkyPulseClient(api_key="your-api-key", units=Units.METRIC)
weather = client.get_current_weather(city="Kyiv")
print(f"{weather.location.name}: {weather.temperature}°C, {weather.condition.description}")
# "Kyiv: 18.5°C, scattered clouds"
print(f"Wind: {weather.wind.speed} m/s, Humidity: {weather.humidity}%")
# "Wind: 3.6 m/s, Humidity: 72%"
Weather
Current Weather
weather = client.get_current_weather(city="Kyiv")
print(weather.temperature) # 18.5
print(weather.feels_like) # 17.2
print(weather.humidity) # 72
print(weather.pressure) # 1013
print(weather.condition.description) # "scattered clouds"
print(weather.wind.speed) # 3.6
print(weather.wind.direction) # 220
print(weather.location.name) # "Kyiv"
Multiple location formats are supported:
client.get_current_weather(city="Lviv")
client.get_current_weather(lat=50.45, lon=30.52) # Kyiv
client.get_current_weather(city_id=2643743) # London
client.get_current_weather(zip_code="10001,us") # New York
5-Day Forecast
forecast = client.get_forecast(city="Berlin", count=8) # next 24 hours
print(forecast.location.name) # "Berlin"
for entry in forecast.entries:
print(f"{entry.forecast_at}: {entry.temperature}°C — {entry.condition.description}")
# "2026-04-05 18:00:00: 14.2°C — light rain"
Geocoding
# City name to coordinates
locations = client.geocode("London")
for loc in locations:
print(f"{loc.name}, {loc.country}: ({loc.latitude}, {loc.longitude})")
# "London, GB: (51.5074, -0.1278)"
# Coordinates to city name
locations = client.reverse_geocode(lat=48.8566, lon=2.3522)
print(locations[0].name) # "Paris"
Air Quality
The SDK queries the OpenWeather Air Pollution API and returns the overall AQI with individual pollutant concentrations.
| AQI | Label | Description |
|---|---|---|
| 1 | Good | Minimal pollution |
| 2 | Fair | Acceptable quality |
| 3 | Moderate | Noticeable for sensitive groups |
| 4 | Poor | Significant pollution |
| 5 | Very Poor | Severe air quality |
Pollutants measured: CO, NO, NO2, O3, SO2, PM2.5, PM10, NH3 (all in μg/m³).
aq = client.get_air_quality(city="Kyiv")
print(aq.aqi) # 3
print(aq.label) # "Moderate"
print(aq.pm2_5) # 12.3
print(aq.o3) # 62.44
# 4-day hourly forecast
forecast = client.get_air_quality_forecast(lat=50.45, lon=30.52)
for entry in forecast:
print(f"{entry.measured_at}: AQI {entry.aqi} — {entry.label}")
# "2026-04-05 12:00:00+00:00: AQI 2 — Fair"
UV Index
No API key required — uses CurrentUVIndex (500 req/day).
| UV Index | Risk Level | Protection Needed |
|---|---|---|
| 0-2 | Low | Minimal |
| 3-5 | Moderate | Sunscreen, seek shade midday |
| 6-7 | High | Reduce sun exposure 10am-4pm |
| 8-10 | Very High | Extra precautions, avoid midday sun |
| 11+ | Extreme | Avoid outdoor activity midday |
uv = client.get_uv_index(city="Rome")
print(uv.value) # 6.2
print(uv.risk_level) # "high"
print(uv.risk_label) # "High"
# 5-day hourly forecast
forecast = client.get_uv_forecast(lat=50.45, lon=30.52)
for entry in forecast:
print(f"{entry.forecast_at}: UV {entry.value}")
# "2026-04-05 13:00:00+00:00: UV 6.5"
Circadian Light Exposure
Computes natural light exposure from sunrise/sunset and cloud cover (free tier, no extra API).
| Effective Light Hours | Quality |
|---|---|
| >= 12 | Excellent |
| 9-12 | Good |
| 6-9 | Moderate |
| < 6 | Poor |
| 0 (polar night) | Extreme Dark |
| 24 (midnight sun) | Extreme Light |
light = client.get_circadian_light(city="Lviv")
print(light.day_length_hours) # 13.25
print(light.cloud_cover_percent) # 45
print(light.effective_light_hours) # 9.94
print(light.quality) # "good"
print(light.quality_label) # "Good"
Geomagnetic Storms
Real-time and forecast geomagnetic data from NOAA Space Weather Prediction Center (SWPC). The SDK fetches the planetary Kp index — a global measure of geomagnetic disturbance on a 0-9 scale — and maps it to the NOAA G-scale (G0-G5) with a human-readable severity label. A Kp value of 5 or higher indicates an active geomagnetic storm.
| Kp Index | G-Scale | Severity |
|---|---|---|
| 0-4 | G0 | Quiet |
| 5 | G1 | Minor storm |
| 6 | G2 | Moderate storm |
| 7 | G3 | Strong storm |
| 8 | G4 | Severe storm |
| 9 | G5 | Extreme storm |
Current Conditions
storm = client.get_magnetic_storm()
print(storm.kp_index) # 5.33
print(storm.g_scale) # "G1"
print(storm.severity) # "Minor storm"
print(storm.is_storm) # True
print(storm.observed_at) # 2026-04-05 06:00:00+00:00
print(storm.data_age_seconds) # 847
print(storm.stale) # False
print(storm.station_count) # 8
The stale flag indicates that NOAA was unreachable and the response was served from cache. The data_age_seconds field shows how old the observation is.
3-Day Magnetic Forecast
The SDK retrieves NOAA's 3-day planetary Kp index forecast, which provides predicted geomagnetic activity in 3-hour intervals. This enables proactive alerting — warn users about upcoming storms before they happen.
Each forecast entry covers a 3-hour window and includes both observed (past) and predicted (future) periods, distinguished by the is_observed flag.
forecast = client.get_magnetic_forecast()
for entry in forecast:
kind = "observed" if entry.is_observed else "predicted"
print(f"{entry.period_start} — {entry.period_end}: Kp {entry.predicted_kp} — {entry.severity} [{kind}]")
# "2026-04-05 09:00:00 — 2026-04-05 12:00:00: Kp 5.0 — Minor storm [predicted]"
if entry.is_storm:
print(" Storm expected!") # " Storm expected!"
Find upcoming storms:
upcoming_storms = [e for e in client.get_magnetic_forecast() if e.is_storm and not e.is_observed]
if upcoming_storms:
first = upcoming_storms[0]
print(f"Storm predicted at {first.period_start}: Kp {first.predicted_kp} — {first.severity}")
# "Storm predicted at 2026-04-05 09:00:00: Kp 5.0 — Minor storm"
Data Freshness & Resilience
NOAA SWPC updates the Kp index approximately every 15 minutes. The SDK caches storm data for 10 minutes by default and uses stale-while-revalidate fallback — if NOAA is temporarily unreachable, the last known data is returned with stale=True rather than raising an error. Stale data expires after 30 minutes for current conditions and 6 hours for forecasts.
Health Impact
Assess potential health effects of geomagnetic activity based on the current Kp index.
impact = client.get_storm_health_impact()
print(impact.level) # "moderate"
print(impact.affected_systems) # ["cardiovascular", "nervous"]
print(impact.recommendations) # ["Migraine-prone and cardiovascular-sensitive individuals should monitor symptoms.", ...]
print(impact.disclaimer) # "Health impact information is for general awareness only..."
Storm Alerts
Location-aware alerts that combine magnetic storm data with latitude-adjusted health impact and aurora visibility.
# Provide coordinates explicitly (Stockholm)
alert = client.get_storm_alert(lat=59.33, lon=18.07)
# Or use auto-location (IP-based)
alert = client.get_storm_alert(auto_locate=True)
print(alert.storm.severity) # "Minor storm"
print(alert.health_impact.level) # "low"
print(alert.location_name) # "Stockholm"
print(alert.aurora_visible) # True
print(alert.latitude_zone) # "mid"
Health impact is adjusted based on latitude — higher latitudes experience stronger effects during geomagnetic storms.
Auto-Location
Detect user location from IP address for hands-free queries:
# Client-level auto-locate
client = SkyPulseClient(api_key="key", auto_locate=True)
weather = client.get_current_weather() # no city needed
# Per-request auto-locate
weather = client.get_current_weather(auto_locate=True)
# Get location directly
location = client.get_location()
print(f"{location.name}, {location.country}: ({location.latitude}, {location.longitude})")
# "Kyiv, UA: (50.4501, 30.5234)"
Translation (i18n)
All human-readable labels support English and Ukrainian:
client_uk = SkyPulseClient(api_key="key", language="uk")
aq = client_uk.get_air_quality(city="Kyiv")
print(aq.label) # "Помірно"
uv = client_uk.get_uv_index(lat=50.45, lon=30.52)
print(uv.risk_label) # "Високий"
storm = client_uk.get_magnetic_storm()
print(storm.severity) # "Сильна буря"
light = client_uk.get_circadian_light(city="Lviv")
print(light.quality_label) # "Добре"
Async Usage
import asyncio
from skypulse import AsyncSkyPulseClient, Units
async def main():
async with AsyncSkyPulseClient(api_key="your-api-key", units=Units.METRIC) as client:
weather = await client.get_current_weather(city="Lviv")
print(f"{weather.temperature}°C") # "12.3°C"
aq = await client.get_air_quality(city="Kyiv")
print(f"AQI: {aq.label}") # "AQI: Moderate"
uv = await client.get_uv_index(lat=41.9, lon=12.5) # Rome
print(f"UV: {uv.risk_label}") # "UV: High"
light = await client.get_circadian_light(city="Stockholm")
print(f"Light: {light.quality_label}") # "Light: Good"
storm = await client.get_magnetic_storm()
print(f"{storm.severity} (Kp {storm.kp_index})") # "Minor storm (Kp 5.33)"
alert = await client.get_storm_alert(lat=59.33, lon=18.07) # Stockholm
print(f"Aurora visible: {alert.aurora_visible}") # "Aurora visible: True"
asyncio.run(main())
All sync methods have async equivalents with identical signatures.
Error Handling
from skypulse import (
SkyPulseClient,
SkyPulseError,
AuthenticationError,
NotFoundError,
RateLimitError,
ServiceUnavailableError,
)
client = SkyPulseClient(api_key="your-api-key")
try:
weather = client.get_current_weather(city="Atlantis")
except NotFoundError as e:
print(f"City not found: {e.message}") # "City not found: city not found"
except AuthenticationError:
print("Check your API key") # "Check your API key"
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds") # "Rate limited. Retry after 60 seconds"
except ServiceUnavailableError:
print("External service (NOAA/geolocation) is down") # "External service (NOAA/geolocation) is down"
except SkyPulseError as e:
print(f"Something went wrong: {e.message}") # "Something went wrong: ..."
Configuration
Unit Systems
from skypulse import Units
# Client-level default
client = SkyPulseClient(api_key="key", units=Units.IMPERIAL)
# Per-request override
weather = client.get_current_weather(city="New York", units=Units.METRIC)
| Units | Temperature | Wind Speed |
|---|---|---|
STANDARD |
Kelvin | m/s |
METRIC |
Celsius | m/s |
IMPERIAL |
Fahrenheit | mph |
Caching
from skypulse import SkyPulseClient, CacheConfig
client = SkyPulseClient(
api_key="your-api-key",
cache=CacheConfig(
enabled=True,
ttl=600, # weather cache TTL (seconds)
max_entries=256, # weather cache size
geo_cache_ttl=3600, # geocode cache TTL (default: 1 hour)
geo_cache_max_entries=256, # geocode cache size
),
)
# Second call returns from cache
weather1 = client.get_current_weather(city="Rome")
weather2 = client.get_current_weather(city="Rome") # instant, from cache
# Force fresh data
weather3 = client.get_current_weather(city="Rome", skip_cache=True)
Geocode results (city-to-coordinates) are cached separately with a longer TTL since coordinates rarely change. When caching is enabled, get_circadian_light() reuses cached weather data from get_current_weather(), and concurrent async UV requests are deduplicated automatically.
Adaptive TTL: Cache TTL automatically scales based on your daily API usage — fresh data when quota is plentiful, longer caching when nearing limits:
| Usage | Cache TTL |
|---|---|
| <50% | Base TTL (default 5 min) |
| 50-75% | 30 min |
| >75% | 1 hour |
Configure your daily limits via CacheConfig(owm_daily_limit=1000, uv_daily_limit=500).
Storm and geolocation data use separate caches with stale-while-revalidate fallback — if the external service is temporarily unavailable, the SDK returns the last known data with a stale=True flag.
Prefetch
Fetch all weather data for a location in one call:
from skypulse import AsyncSkyPulseClient, CacheConfig
async with AsyncSkyPulseClient(api_key="key", cache=CacheConfig()) as client:
snapshot = await client.prefetch(lat=50.45, lon=30.52)
print(snapshot.weather.temperature)
print(snapshot.uv.value)
print(snapshot.circadian.quality)
# Individual calls after prefetch are cache hits
weather = await client.get_current_weather(lat=50.45, lon=30.52) # instant
The WeatherSnapshot contains all data: weather, forecast, air quality, UV, circadian, magnetic storms. Partial failures (e.g., UV API down) return None for that field with error details in snapshot.errors.
Retry
from skypulse import RetryConfig
client = SkyPulseClient(
api_key="key",
retry=RetryConfig(enabled=True, max_retries=3, backoff_factor=0.5),
)
Retries on 429 (rate limit), 500, 502, 503 with exponential backoff. Respects Retry-After headers.
API Coverage
| Source | Endpoint | Method |
|---|---|---|
| OpenWeather | Current Weather | get_current_weather() |
| OpenWeather | 5-Day Forecast | get_forecast() |
| OpenWeather | Direct Geocoding | geocode() |
| OpenWeather | Reverse Geocoding | reverse_geocode() |
| OpenWeather | Air Pollution | get_air_quality() |
| OpenWeather | Air Pollution Forecast | get_air_quality_forecast() |
| CurrentUVIndex | UV Index | get_uv_index() |
| CurrentUVIndex | UV Forecast | get_uv_forecast() |
| NOAA SWPC | Planetary K-Index | get_magnetic_storm() |
| NOAA SWPC | K-Index Forecast | get_magnetic_forecast() |
| ip-api.com | IP Geolocation | get_location() |
| Derived | Health Impact | get_storm_health_impact() |
| Derived | Storm Alert | get_storm_alert() |
| Derived | Circadian Light | get_circadian_light() |
Development
pip install -e ".[dev]"
pytest
mypy src/skypulse
ruff check src/
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file skypulse_weather-2.1.0.tar.gz.
File metadata
- Download URL: skypulse_weather-2.1.0.tar.gz
- Upload date:
- Size: 153.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3eeb3b8e9ffb2c04b67c5c34427d99795188916121e31b82aac1f07992c4d017
|
|
| MD5 |
7513847d086c8227be1e2a912a93f4c1
|
|
| BLAKE2b-256 |
b07d5e49008ee4edac16093db3fab73918161e08297af7916e11cf79578cc451
|
File details
Details for the file skypulse_weather-2.1.0-py3-none-any.whl.
File metadata
- Download URL: skypulse_weather-2.1.0-py3-none-any.whl
- Upload date:
- Size: 40.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7fb2c3f68401ef3cd4ac4dcbeb0623d97d0a7363cdff9d5411f36c658bf43ad
|
|
| MD5 |
b4c0093a98b7ddf61f790864547d0af5
|
|
| BLAKE2b-256 |
11632b580fc83027a1b24c744f202bb10ca23cf69bbecbd6297c95a712e24b20
|