Async Python client for the PurpleAir API, with the organization endpoint and typed error codes.
Project description
aiopurpleair
Async Python client library for the PurpleAir air-quality API, with the organization endpoint and typed error codes.
Build and Distribution
- Source Code: GitHub - Source code, issues, discussions, and CI/CD pipelines.
- Versioned Releases: GitHub Releases - Version tagged source code and build artifacts.
- PyPI Packages: PyPI - Python wheel + sdist published to PyPI.org as
ptr727-aiopurpleair.
Build Status
Releases
Release Notes
See HISTORY.md for the release notes ledger.
Getting Started
Get started with aiopurpleair in two easy steps:
-
Add aiopurpleair to your project:
# Add the package to your project (import name stays `aiopurpleair`) pip install ptr727-aiopurpleair
-
Write some code:
import asyncio from aiopurpleair import API async def main() -> None: """Check an API key and fetch sensors.""" api = API("<API_KEY>") keys = await api.async_check_api_key() sensors = await api.sensors.async_get_sensors(["name", "pm2.5"]) organization = await api.organizations.async_get_organization() asyncio.run(main())
See Usage for detailed usage instructions.
Table of Contents
Features
- Async client for the PurpleAir API, covering the sensors and keys endpoints.
GET /v1/organizationendpoint exposing remaining API points and consumption rate.- Typed exception hierarchy mapped from the API's documented error codes.
- Timezone-aware UTC datetimes and typed Pydantic response models with a
py.typedmarker. - Modern packaging: hatchling, uv, automatic versioning, OIDC-published releases, and 100% test coverage.
Usage
In-depth documentation on the API is available from PurpleAir. Unless otherwise noted, aiopurpleair follows the API as closely as possible.
Checking an API Key
import asyncio
from aiopurpleair import API
async def main() -> None:
"""Check whether an API key is valid and what properties it has."""
api = API("<API_KEY>")
response = await api.async_check_api_key()
# >>> response.api_key_type == ApiKeyType.READ
# >>> response.api_version == "V1.0.11-0.0.41"
asyncio.run(main())
Getting Sensors
import asyncio
from aiopurpleair import API
async def main() -> None:
"""Fetch sensor data for the requested fields."""
api = API("<API_KEY>")
response = await api.sensors.async_get_sensors(["name", "pm2.5"])
# >>> response.data == {131075: SensorModel(...), 131079: SensorModel(...)}
asyncio.run(main())
Private sensors require their per-sensor read key: pass read_key= to async_get_sensor, or read_keys=[...] to async_get_sensors. Use async_get_nearby_sensors(fields, latitude, longitude, distance) for a distance-sorted search, and get_map_url(sensor_index) for a map link.
Getting the Organization
The organization endpoint reports the account's remaining API points and consumption rate, useful for surfacing a low-points warning before queries start failing:
import asyncio
from aiopurpleair import API
async def main() -> None:
"""Fetch the organization associated with the API key."""
api = API("<API_KEY>")
response = await api.organizations.async_get_organization()
# >>> response.remaining_points == 500000
# >>> response.consumption_rate == 1234.5
# >>> response.organization_id == "..."
# >>> response.organization_name == "..."
asyncio.run(main())
Error Handling
Each documented PurpleAir API error code maps to a specific exception subclass, so callers can catch a precise condition instead of pattern-matching on str(err). Every subclass derives from PurpleAirError:
import asyncio
from aiopurpleair import API
from aiopurpleair.errors import InvalidApiKeyError, RateLimitExceededError
async def main() -> None:
"""Handle specific PurpleAir error conditions."""
api = API("<API_KEY>")
try:
await api.sensors.async_get_sensors(["name"])
except InvalidApiKeyError:
... # the API key is missing or invalid
except RateLimitExceededError:
... # back off and retry later
asyncio.run(main())
All error codes and semantics are verified against the official PurpleAir API documentation.
Connection Pooling
By default a new connection is created per coroutine. Pass an existing aiohttp ClientSession for connection pooling:
import asyncio
from aiohttp import ClientSession
from aiopurpleair import API
async def main() -> None:
"""Reuse a session across calls."""
async with ClientSession() as session:
api = API("<API_KEY>", session=session)
...
asyncio.run(main())
Installation
Project integration:
# Add the package to your project
pip install ptr727-aiopurpleair
# Import the library (the import name stays `aiopurpleair`)
import aiopurpleair
The distribution name is ptr727-aiopurpleair (distinct from the canonical aiopurpleair on PyPI); the import path is unchanged. aiopurpleair supports Python 3.13 and 3.14, and depends on aiohttp, pydantic, yarl, and certifi.
Questions or Issues
General questions:
- Use the Discussions forum for general questions.
Bug reports:
- Ask in the Discussions forum if you are not sure if it is a bug.
- Check the existing Issues tracker for known problems.
- If the issue is unique and a bug, file it in Issues, and include all pertinent steps to reproduce the issue.
Build Artifacts
Build process and artifacts:
- Package: a Python wheel + sdist (
ptr727-aiopurpleair), built with the hatchling backend on a src-layout (src/aiopurpleair/) and managed with uv. - Versioning: automatic via Nerdbank.GitVersioning from
version.json(1.0base) plus git height;mainbuilds a clean stableX.Y.Z,developaX.Y.Z.dev0prerelease. There is no manual tagging. - Publishing: releases publish to PyPI over OIDC Trusted Publishing (no stored API token). A shipped-path push to
main(stable) ordevelop(prerelease), or a manual dispatch, cuts a GitHub Release and uploads the wheel + sdist to PyPI. SeeWORKFLOW.mdfor the complete CI/CD contract.
API Reference
PurpleAir does not publish an OpenAPI/Swagger spec. This repo reconstructs one at docs/purpleair-openapi.yaml from PurpleAir's apiDoc-generated docs (which serve machine-readable api_data.js), using scripts/generate_openapi.py. The library's endpoint, field, and error-code coverage is validated against this spec.
Regenerate it after an upstream API change:
# Live-fetch https://api.purpleair.com/api_data.js, rebuild and validate the spec
uv run --with pyyaml --with openapi-spec-validator python scripts/generate_openapi.py
The generator takes the API version from the docs' changelog (the apiDoc build-metadata version lags behind), validates the result, and writes docs/purpleair-openapi.yaml. A non-empty diff means the upstream API changed. See AGENTS.md for how the code is validated against the spec.
Contributing
- Branching workflow:
- The repo uses a two-branch model with ruleset-enforced merge methods.
- Feature branch ->
developvia squash merge (develop is kept linear). develop->mainvia merge commit (preserves develop's commit list on main as the second parent of each release commit).- Dependabot targets
mainanddevelopin parallel via separate PRs and auto-merges every tier once the required check passes. - See
WORKFLOW.mdandAGENTS.mdfor complete details.
- Code style:
- See
CODESTYLE.mdand.editorconfigfor Python code style rules. Everything runs throughuv run(ruff,mypy,pyright,pytestwith 100% coverage and syrupy snapshots).
- See
- Repository setup:
- See
repo-config/README.mdfor repo configuration details.
- See
Origin
aiopurpleair is an independent, MIT-licensed continuation of the bachya/aiopurpleair PurpleAir API client. Its distinguishing capabilities originated in two upstream contributions that were abandoned after the upstream maintainers became unresponsive:
- A pull request against bachya/aiopurpleair adding the organization endpoint and typed error codes, which was not merged.
- A PurpleAir integration proposed for Home Assistant core as home-assistant/core#140901, now maintained separately as the
homeassistant-purpleairHACS custom integration, this library's primary consumer.
The import package name is aiopurpleair; the distribution name ptr727-aiopurpleair keeps it distinct from the canonical aiopurpleair on PyPI. The original MIT copyright is retained alongside the current maintainer's in LICENSE and NOTICE.
License
Licensed under the MIT License
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 ptr727_aiopurpleair-1.0.0.dev0.tar.gz.
File metadata
- Download URL: ptr727_aiopurpleair-1.0.0.dev0.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0bf7ad2ed2e2fa41438d11175da9cf4909eb06d708f9eaa156c4ed6ae82ca8bf
|
|
| MD5 |
d58f67bd2cc86daecfa04af4f7276393
|
|
| BLAKE2b-256 |
8e93d77411f7aa095f0509ec8fb44d0ee260398413a9b4a826d6f3b198dfba0d
|
Provenance
The following attestation bundles were made for ptr727_aiopurpleair-1.0.0.dev0.tar.gz:
Publisher:
publish-release.yml on ptr727/aiopurpleair
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ptr727_aiopurpleair-1.0.0.dev0.tar.gz -
Subject digest:
0bf7ad2ed2e2fa41438d11175da9cf4909eb06d708f9eaa156c4ed6ae82ca8bf - Sigstore transparency entry: 2064059002
- Sigstore integration time:
-
Permalink:
ptr727/aiopurpleair@83e1b982e67aea3017ee1cd8b4974a66759f0644 -
Branch / Tag:
refs/heads/develop - Owner: https://github.com/ptr727
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@83e1b982e67aea3017ee1cd8b4974a66759f0644 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ptr727_aiopurpleair-1.0.0.dev0-py3-none-any.whl.
File metadata
- Download URL: ptr727_aiopurpleair-1.0.0.dev0-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75b962f66d5c7ca07fb4ba8cfcdd7cc69fc3b80b28659078db92076c8f0856e6
|
|
| MD5 |
4d9c030d0b3441082b5ffe600186f5ad
|
|
| BLAKE2b-256 |
f0b0aa7d129dfad1cbb1d7c8548224689e2e6c7eecdd0a20fd567b019a27d183
|
Provenance
The following attestation bundles were made for ptr727_aiopurpleair-1.0.0.dev0-py3-none-any.whl:
Publisher:
publish-release.yml on ptr727/aiopurpleair
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ptr727_aiopurpleair-1.0.0.dev0-py3-none-any.whl -
Subject digest:
75b962f66d5c7ca07fb4ba8cfcdd7cc69fc3b80b28659078db92076c8f0856e6 - Sigstore transparency entry: 2064059021
- Sigstore integration time:
-
Permalink:
ptr727/aiopurpleair@83e1b982e67aea3017ee1cd8b4974a66759f0644 -
Branch / Tag:
refs/heads/develop - Owner: https://github.com/ptr727
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@83e1b982e67aea3017ee1cd8b4974a66759f0644 -
Trigger Event:
push
-
Statement type: