python-aqi 0.5.1__tar.gz → 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. python_aqi-1.0.0/.gitignore +15 -0
  2. python_aqi-1.0.0/LICENSE +12 -0
  3. python_aqi-1.0.0/PKG-INFO +291 -0
  4. python_aqi-1.0.0/README.rst +267 -0
  5. python_aqi-1.0.0/aqi/__init__.py +248 -0
  6. python_aqi-1.0.0/aqi/algos/__init__.py +62 -0
  7. python_aqi-1.0.0/aqi/algos/base.py +242 -0
  8. python_aqi-1.0.0/aqi/algos/epa.py +207 -0
  9. python_aqi-1.0.0/aqi/algos/mep.py +128 -0
  10. python_aqi-1.0.0/aqi/constants.py +23 -0
  11. python_aqi-1.0.0/aqi/exceptions.py +32 -0
  12. python_aqi-1.0.0/docs/Makefile +153 -0
  13. python_aqi-1.0.0/docs/_ext/aqi_compare.py +184 -0
  14. python_aqi-1.0.0/docs/_ext/aqi_tables.py +80 -0
  15. python_aqi-1.0.0/docs/_static/aqi_compare.css +14 -0
  16. python_aqi-1.0.0/docs/_static/aqi_compare.js +159 -0
  17. python_aqi-1.0.0/docs/_static/uPlot.LICENSE +21 -0
  18. python_aqi-1.0.0/docs/_static/uPlot.iife.min.js +2 -0
  19. python_aqi-1.0.0/docs/_static/uPlot.min.css +1 -0
  20. python_aqi-1.0.0/docs/api.rst +31 -0
  21. python_aqi-1.0.0/docs/changelog.rst +81 -0
  22. python_aqi-1.0.0/docs/compare.rst +20 -0
  23. python_aqi-1.0.0/docs/concepts.rst +96 -0
  24. python_aqi-1.0.0/docs/conf.py +249 -0
  25. python_aqi-1.0.0/docs/index.rst +25 -0
  26. python_aqi-1.0.0/docs/standards/hj633-2026-table.rst +65 -0
  27. python_aqi-1.0.0/docs/standards/index.rst +195 -0
  28. python_aqi-1.0.0/docs/versions.rst +196 -0
  29. python_aqi-1.0.0/pyproject.toml +61 -0
  30. python_aqi-1.0.0/tests/__init__.py +0 -0
  31. python_aqi-1.0.0/tests/data/generate_legacy.py +49 -0
  32. python_aqi-1.0.0/tests/data/legacy_0_6_1.json +1 -0
  33. python_aqi-1.0.0/tests/test_algos.py +18 -0
  34. python_aqi-1.0.0/tests/test_aqi.py +29 -0
  35. python_aqi-1.0.0/tests/test_base.py +20 -0
  36. python_aqi-1.0.0/tests/test_docs_compare.py +64 -0
  37. python_aqi-1.0.0/tests/test_docs_tables.py +39 -0
  38. python_aqi-1.0.0/tests/test_epa.py +63 -0
  39. python_aqi-1.0.0/tests/test_epa_versions.py +214 -0
  40. python_aqi-1.0.0/tests/test_legacy.py +51 -0
  41. python_aqi-1.0.0/tests/test_mep_versions.py +248 -0
  42. python_aqi-1.0.0/tests/test_out_of_range.py +198 -0
  43. python_aqi-1.0.0/tests/test_versions.py +178 -0
  44. home/hr/.virtualenvs/python-aqi/bin/aqi +0 -10
  45. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/__init__.py +0 -107
  46. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/__init__.pyc +0 -0
  47. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/__init__.py +0 -38
  48. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/__init__.pyc +0 -0
  49. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/base.py +0 -85
  50. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/base.pyc +0 -0
  51. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/epa.py +0 -106
  52. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/epa.pyc +0 -0
  53. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/mep.py +0 -137
  54. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/algos/mep.pyc +0 -0
  55. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/constants.py +0 -18
  56. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/aqi/constants.pyc +0 -0
  57. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/PKG-INFO +0 -154
  58. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/SOURCES.txt +0 -19
  59. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/dependency_links.txt +0 -1
  60. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/entry_points.txt +0 -3
  61. home/hr/.virtualenvs/python-aqi/lib/python2.7/site-packages/python_aqi-0.5.1-py2.7.egg-info/top_level.txt +0 -1
