Skip to content

DarrenN/openweathermap

Repository files navigation

OpenWeatherMap API Client

Common Lisp client library for multiple OpenWeatherMap APIs: https://openweathermap.org/api

https://github.com/DarrenN/openweathermap/actions/workflows/ci.yml/badge.svg

✨ AI Disclosure

Large portions of the codebase are AI-assisted. For more information checkout AGENTS.md and /plans.

Status

Active development. Targeting Quicklisp inclusion.

Goals

  • Idiomatic Common Lisp client interface.
  • Strong unit test suite.
  • Separate integration test suite for live API checks.
  • Project-local OpenAPI specifications per API family.

Consumer Quickstart

Set your API key:

export OPENWEATHER_API_KEY="your-key"

Load the library via Quicklisp:

(ql:quickload :openweathermap)

Configure API key and make a first request:

(openweathermap:configure-api-key (uiop:getenv "OPENWEATHER_API_KEY"))

(let ((current (openweathermap:fetch-current-weather :q "London" :units :metric)))
  (format t "~&City: ~A~%" (openweathermap:ht-get current "name"))
  (format t "Temp (C): ~F~%" (openweathermap:ht-get current "main" "temp")))

All JSON fetch responses are decoded to hash-tables with string keys (via com.inuoe.jzon). Use ht-get to read fields: (openweathermap:ht-get result "name").

Quick Start

Prerequisites

  • SBCL
  • ASDF
  • Quicklisp (for external dependencies such as FiveAM)
  • Node.js (required for OpenAPI linting with Redocly CLI)
    • Recommended: Node >= 20.19.0 (or >= 22.12.0)
  • npm

Install development dependencies

npm install

This installs @redocly/cli for OpenAPI spec validation.

Run unit tests

make unit

Start REPL

Load the main library system:

make repl

Load test systems in REPL:

make repl-tests
make repl-integration

Run integration tests

Set your API key and enable live integration runs:

export OPENWEATHER_API_KEY="your-key"
export OPENWEATHERMAP_RUN_LIVE_TESTS=1
make integration

Per-API integration smoke targets:

make integration-onecall
make integration-current
make integration-forecast
make integration-geocoding
make integration-air-pollution
make integration-maps

If OPENWEATHERMAP_RUN_LIVE_TESTS is not set to 1 (or =true/=yes), integration tests are skipped.

Validate OpenAPI spec

make spec-check

Refresh weather condition mapping artifacts

make update-weather-conditions

Run all examples

make examples

When OPENWEATHER_API_KEY is set, live examples are included. Without it, only offline examples run. Set OPENWEATHERMAP_RUN_LIVE_EXAMPLES=1 to require live example execution.

Clean local cache/compiled artifacts

make clean-cache

Development Workflow

  • Use make check for local quality checks.
  • Use make unit or make check for deterministic non-live checks (CI-safe default).
  • Use make update-weather-conditions to refresh generated weather condition/icon data files from OpenWeather docs.
  • Keep implementation in src/.
  • Keep unit tests in tests/.
  • Keep live API integration tests in integration-tests/.
  • Keep plans in plans/.
  • Keep API specs in spec/ (one file per API family).
  • Update CHANGELOG.md for each session.

API Surface Conventions

  • Module layout by API family:
    • src/apis/current.lisp
    • src/apis/forecast.lisp
    • src/apis/geocoding.lisp
    • src/apis/air-pollution.lisp
    • src/apis/maps.lisp
    • src/client.lisp keeps shared request/retry/decode pipeline.
  • Function naming:
    • build-* functions generate endpoint URLs.
    • make-* functions return request metadata plist (:method :get :url "...").
    • fetch-* functions execute HTTP and return decoded hash-table responses (or raw tile payload for maps).
    • src/apis/onecall.lisp keeps One Call endpoints (fetch-onecall, fetch-timemachine, etc.).
  • Response strategy:
    • JSON objects decode to hash-tables with string keys (via com.inuoe.jzon).
    • JSON arrays decode to vectors.
    • Use ht-get accessors to read response fields.

Error Handling

