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.
Metadata
Release files for montreal-aqi-api 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| montreal_aqi_api-0.8.0.tar.gz | 26.8 kB | Without provenance | Details for montreal_aqi_api-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| montreal_aqi_api-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for montreal_aqi_api-0.8.0-py3-none-any.whl |
Total release size: 43.6 kB
Release files / montreal_aqi_api-0.8.0.tar.gz
| Download URL | montreal_aqi_api-0.8.0.tar.gz |
|---|---|
| Size | 26.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
79ea245db0e62ef1743285e690e7c44f373ca8f6c85d0f4d6abcfb6359522379
|
|
BLAKE2b-256 checksum How to use checksums |
c097ebb662c95ad5343d12c0f3575e80ec05792093400fc45c6d9de4ef22d9f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Release files / montreal_aqi_api-0.8.0-py3-none-any.whl
| Download URL | montreal_aqi_api-0.8.0-py3-none-any.whl |
|---|---|
| Size | 16.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
41326b6de8ade023884c8f3a7fdbad4dbf5cccc8e8c77a6544289081567c972a
|
|
BLAKE2b-256 checksum How to use checksums |
074c87a64aef575c1895f4ef8fc06d7ae717b9637cf54124a055fb967fea3449
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Security for montreal-aqi-api 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.