@@ -0,0 +1,15 @@
1
+ build
2
+ dist
3
+ python_aqi.egg-info
4
+ dev_aqi.py
5
+
6
+ docs/_build/
7
+
8
+ .tox
9
+ .envrc
10
+
11
+ # editors and bytecode
12
+ *.sw[po]
13
+ __pycache__/
14
+ *.pyc
15
+ .venv/
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2014, Stefan "hr" Berder
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-aqi
3
+ Version: 1.0.0
4
+ Summary: A library to convert between AQI value and pollutant concentration (µg/m³ or ppm)
5
+ Project-URL: Homepage, https://github.com/hrbonz/python-aqi
6
+ Project-URL: Documentation, https://python-aqi.readthedocs.io/
7
+ Author-email: "Stefan \"hr\" Berder" <hr@bonz.org>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: EPA,MEP,air quality,pm2.5
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Healthcare Industry
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/x-rst
24
+
25
+ ###########################
26
+ python-aqi - AQI conversion
27
+ ###########################
28
+
29
+ A library to convert between AQI value and pollutant concentration
30
+ (µg/m³ or ppm) using the following algorithms:
31
+
32
+ * United States Environmental Protection Agency (EPA)
33
+ * China Ministry of Environmental Protection (MEP)
34
+
35
+ The versions of each standard are kept, see `the documentation
36
+ <https://python-aqi.readthedocs.io/>`_ for how an AQI is computed, the
37
+ standards and versions, and the API reference.
38
+
39
+ .. image:: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml/badge.svg?branch=master
40
+ :target: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml
41
+ :alt: Testing Status
42
+
43
+ .. image:: https://readthedocs.org/projects/python-aqi/badge/?version=latest
44
+ :target: https://readthedocs.org/projects/python-aqi/?badge=latest
45
+ :alt: Documentation Status
46
+
47
+ .. image:: http://img.shields.io/badge/license-BSD%203--Clause-blue.svg
48
+ :target: http://opensource.org/licenses/BSD-3-Clause
49
+ :alt: license BSD 3-Clause
50
+
51
+
52
+ Install
53
+ =======
54
+
55
+ With pip::
56
+
57
+ $ pip install python-aqi
58
+
59
+ With `uv <https://docs.astral.sh/uv/>`_, to add it to a project::
60
+
61
+ $ uv add python-aqi
62
+
63
+ or to install the ``aqi`` command line tool::
64
+
65
+ $ uv tool install python-aqi
66
+
67
+
68
+ Usage
69
+ =====
70
+
71
+ Library
72
+ -------
73
+
74
+ Convert a pollutant to its IAQI (Intermediate Air Quality Index)::
75
+
76
+ import aqi
77
+ myaqi = aqi.to_iaqi(aqi.POLLUTANT_PM25, '12', algo=aqi.ALGO_EPA)
78
+
79
+ Get an AQI out of several pollutant concentrations, default algorithm is EPA::
80
+
81
+ import aqi
82
+ myaqi = aqi.to_aqi([
83
+ (aqi.POLLUTANT_PM25, '12'),
84
+ (aqi.POLLUTANT_PM10, '24'),
85
+ (aqi.POLLUTANT_O3_8H, '0.087')
86
+ ])
87
+
88
+ The algorithm is a standard and its version: ``'epa:2012'``, or ``'epa'`` for
89
+ the latest version of the family (``aqi.ALGO_EPA``, ``aqi.ALGO_EPA_2012`` and
90
+ the matching ``MEP`` constants are available). Old versions stay selectable, the
91
+ default is always the latest. ``aqi.available_algos()`` lists them all::
92
+
93
+ aqi.available_algos()
94
+ # ['epa:2012', 'epa:2015', 'epa:2024', 'mep:2012', 'mep:2026']
95
+
96
+ The EPA versions are named after the revision that changed the table:
97
+
98
+ * ``epa:2012``: what python-aqi computed until 0.6, PM2.5 Good up to 12.0
99
+ µg/m³ and the 1997 ozone 8 hour breakpoints
100
+ * ``epa:2015``: new ozone 8 hour breakpoints (0.054, 0.070, 0.085, 0.105, 0.200
101
+ ppm), 1 hour ozone only above 0.125 ppm
102
+ * ``epa:2024``: new PM2.5 breakpoints (Good up to 9.0 µg/m³) and a single
103
+ Hazardous category, 301 to 500, for every pollutant. This is the default.
104
+
105
+ The MEP (China) versions follow the standards. The name ``MEP`` stands for the
106
+ Ministry of Environmental Protection, which published HJ 633-2012. The ministry
107
+ was renamed Ministry of Ecology and Environment (MEE) in 2018 and publishes
108
+ HJ 633-2026 under that name, the library keeps ``MEP`` for all versions so that
109
+ the name used by existing code does not change:
110
+
111
+ * ``mep:2012``: HJ 633-2012, IAQI interpolated between the limits of the
112
+ table and IAQI and AQI rounded up, concentrations used as given
113
+ * ``mep:2026``: HJ 633-2026, effective 1 March 2026. The limits of IAQI 100
114
+ change for PM10 (150 to 120 µg/m³) and PM2.5 (75 to 60 µg/m³), and the
115
+ calculation follows the standard: IAQI interpolated between the limits of
116
+ the table, IAQI and AQI rounded up, concentrations rounded to integers (CO to
117
+ one decimal). This is the default for MEP.
118
+
119
+ ``mep:2012`` used to be laid out like the EPA tables and now follows
120
+ HJ 633-2012 (``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP_2012)``
121
+ is 82, it was 81). The default changed with the 2024 and 2026 versions: the same PM2.5 concentration can
122
+ now give a different AQI, ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '12.1')`` is 57,
123
+ it was 51, and ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP)`` is
124
+ 100, it was 81. Use ``algo=aqi.ALGO_EPA_2012`` to keep the old EPA results.
125
+
126
+ 1 hour ozone has no value under 0.125 ppm in the EPA table. ``to_aqi`` ignores
127
+ it when other pollutants are given (the standard says to disregard it), for
128
+ ``to_iaqi`` and ``to_cc`` it raises ``aqi.NotApplicableError``, an
129
+ ``OutOfRangeError``.
130
+
131
+ Convert an IAQI to its pollutant concentration::
132
+
133
+ import aqi
134
+ mycc = aqi.to_cc(aqi.POLLUTANT_PM25, '22', algo=aqi.ALGO_EPA)
135
+
136
+ Out of range values
137
+ ~~~~~~~~~~~~~~~~~~~
138
+
139
+ A concentration (or an AQI) that is not covered by the algorithm raises
140
+ ``aqi.OutOfRangeError``, a subclass of ``ValueError`` that carries ``elem``,
141
+ ``value``, ``low`` and ``high``. Values are never capped: a capped value would
142
+ hide how far above the scale a measure is, so what to do with it is up to the
143
+ calling code::
144
+
145
+ try:
146
+ myaqi = aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')])
147
+ except aqi.OutOfRangeError as err:
148
+ print(err.elem, err.value, err.high)
149
+
150
+ The EPA calls AQI values above 500 "Beyond the AQI" and explains how to
151
+ compute them: use the regular formula with the Hazardous breakpoints. This is
152
+ available on request with ``extrapolate=True``::
153
+
154
+ aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')], extrapolate=True) # 1445
155
+
156
+ This only applies to the EPA algorithm and to pollutants whose table covers
157
+ the whole scale. 8 hour ozone, for example, stops at 300 because the EPA uses
158
+ 1 hour ozone above that. The MEP standard has no such method, so ``extrapolate=True``
159
+ still raises with it. HJ 633-2026 does fix the IAQI of two pollutants above
160
+ their table: SO2 1 hour above 800 µg/m³ is 200 and O3 8 hour above 800 µg/m³
161
+ is 300 (HJ 633-2012 said they are not computed: ``mep:2012`` raises
162
+ ``aqi.NotApplicableError`` and ``to_aqi`` leaves them out).
163
+
164
+
165
+ Command line
166
+ ------------
167
+
168
+ List supported algorithms and pollutants::
169
+
170
+ $ aqi -l
171
+ epa:2012: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
172
+ epa:2015: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
173
+ epa:2024 (latest): o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
174
+ mep:2012: so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
175
+ mep:2026 (latest): so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
176
+
177
+ Pick the version of a standard with ``family:version``, ``epa`` alone means the
178
+ latest version, the module names (``aqi.algos.epa``) still work and also mean
179
+ the latest::
180
+
181
+ $ aqi epa:2012 pm25:12
182
+ 50
183
+
184
+ Convert PM2.5 to IAQI using EPA algorithm::
185
+
186
+ $ aqi aqi.algos.epa pm25:12
187
+ 56
188
+
189
+ Convert PM2.5 to IAQI using EPA algorithm (full length)::
190
+
191
+ $ aqi -c aqi aqi.algos.epa pm25:12
192
+ 56
193
+
194
+ Convert pollutants concentrations to AQI using EPA algorithm::
195
+
196
+ $ aqi aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
197
+ 122
198
+
199
+ Convert pollutants concentrations to AQI using EPA algorithm, display IAQIs::
200
+
201
+ $ aqi -v aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
202
+ pm25:114 o3_8h:122 co_8h:90
203
+ 122
204
+
205
+ Convert PM2.5 IAQI to concentration using EPA algorithm::
206
+
207
+ $ aqi -c cc aqi.algos.epa pm25:39
208
+ 7.0
209
+
210
+ Out of range values are an error, the message goes to stderr and the exit
211
+ code is 3::
212
+
213
+ $ aqi aqi.algos.epa pm25:800
214
+ Out of range: pm25: 800.0 is not covered by the algorithm (valid range is 0.0 to 325.4)
215
+
216
+ ``-e`` computes EPA values beyond the scale ("Beyond the AQI")::
217
+
218
+ $ aqi -e aqi.algos.epa pm25:800
219
+ 1445
220
+
221
+
222
+ Development
223
+ ===========
224
+
225
+ Set up the development environment (ruff, pre-commit, hatch, coverage)::
226
+
227
+ $ uv sync
228
+ $ uv run pre-commit install
229
+
230
+ Code is linted and formatted with `ruff <https://docs.astral.sh/ruff/>`_
231
+ through `pre-commit <https://pre-commit.com/>`_, the checks run on each
232
+ commit. To run them on all files::
233
+
234
+ $ uv run pre-commit run --all-files
235
+
236
+ Test the package::
237
+
238
+ $ uv run python -m unittest discover
239
+
240
+ The same checks and the tests on Python 3.9 to 3.14 run on GitHub Actions
241
+ (``.github/workflows/ci.yml``) for each push and pull request.
242
+
243
+
244
+ Release
245
+ =======
246
+
247
+ A release is made from ``master`` with a clean tree, after adding a
248
+ ``X.Y.Z (unreleased)`` section to ``docs/changelog.rst``::
249
+
250
+ $ make release SEGMENT=major # or minor, patch (the default)
251
+
252
+ It runs, in this order:
253
+
254
+ ``make bump``
255
+ runs the tests, bumps the version with ``hatch version``, refreshes
256
+ ``uv.lock``, dates the changelog section, commits ``release: bump to
257
+ X.Y.Z`` and tags ``X.Y.Z`` (no ``v``).
258
+ ``make publish``
259
+ builds the tagged commit and uploads it to PyPI with ``uv publish``. The
260
+ token is a PyPI API token in the ``UV_PUBLISH_TOKEN`` environment
261
+ variable, it is not stored in the repository.
262
+ ``make push``
263
+ pushes the commit and the tag to ``origin`` (``REMOTE=...`` to change it)
264
+ once the upload worked.
265
+
266
+
267
+ Project
268
+ =======
269
+
270
+ * `Source code on github <https://github.com/hrbonz/python-aqi>`_
271
+ * `Documentation on readthedocs <https://python-aqi.readthedocs.io/>`_
272
+ * `Package on pypi <https://pypi.python.org/pypi/python-aqi>`_
273
+
274
+
275
+ Resources
276
+ =========
277
+
278
+ * EPA AQI: Technical Assistance Document for the Reporting of Daily Air
279
+ Quality – the Air Quality Index (AQI) December 2013) found at http://www.epa.gov/airnow/aqi-technical-assistance-document-dec2013.pdf
280
+ * National Ambient Air Quality Standards for Particulate Matter found at http://www.gpo.gov/fdsys/pkg/FR-2013-01-15/pdf/2012-30946.pdf
281
+ * MEP AQI:
282
+
283
+ * GB3095—2012 (2012/02/29) found at http://www.mep.gov.cn/gkml/hbb/bwj/201203/t20120302_224147.htm
284
+ * HJ633-2012 (2012/02/29) found at https://www.mee.gov.cn/ywgz/fgbz/bz/bzwb/jcffbz/201203/W020120410332725219541.pdf
285
+
286
+
287
+ License
288
+ =======
289
+
290
+ python-aqi is published under a BSD 3-clause license, see the LICENSE file
291
+ distributed with the project.
@@ -0,0 +1,267 @@
1
+ ###########################
2
+ python-aqi - AQI conversion
3
+ ###########################
4
+
5
+ A library to convert between AQI value and pollutant concentration
6
+ (µg/m³ or ppm) using the following algorithms:
7
+
8
+ * United States Environmental Protection Agency (EPA)
9
+ * China Ministry of Environmental Protection (MEP)
10
+
11
+ The versions of each standard are kept, see `the documentation
12
+ <https://python-aqi.readthedocs.io/>`_ for how an AQI is computed, the
13
+ standards and versions, and the API reference.
14
+
15
+ .. image:: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml/badge.svg?branch=master
16
+ :target: https://github.com/hrbonz/python-aqi/actions/workflows/ci.yml
17
+ :alt: Testing Status
18
+
19
+ .. image:: https://readthedocs.org/projects/python-aqi/badge/?version=latest
20
+ :target: https://readthedocs.org/projects/python-aqi/?badge=latest
21
+ :alt: Documentation Status
22
+
23
+ .. image:: http://img.shields.io/badge/license-BSD%203--Clause-blue.svg
24
+ :target: http://opensource.org/licenses/BSD-3-Clause
25
+ :alt: license BSD 3-Clause
26
+
27
+
28
+ Install
29
+ =======
30
+
31
+ With pip::
32
+
33
+ $ pip install python-aqi
34
+
35
+ With `uv <https://docs.astral.sh/uv/>`_, to add it to a project::
36
+
37
+ $ uv add python-aqi
38
+
39
+ or to install the ``aqi`` command line tool::
40
+
41
+ $ uv tool install python-aqi
42
+
43
+
44
+ Usage
45
+ =====
46
+
47
+ Library
48
+ -------
49
+
50
+ Convert a pollutant to its IAQI (Intermediate Air Quality Index)::
51
+
52
+ import aqi
53
+ myaqi = aqi.to_iaqi(aqi.POLLUTANT_PM25, '12', algo=aqi.ALGO_EPA)
54
+
55
+ Get an AQI out of several pollutant concentrations, default algorithm is EPA::
56
+
57
+ import aqi
58
+ myaqi = aqi.to_aqi([
59
+ (aqi.POLLUTANT_PM25, '12'),
60
+ (aqi.POLLUTANT_PM10, '24'),
61
+ (aqi.POLLUTANT_O3_8H, '0.087')
62
+ ])
63
+
64
+ The algorithm is a standard and its version: ``'epa:2012'``, or ``'epa'`` for
65
+ the latest version of the family (``aqi.ALGO_EPA``, ``aqi.ALGO_EPA_2012`` and
66
+ the matching ``MEP`` constants are available). Old versions stay selectable, the
67
+ default is always the latest. ``aqi.available_algos()`` lists them all::
68
+
69
+ aqi.available_algos()
70
+ # ['epa:2012', 'epa:2015', 'epa:2024', 'mep:2012', 'mep:2026']
71
+
72
+ The EPA versions are named after the revision that changed the table:
73
+
74
+ * ``epa:2012``: what python-aqi computed until 0.6, PM2.5 Good up to 12.0
75
+ µg/m³ and the 1997 ozone 8 hour breakpoints
76
+ * ``epa:2015``: new ozone 8 hour breakpoints (0.054, 0.070, 0.085, 0.105, 0.200
77
+ ppm), 1 hour ozone only above 0.125 ppm
78
+ * ``epa:2024``: new PM2.5 breakpoints (Good up to 9.0 µg/m³) and a single
79
+ Hazardous category, 301 to 500, for every pollutant. This is the default.
80
+
81
+ The MEP (China) versions follow the standards. The name ``MEP`` stands for the
82
+ Ministry of Environmental Protection, which published HJ 633-2012. The ministry
83
+ was renamed Ministry of Ecology and Environment (MEE) in 2018 and publishes
84
+ HJ 633-2026 under that name, the library keeps ``MEP`` for all versions so that
85
+ the name used by existing code does not change:
86
+
87
+ * ``mep:2012``: HJ 633-2012, IAQI interpolated between the limits of the
88
+ table and IAQI and AQI rounded up, concentrations used as given
89
+ * ``mep:2026``: HJ 633-2026, effective 1 March 2026. The limits of IAQI 100
90
+ change for PM10 (150 to 120 µg/m³) and PM2.5 (75 to 60 µg/m³), and the
91
+ calculation follows the standard: IAQI interpolated between the limits of
92
+ the table, IAQI and AQI rounded up, concentrations rounded to integers (CO to
93
+ one decimal). This is the default for MEP.
94
+
95
+ ``mep:2012`` used to be laid out like the EPA tables and now follows
96
+ HJ 633-2012 (``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP_2012)``
97
+ is 82, it was 81). The default changed with the 2024 and 2026 versions: the same PM2.5 concentration can
98
+ now give a different AQI, ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '12.1')`` is 57,
99
+ it was 51, and ``aqi.to_iaqi(aqi.POLLUTANT_PM25, '60', algo=aqi.ALGO_MEP)`` is
100
+ 100, it was 81. Use ``algo=aqi.ALGO_EPA_2012`` to keep the old EPA results.
101
+
102
+ 1 hour ozone has no value under 0.125 ppm in the EPA table. ``to_aqi`` ignores
103
+ it when other pollutants are given (the standard says to disregard it), for
104
+ ``to_iaqi`` and ``to_cc`` it raises ``aqi.NotApplicableError``, an
105
+ ``OutOfRangeError``.
106
+
107
+ Convert an IAQI to its pollutant concentration::
108
+
109
+ import aqi
110
+ mycc = aqi.to_cc(aqi.POLLUTANT_PM25, '22', algo=aqi.ALGO_EPA)
111
+
112
+ Out of range values
113
+ ~~~~~~~~~~~~~~~~~~~
114
+
115
+ A concentration (or an AQI) that is not covered by the algorithm raises
116
+ ``aqi.OutOfRangeError``, a subclass of ``ValueError`` that carries ``elem``,
117
+ ``value``, ``low`` and ``high``. Values are never capped: a capped value would
118
+ hide how far above the scale a measure is, so what to do with it is up to the
119
+ calling code::
120
+
121
+ try:
122
+ myaqi = aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')])
123
+ except aqi.OutOfRangeError as err:
124
+ print(err.elem, err.value, err.high)
125
+
126
+ The EPA calls AQI values above 500 "Beyond the AQI" and explains how to
127
+ compute them: use the regular formula with the Hazardous breakpoints. This is
128
+ available on request with ``extrapolate=True``::
129
+
130
+ aqi.to_aqi([(aqi.POLLUTANT_PM25, '800')], extrapolate=True) # 1445
131
+
132
+ This only applies to the EPA algorithm and to pollutants whose table covers
133
+ the whole scale. 8 hour ozone, for example, stops at 300 because the EPA uses
134
+ 1 hour ozone above that. The MEP standard has no such method, so ``extrapolate=True``
135
+ still raises with it. HJ 633-2026 does fix the IAQI of two pollutants above
136
+ their table: SO2 1 hour above 800 µg/m³ is 200 and O3 8 hour above 800 µg/m³
137
+ is 300 (HJ 633-2012 said they are not computed: ``mep:2012`` raises
138
+ ``aqi.NotApplicableError`` and ``to_aqi`` leaves them out).
139
+
140
+
141
+ Command line
142
+ ------------
143
+
144
+ List supported algorithms and pollutants::
145
+
146
+ $ aqi -l
147
+ epa:2012: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
148
+ epa:2015: o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
149
+ epa:2024 (latest): o3_8h (ppm), o3_1h (ppm), pm10 (µg/m³), pm25 (µg/m³), co_8h (ppm), so2_1h (ppb), no2_1h (ppb)
150
+ mep:2012: so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
151
+ mep:2026 (latest): so2_24h (µg/m³), so2_1h (µg/m³), no2_24h (µg/m³), no2_1h (µg/m³), pm10 (µg/m³), co_24h (mg/m³), co_1h (mg/m³), o3_1h (µg/m³), o3_8h (µg/m³), pm25 (µg/m³)
152
+
153
+ Pick the version of a standard with ``family:version``, ``epa`` alone means the
154
+ latest version, the module names (``aqi.algos.epa``) still work and also mean
155
+ the latest::
156
+
157
+ $ aqi epa:2012 pm25:12
158
+ 50
159
+
160
+ Convert PM2.5 to IAQI using EPA algorithm::
161
+
162
+ $ aqi aqi.algos.epa pm25:12
163
+ 56
164
+
165
+ Convert PM2.5 to IAQI using EPA algorithm (full length)::
166
+
167
+ $ aqi -c aqi aqi.algos.epa pm25:12
168
+ 56
169
+
170
+ Convert pollutants concentrations to AQI using EPA algorithm::
171
+
172
+ $ aqi aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
173
+ 122
174
+
175
+ Convert pollutants concentrations to AQI using EPA algorithm, display IAQIs::
176
+
177
+ $ aqi -v aqi.algos.epa pm25:40.9 o3_8h:0.077 co_8h:8.4
178
+ pm25:114 o3_8h:122 co_8h:90
179
+ 122
180
+
181
+ Convert PM2.5 IAQI to concentration using EPA algorithm::
182
+
183
+ $ aqi -c cc aqi.algos.epa pm25:39
184
+ 7.0
185
+
186
+ Out of range values are an error, the message goes to stderr and the exit
187
+ code is 3::
188
+
189
+ $ aqi aqi.algos.epa pm25:800
190
+ Out of range: pm25: 800.0 is not covered by the algorithm (valid range is 0.0 to 325.4)
191
+
192
+ ``-e`` computes EPA values beyond the scale ("Beyond the AQI")::
193
+
194
+ $ aqi -e aqi.algos.epa pm25:800
195
+ 1445
196
+
197
+
198
+ Development
199
+ ===========
200
+
201
+ Set up the development environment (ruff, pre-commit, hatch, coverage)::
202
+
203
+ $ uv sync
204
+ $ uv run pre-commit install
205
+
206
+ Code is linted and formatted with `ruff <https://docs.astral.sh/ruff/>`_
207
+ through `pre-commit <https://pre-commit.com/>`_, the checks run on each
208
+ commit. To run them on all files::
209
+
210
+ $ uv run pre-commit run --all-files
211
+
212
+ Test the package::
213
+
214
+ $ uv run python -m unittest discover
215
+
216
+ The same checks and the tests on Python 3.9 to 3.14 run on GitHub Actions
217
+ (``.github/workflows/ci.yml``) for each push and pull request.
218
+
219
+
220
+ Release
221
+ =======
222
+
223
+ A release is made from ``master`` with a clean tree, after adding a
224
+ ``X.Y.Z (unreleased)`` section to ``docs/changelog.rst``::
225
+
226
+ $ make release SEGMENT=major # or minor, patch (the default)
227
+
228
+ It runs, in this order:
229
+
230
+ ``make bump``
231
+ runs the tests, bumps the version with ``hatch version``, refreshes
232
+ ``uv.lock``, dates the changelog section, commits ``release: bump to
233
+ X.Y.Z`` and tags ``X.Y.Z`` (no ``v``).
234
+ ``make publish``
235
+ builds the tagged commit and uploads it to PyPI with ``uv publish``. The
236
+ token is a PyPI API token in the ``UV_PUBLISH_TOKEN`` environment
237
+ variable, it is not stored in the repository.
238
+ ``make push``
239
+ pushes the commit and the tag to ``origin`` (``REMOTE=...`` to change it)
240
+ once the upload worked.
241
+
242
+
243
+ Project
244
+ =======
245
+
246
+ * `Source code on github <https://github.com/hrbonz/python-aqi>`_
247
+ * `Documentation on readthedocs <https://python-aqi.readthedocs.io/>`_
248
+ * `Package on pypi <https://pypi.python.org/pypi/python-aqi>`_
249
+
250
+
251
+ Resources
252
+ =========
253
+
254
+ * EPA AQI: Technical Assistance Document for the Reporting of Daily Air
255
+ Quality – the Air Quality Index (AQI) December 2013) found at http://www.epa.gov/airnow/aqi-technical-assistance-document-dec2013.pdf
256
+ * National Ambient Air Quality Standards for Particulate Matter found at http://www.gpo.gov/fdsys/pkg/FR-2013-01-15/pdf/2012-30946.pdf
257
+ * MEP AQI:
258
+
259
+ * GB3095—2012 (2012/02/29) found at http://www.mep.gov.cn/gkml/hbb/bwj/201203/t20120302_224147.htm
260
+ * HJ633-2012 (2012/02/29) found at https://www.mee.gov.cn/ywgz/fgbz/bz/bzwb/jcffbz/201203/W020120410332725219541.pdf
261
+
262
+
263
+ License
264
+ =======
265
+
266
+ python-aqi is published under a BSD 3-clause license, see the LICENSE file
267
+ distributed with the project.