Common conditions and how to inspect them:

  • missing-api-key-error: API key not configured.
  • invalid-parameters-error: local contract validation failure.
  • api-request-error: non-200 API response.
  • api-network-error: transport/network failure after retries.
  • api-response-parse-error: response decode failure.

Reader accessors available for structured handling:

  • api-request-error-status-code
  • api-request-error-message
  • api-request-error-endpoint
  • invalid-parameters-error-message

Example:

(handler-case
    (openweathermap:fetch-current-weather :q "London" :units :metric)
  (openweathermap:invalid-parameters-error (err)
    (format t "~&Invalid params: ~A~%"
            (openweathermap:invalid-parameters-error-message err)))
  (openweathermap:api-request-error (err)
    (format t "~&API error status=~A endpoint=~A message=~A~%"
            (openweathermap:api-request-error-status-code err)
            (openweathermap:api-request-error-endpoint err)
            (openweathermap:api-request-error-message err))))

Contract Notes

  • JSON response objects are hash-tables with string keys (via com.inuoe.jzon).
    • Example: use (openweathermap:ht-get result "name") to read a top-level field.
    • Nested access: (openweathermap:ht-get result "main" "temp").
    • JSON arrays are vectors; use aref or loop for item across result.
  • Hash-table accessor helpers: ht-get, ht-p, ht-keys, ht-values, ht-map, ht->alist, ht->alist*.
  • Query parameter rest arguments must be even key/value lists.
    • Example invalid form: :units with missing value.
  • One Call family URL builders validate required positional arguments locally:
    • build-onecall-url requires numeric lat/lon
    • build-timemachine-url requires integer dt
    • build-day-summary-url requires YYYY-MM-DD date
  • Current/Forecast fetch helpers decode JSON only; use :mode "json" or omit :mode.
  • Current/Forecast location selectors are exclusive:
    • exactly one selector: lat/lon pair, q, id, or zip.

Public API (Current Draft)

Configure client settings:

(openweathermap:configure-api-key "your-api-key")
(openweathermap:configure-client
 :request-timeout-seconds 10
 :max-retries 2
 :retry-backoff-seconds 1)

Fetch JSON-decoded responses (hash-table form):

(openweathermap:fetch-current-weather :q "London" :units :metric)
(openweathermap:fetch-forecast :q "London" :units :metric :cnt 8)
(openweathermap:fetch-geocoding "London" :limit 5)
(openweathermap:fetch-reverse-geocoding 35.0 139.0 :limit 1)
(openweathermap:fetch-zip-geocoding "94040" :country-code "US")
(openweathermap:fetch-air-pollution 35.0 139.0)
(openweathermap:fetch-air-pollution-forecast 35.0 139.0)
(openweathermap:fetch-air-pollution-history 35.0 139.0 1700000000 1700003600)
(openweathermap:fetch-weather-tile :temp_new 3 4 5 :opacity 0.7)
(openweathermap:fetch-onecall 35.0 139.0 :units :metric :lang "en")
(openweathermap:fetch-timemachine 35.0 139.0 1700000000 :units :metric)
(openweathermap:fetch-day-summary 35.0 139.0 "2026-02-15" :units :metric)
(openweathermap:fetch-overview 35.0 139.0 :date "2026-02-16")

Build request URLs only (no network call):

(openweathermap:build-current-weather-url :lat 35.0 :lon 139.0 :units :metric)
(openweathermap:build-forecast-url :lat 35.0 :lon 139.0 :units :metric :cnt 8)
(openweathermap:build-geocoding-url "London" :limit 5)
(openweathermap:build-reverse-geocoding-url 35.0 139.0 :limit 1)
(openweathermap:build-zip-geocoding-url "94040" :country-code "US")
(openweathermap:build-air-pollution-url 35.0 139.0)
(openweathermap:build-air-pollution-forecast-url 35.0 139.0)
(openweathermap:build-air-pollution-history-url 35.0 139.0 1700000000 1700003600)
(openweathermap:build-weather-tile-url :temp_new 3 4 5 :opacity 0.7)
(openweathermap:build-onecall-url 35.0 139.0 :units :metric)
(openweathermap:build-timemachine-url 35.0 139.0 1700000000)
(openweathermap:build-day-summary-url 35.0 139.0 "2026-02-15")
(openweathermap:build-overview-url 35.0 139.0)

Hash-table accessor helpers:

;; Read response fields
(openweathermap:ht-get result "name")                    ; top-level field
(openweathermap:ht-get result "main" "temp")             ; nested access
(openweathermap:ht-p result)                             ; is it a hash-table?
(openweathermap:ht-keys result)                          ; list of string keys
(openweathermap:ht->alist result)                        ; shallow alist conversion
(openweathermap:ht->alist* result)                       ; recursive alist conversion

Weather condition helpers:

(openweathermap:lookup-weather-condition 500)
(openweathermap:weather-icon-url "10d")                 ; => .../10d@2x.png
(openweathermap:weather-icon-url "10d" :size :1x)       ; => .../10d.png
(openweathermap:resolve-weather-condition
 :id 500 :icon "10n" :description "light rain" :main "Rain")
;; enrich-weather-entry accepts hash-table (from fetch-*) or plist
(openweathermap:enrich-weather-entry weather-entry-hash-table)

Hash-Table Accessor API

FunctionSignatureReturnsNotes
ht-get(hash-table &rest keys)value or NILRead nested value by successive string keys. Returns NIL on missing key.
ht-p(value)booleanTrue when VALUE is a hash-table.
ht-keys(hash-table)listList of string keys in HASH-TABLE.
ht-values(hash-table)listList of values in HASH-TABLE.
ht-map(function hash-table)listApply FUNCTION to each (key value) pair, collecting results.
ht->alist(hash-table)alistShallow conversion to association list.
ht->alist*(hash-table)alistRecursive conversion; nested hash-tables and vectors become nested alists/lists.

Weather Condition Helper API

FunctionSignatureReturnsNotes
lookup-weather-condition(condition-id)plist or NILLooks up canonical condition by numeric ID (e.g. 500).
weather-icon-url(icon-code &key size secure-p)string URLIcon code must be NNd or NNn. size accepts :1x, :2x (default), :4x.
resolve-weather-condition(&key id icon description main)plistMerges payload fields with canonical mapping; includes fallback metadata and :unknown-condition-p.
enrich-weather-entry(weather-entry)plistEnriches one weather object (hash-table from fetch-* or plist) with canonical condition metadata.
enrich-weather-list(weather-list)listMaps enrichment across list or vector of weather objects.

Resolution behavior:

  • Payload :description and :icon are preferred when present.
  • Canonical mapping is used as fallback and for metadata like :icon-day / :icon-night.
  • Unknown condition IDs do not error; result marks :unknown-condition-p as true.

Runnable helper demos:

  • examples/07-weather-condition-helpers.lisp (offline)
  • examples/08-current-weather-enrichment.lisp (live current-weather enrichment)
  • examples/09-live-parser-contract-smoke.lisp (live parser/contract smoke across JSON APIs)
  • See examples/README.org for commands.

Parameter Reference

This client accepts Common Lisp values and serializes them into query params.

Common Optional Params

ParamTypeAllowed / FormatExampleUsed By
:unitssymbol/string:standard, :metric, :imperial:units :metricOne Call, Current, Forecast
:langstringlanguage code:lang "en"One Call, Current, Forecast
:modestringAPI-supported response mode:mode "json"Current, Forecast

Notes:

  • fetch-current-weather and fetch-forecast currently support only JSON-decoded responses; use :mode "json" (or omit :mode).
  • Non-JSON modes (for example xml=/=html) can still be generated via build-* and make-* helpers.

Endpoint Parameters

