CLI and Python API to fetch air quality index (AQI) data from the Montreal open data portal
Project description
AQI monitoring for the city of Montréal (Québec, Canada)
A Python library and CLI tool to fetch, process, and expose air quality index (AQI) data from the City of Montréal open data platform.
We designed this project to provide:
- scriptable output (JSON by default)
- embeddable components as a Python library
- automation-ready tools (Home Assistant, cron jobs, data pipelines)
Features
- Fetches the latest air quality data from Montréal’s open data portal
- Lists active air quality monitoring stations
- Computes the AQI based on RSQA
- Estimates pollutant concentrations from reported AQI contributions¹
- Exposes structured station and pollutant data via Python models
- Outputs machine-readable JSON from the CLI
- Structured logging with optional debug mode
- Test suite covering core logic, JSON contract, and CLI behavior
¹ Estimated concentrations come from rounded AQI values; treat them as approximations.
Performance & Optimization
- Intelligent caching (TTL-based, max 100 entries)
- Batch requests via
get_stations_aqi()for parallel multi-station queries - Server-side filtering, sorting, and column selection (fields parameter)
- Pagination support with offset parameter for large datasets
- Debug logging includes full API URLs and request parameters
Requirements
- Python 3.11 or newer
requests
Installation
From PyPI (recommended)
pip install montreal-aqi-api
From source
git clone https://github.com/normcyr/montreal-aqi-api.git
cd montreal-aqi-api
python3 -m venv venv
source venv/bin/activate
pip install .
CLI Usage
The CLI always outputs JSON on stdout and writes logs and diagnostics to stderr.
Fetch AQI for a specific station
montreal-aqi --station <station_id>
List available monitoring stations
montreal-aqi --list
Enable debug logging
montreal-aqi --station <station_id> --debug
Print the JSON in a pretty format
montreal-aqi --station <station_id> --pretty
or
montreal-aqi --list --pretty
No arguments
When you provide no arguments, the CLI returns a JSON error payload. We intentionally avoid interactive prompts to keep behavior predictable in automated environments.
Advanced examples
Fetch multiple stations
montreal-aqi --station 1,2,3 --pretty
Suppress output (for scripts)
montreal-aqi --station 80 --quiet
# No output if successful, useful for cron jobs
Verbose logging
montreal-aqi --station 80 --verbose
# Shows detailed logs including API request times and cache status
Combine options
montreal-aqi --list --pretty --verbose
Integrations
Use this library to integrate AQI monitoring with automated systems and workflows.
Home Assistant
Used by the custom Home Assistant integration:
Other Use Cases
- Cron jobs
- Data ingestion pipelines
- Monitoring dashboards
- Research / environmental analysis
JSON Contract — Version 1 (Frozen)
As of v0.4.0, we explicitly versioned and froze the JSON output contract. We formally specify the output format in: docs/json_contract_v1.md.
The official JSON Schema v1 governs the JSON output, and all payloads include:
{
"version": 1,
"type": "..."
}
Error Payload
{
"version": 1,
"type": "error",
"error": {
"code": "NO_DATA",
"message": "No data available for this station"
}
}
Stations List Payload
{
"version": 1,
"type": "stations",
"stations": [
{
"station_id": "3",
"name": "Saint-Jean-Baptiste",
"borough": "Rivière-des-Prairies"
}
]
}
Station AQI Payload
{
"version": 1,
"type": "station",
"station_id": "80",
"date": "2025-08-08",
"hour": 10,
"aqi": 49,
"dominant_pollutant": "PM2.5",
"pollutants": {
"PM2.5": {
"name": "PM2.5",
"aqi": 49,
"concentration": 34.3
},
"O3": {
"name": "O3",
"aqi": 22,
"concentration": 70.4
}
}
}
Python Usage
Fetch AQI for a single station
from montreal_aqi_api.service import get_station_aqi
station = get_station_aqi("80")
if station:
print(station.to_dict())
Fetch AQI for multiple stations (parallel requests)
For better performance when fetching data for multiple stations, use get_stations_aqi():
from montreal_aqi_api.service import get_stations_aqi
# Fetch AQI for multiple stations concurrently (default: 5 workers)
stations = get_stations_aqi(["1", "3", "5", "80"])
for station in stations:
if station:
print(f"Station {station.station_id}: AQI={station.aqi}")
else:
print("Failed to fetch data")
Domain objects (Station, Pollutant) expose explicit serialization helpers:
station = get_station_aqi("80")
data = station.to_dict() # Returns fully serialized JSON-compatible dict
print(data["pollutants"]["PM2.5"]) # Access individual pollutants
Exit codes
The montreal-aqi CLI exits with explicit status codes to make it suitable for scripting, automation, and CI pipelines.
| Exit code | Meaning | Description |
|---|---|---|
0 |
Success | Command executed successfully |
1 |
Generic API error | An unexpected internal API error occurred |
2 |
API unreachable | The CLI could not reach the Montreal Open Data API (network error, timeout, DNS, etc.) |
3 |
Invalid API response | API returned malformed or unexpected JSON payload |
Details
- The CLI reports all errors both via:
- a structured JSON error payload on
stdout - a non-zero process exit code
- a structured JSON error payload on
- This ensures compatibility with:
- shell scripts
- cron jobs
- CI/CD pipelines
- Home Assistant / automation tools
Example
$ montreal-aqi --station 80
{
"version": "1",
"type": "error",
"error": {
"code": "API_UNREACHABLE",
"message": "Montreal open data API is unreachable"
}
}
$ echo $?
2
AQI Methodology
AQI values follow the methodology defined by the Réseau de surveillance de la qualité de l’air (RSQA).
Reference Values
| Pollutant | Full Name | Reference |
|---|---|---|
| SO₂ | Sulfur Dioxide | 500 µg/m³ |
| CO | Carbon Monoxide | 35 mg/m³ |
| O₃ | Ozone | 160 µg/m³ |
| NO₂ | Nitrogen Dioxide | 400 µg/m³ |
| PM2.5 | Particulate Matter | 35 µg/m³ |
Project Status
- JSON contract v1 frozen and validated by tests
- Suitable for automation and integration
- API stability guaranteed within v1
Contributing
Contributions are welcome.
Please ensure that:
- tests pass (
pytest) - the JSON contract remains backward compatible
- tests cover any change affecting output
- lint and format code with
ruff(runruff format --check .andruff check .)
Open an issue before proposing breaking changes.
Data Source
Data retrieved from the Ville de Montréal Open Data Portal.
License
We license this project under the MIT License.
Project details
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 montreal_aqi_api-0.7.1.tar.gz.
File metadata
- Download URL: montreal_aqi_api-0.7.1.tar.gz
- Upload date:
- Size: 26.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a5fd20c8c1efef9776380177c802701e935a53479ac354c6a0af82bd278f04d
|
|
| MD5 |
8f48b8b26148a8aa95b3b9015e0908f1
|
|
| BLAKE2b-256 |
41e33fd2b235dda61e12f94c699368b2de103b90d5f565b370cf154d3d659a7a
|
File details
Details for the file montreal_aqi_api-0.7.1-py3-none-any.whl.
File metadata
- Download URL: montreal_aqi_api-0.7.1-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec6a9bfa096477cc962078e8f0ab9764bec278fc967340e7d5a039db71aab5d1
|
|
| MD5 |
66136006c51e01975c3a3b7c13c9e046
|
|
| BLAKE2b-256 |
38f4d1fc7dac2c9435394026317622da1295db4689673dd93c39eafdc6505c90
|