Endpoint FamilyFunction(s)ParamTypeRequiredNotes / Example
One Callfetch-onecall / build-onecall-urllat, lonnumberyespositional args
One Callfetch-onecall / build-onecall-url:excludestringnocomma-separated, e.g. "minutely,alerts"
Timemachinefetch-timemachinedtintegeryesUNIX timestamp
Day Summaryfetch-day-summarydatestringyesYYYY-MM-DD
Day Summaryfetch-day-summary:tzstringnotimezone offset/name per API docs
Overviewfetch-overview:datestringnoYYYY-MM-DD
Current/Forecastfetch-current-weather, fetch-forecastlocation selectorsee noteyesexactly one selector: :lat/:lon pair, :q, :id, or :zip
Forecastfetch-forecast:cntintegernonumber of forecast entries (1..40)
Geocodingfetch-geocodingquery (positional)non-empty stringyescity/place query
Geocodingfetch-geocoding:limitintegernomax results (1..5)
Reverse Geofetch-reverse-geocodinglat, lonnumberyespositional args
Reverse Geofetch-reverse-geocoding:limitintegernomax results (1..5)
ZIP Geofetch-zip-geocodingzip (positional)non-empty stringyese.g. "94040"
ZIP Geofetch-zip-geocoding:country-codestringnoe.g. "US"
Air Pollutionfetch-air-pollution, ...-forecastlat, lonnumberyespositional args
Air Historyfetch-air-pollution-historystart, endintegeryesUNIX timestamps, start < end=
Mapsfetch-weather-tilelayerkeyword/string/symbolyesone of :clouds_new, :precipitation_new, :pressure_new, :temp_new, :temperature_new, :wind_new
Mapsfetch-weather-tilez, x, ynon-negative integeryestile coordinates
Mapsfetch-weather-tile:opacitynumbernoe.g. 0.7
Mapsfetch-weather-tile:palettestringnoe.g. "warm/cold"

Response Types (Draft)

The table below summarizes successful (200 OK) response shapes based on OpenWeather docs and current project specs.

API Family / EndpointFunction(s)Response TypeTop-level fields (typical)
One Call /data/3.0/onecallfetch-onecallJSON object (hash-table)lat number, lon number, timezone string, timezone_offset integer, current object, minutely array, hourly array, daily array, alerts array
One Call Timemachine /data/3.0/onecall/timemachinefetch-timemachineJSON object (hash-table)lat, lon, timezone, timezone_offset, data array of weather objects
One Call Day Summary /data/3.0/onecall/day_summaryfetch-day-summaryJSON object (hash-table)lat, lon, tz, date (YYYY-MM-DD), units, temperature object, humidity object, cloud_cover object, pressure object, precipitation object, wind object
One Call Overview /data/3.0/onecall/overviewfetch-overviewJSON object (hash-table)lat, lon, tz, date, units, weather_overview string
Current Weather /data/2.5/weatherfetch-current-weatherJSON object (hash-table)coord object, weather array, base string, main object, visibility integer, wind object, clouds object, rain object?, snow object?, dt integer, sys object, timezone integer, id integer, name string, cod integer/string
Forecast /data/2.5/forecastfetch-forecastJSON object (hash-table)cod string, message number, cnt integer, list array (forecast steps), city object
Geocoding Direct /geo/1.0/directfetch-geocodingJSON array (vector)each item typically has name string, local_names object?, lat number, lon number, country string, state string?
Geocoding Reverse /geo/1.0/reversefetch-reverse-geocodingJSON array (vector)same item shape as direct geocoding
Geocoding ZIP /geo/1.0/zipfetch-zip-geocodingJSON object (hash-table)zip string, name string, lat number, lon number, country string
Air Pollution Current /data/2.5/air_pollutionfetch-air-pollutionJSON object (hash-table)coord object, list array; each item includes dt, main (aqi), components (pollutant concentrations)
Air Pollution Forecast /data/2.5/air_pollution/forecastfetch-air-pollution-forecastJSON object (hash-table)same top-level shape as current air pollution
Air Pollution History /data/2.5/air_pollution/historyfetch-air-pollution-historyJSON object (hash-table)same top-level shape as current air pollution
Maps Tile /map/{layer}/{z}/{x}/{y}.pngfetch-weather-tileBinary payloadPNG image bytes/string payload (not JSON)

Notes:

  • JSON objects decode to hash-tables with string keys (use ht-get result "name", not getf with keywords).
  • JSON arrays decode to vectors (use aref or loop for item across result).
  • Optional fields may be absent depending on endpoint, location, and data availability.
  • For One Call endpoint schemas, see spec/onecall.yaml.

Current Project Layout

src/
tests/
integration-tests/
spec/
examples/
plans/