noaa-gml-file-reader 2.0.0__py3-none-any.whl
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.
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""noaa-gml-file-reader — read NOAA GML trace-gas ASCII files as DataFrames.
|
|
2
|
+
|
|
3
|
+
Public API: :func:`read_data`. The parsing implementation lives in
|
|
4
|
+
:mod:`noaa_gml_file_reader._reader`; the license is governed by the LICENSE file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Local imports.
|
|
8
|
+
from noaa_gml_file_reader._reader import read_data, UnrecognizedFormatError
|
|
9
|
+
|
|
10
|
+
# Dunder definitions.
|
|
11
|
+
# - Versioning system: {major_version}.{minor_version}.{patch}
|
|
12
|
+
# - Single-sourced here; pyproject.toml reads __version__ from this module.
|
|
13
|
+
__author__ = "Erick Edward Shepherd"
|
|
14
|
+
__version__ = "2.0.0"
|
|
15
|
+
|
|
16
|
+
__all__ = ["read_data", "UnrecognizedFormatError", "__version__"]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Internal parsing implementation for noaa-gml-file-reader.
|
|
2
|
+
|
|
3
|
+
Parses NOAA GML trace-gas ASCII files into pandas DataFrames, auto-detecting the
|
|
4
|
+
header dialect (legacy ``# number_of_header_lines:`` vs current
|
|
5
|
+
``# header_lines :``). See ``docs/format-notes.md`` for the observed grammars.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Standard library imports.
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# Third party imports.
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
# The data section is whitespace-delimited (variable-width columns).
|
|
15
|
+
_DATA_DELIMITER = r"\s+"
|
|
16
|
+
|
|
17
|
+
# Documented missing-value sentinels (docs/format-notes.md), mapped to NaN so
|
|
18
|
+
# numeric columns parse numeric instead of being dragged to object/float fill.
|
|
19
|
+
_NA_VALUES = ["-999.99", "-999.999", "nan"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnrecognizedFormatError(Exception):
|
|
23
|
+
"""Raised when a file's header matches no known NOAA GML dialect.
|
|
24
|
+
|
|
25
|
+
Carries the offending ``path`` and the file's ``first_header_line`` so the
|
|
26
|
+
failure is explicit and diagnosable (replacing v1's silent empty DataFrame).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, path: str, first_header_line: str) -> None:
|
|
30
|
+
self.path = path
|
|
31
|
+
self.first_header_line = first_header_line
|
|
32
|
+
super().__init__(
|
|
33
|
+
f"Unrecognized NOAA GML file format: {path!r} — no "
|
|
34
|
+
f"'number_of_header_lines'/'header_lines' header key found or the "
|
|
35
|
+
f"header is truncated (first line: {first_header_line!r})."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Header-length keys, one per dialect. ``\s*`` around the colon tolerates the
|
|
40
|
+
# observed spacing variants ("number_of_header_lines: N" vs "header_lines : N").
|
|
41
|
+
# The legacy key is tried first: "header_lines" is a substring of
|
|
42
|
+
# "number_of_header_lines", so a legacy line must not fall through to the
|
|
43
|
+
# current pattern.
|
|
44
|
+
_LEGACY_HEADER_RE = re.compile(r"^#\s*number_of_header_lines\s*:\s*(?P<n>\d+)")
|
|
45
|
+
_CURRENT_HEADER_RE = re.compile(r"^#\s*header_lines\s*:\s*(?P<n>\d+)")
|
|
46
|
+
|
|
47
|
+
# The legacy dialect conveys column names via a "# data_fields:" header line.
|
|
48
|
+
_DATA_FIELDS_RE = re.compile(r"^#\s*data_fields\s*:\s*(?P<fields>.+)")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _detect_header(lines: list[str]) -> tuple[str | None, int | None]:
|
|
52
|
+
"""Return ``(dialect, header_count)`` from the header, or ``(None, None)``.
|
|
53
|
+
|
|
54
|
+
``dialect`` is ``"legacy"`` or ``"current"``; ``header_count`` is the number
|
|
55
|
+
of leading header lines (the data section begins on the next line).
|
|
56
|
+
"""
|
|
57
|
+
for line in lines:
|
|
58
|
+
match = _LEGACY_HEADER_RE.match(line)
|
|
59
|
+
if match:
|
|
60
|
+
return "legacy", int(match.group("n"))
|
|
61
|
+
match = _CURRENT_HEADER_RE.match(line)
|
|
62
|
+
if match:
|
|
63
|
+
return "current", int(match.group("n"))
|
|
64
|
+
return None, None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _column_names(
|
|
68
|
+
lines: list[str], dialect: str, header_count: int
|
|
69
|
+
) -> list[str] | None:
|
|
70
|
+
"""Extract the column names per dialect, or ``None`` if absent.
|
|
71
|
+
|
|
72
|
+
Legacy: the ``# data_fields:`` header line. Current: the bare (non-``#``)
|
|
73
|
+
column row that is the last header line (line ``header_count``, 1-indexed).
|
|
74
|
+
"""
|
|
75
|
+
if dialect == "legacy":
|
|
76
|
+
for line in lines[:header_count]:
|
|
77
|
+
match = _DATA_FIELDS_RE.match(line)
|
|
78
|
+
if match:
|
|
79
|
+
return match.group("fields").split()
|
|
80
|
+
return None
|
|
81
|
+
# Current dialect: names are the last header line. A truncated file whose
|
|
82
|
+
# declared header extends past EOF has no column row -> unrecognized.
|
|
83
|
+
if header_count < 1 or header_count > len(lines):
|
|
84
|
+
return None
|
|
85
|
+
return lines[header_count - 1].split()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_data(path: str) -> pd.DataFrame:
|
|
89
|
+
|
|
90
|
+
'''
|
|
91
|
+
|
|
92
|
+
Read a NOAA GML trace-gas ASCII file into a ``pandas.DataFrame``.
|
|
93
|
+
|
|
94
|
+
The header dialect is auto-detected and both are supported: the legacy
|
|
95
|
+
``# number_of_header_lines:`` form (column names from ``# data_fields:``)
|
|
96
|
+
and the current ``# header_lines :`` form (column names from the bare
|
|
97
|
+
column row that is the last header line). Documented missing-value
|
|
98
|
+
sentinels (``-999.99``, ``-999.999``, ``nan``) are mapped to ``NaN`` so
|
|
99
|
+
numeric columns parse numeric.
|
|
100
|
+
|
|
101
|
+
:param path: The path to the data file.
|
|
102
|
+
:type path: str
|
|
103
|
+
|
|
104
|
+
:return: The parsed file data, one column per header field.
|
|
105
|
+
:rtype: pandas.DataFrame
|
|
106
|
+
|
|
107
|
+
:raises UnrecognizedFormatError: If no known header dialect is detected or
|
|
108
|
+
the header is truncated. (In v1 this was a silent empty DataFrame.)
|
|
109
|
+
|
|
110
|
+
'''
|
|
111
|
+
|
|
112
|
+
with open(path) as file:
|
|
113
|
+
lines = file.readlines()
|
|
114
|
+
|
|
115
|
+
first_header_line = lines[0].rstrip("\n") if lines else ""
|
|
116
|
+
|
|
117
|
+
dialect, header_count = _detect_header(lines)
|
|
118
|
+
|
|
119
|
+
# No recognized header key: fail loudly instead of returning an empty frame.
|
|
120
|
+
if dialect is None:
|
|
121
|
+
raise UnrecognizedFormatError(path, first_header_line)
|
|
122
|
+
|
|
123
|
+
names = _column_names(lines, dialect, header_count)
|
|
124
|
+
if names is None:
|
|
125
|
+
raise UnrecognizedFormatError(path, first_header_line)
|
|
126
|
+
|
|
127
|
+
# One generic read for both dialects: skip the N header lines and apply the
|
|
128
|
+
# per-dialect column names. engine="python" is required for the regex
|
|
129
|
+
# separator under pandas >= 2; na_values maps the documented sentinels to
|
|
130
|
+
# NaN so numeric columns parse numeric.
|
|
131
|
+
return pd.read_csv(
|
|
132
|
+
path,
|
|
133
|
+
sep = _DATA_DELIMITER,
|
|
134
|
+
engine = "python",
|
|
135
|
+
names = names,
|
|
136
|
+
skiprows = header_count,
|
|
137
|
+
na_values = _NA_VALUES,
|
|
138
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: noaa-gml-file-reader
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Read NOAA GML atmospheric trace-gas ASCII data files as pandas DataFrames.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ErickShepherd/noaa-gml-file-reader
|
|
6
|
+
Author-email: Erick Edward Shepherd <Contact@ErickShepherd.com>
|
|
7
|
+
Maintainer-email: Erick Edward Shepherd <Contact@ErickShepherd.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: CH4,CO2,GMD,GML,NOAA,atmospheric,pandas,trace gases
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: pandas>=2.0
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# noaa-gml-file-reader
|
|
26
|
+
|
|
27
|
+
[](https://github.com/ErickShepherd/noaa-gml-file-reader/actions/workflows/ci.yml)
|
|
28
|
+

|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
Read [NOAA GML](https://gml.noaa.gov) atmospheric trace-gas ASCII data files as
|
|
32
|
+
[`pandas.DataFrame`](https://pandas.pydata.org/) objects — one function,
|
|
33
|
+
`read_data(path)`.
|
|
34
|
+
|
|
35
|
+
## What this reads
|
|
36
|
+
|
|
37
|
+
NOAA's Global Monitoring Laboratory (GML) publishes long-term atmospheric
|
|
38
|
+
trace-gas measurements (CO₂, CH₄, and others) as whitespace-delimited ASCII
|
|
39
|
+
files with a `#`-commented header — surface-flask **event** and **monthly**
|
|
40
|
+
series, in-situ **hourly**/daily series, and more, at
|
|
41
|
+
[gml.noaa.gov/aftp/data/trace_gases](https://gml.noaa.gov/aftp/data/trace_gases/).
|
|
42
|
+
This package turns one of those files into a tidy DataFrame, with the header's
|
|
43
|
+
column names and the documented missing-value sentinels handled for you.
|
|
44
|
+
|
|
45
|
+
> **Lineage (GMD → GML).** NOAA renamed ESRL's Global Monitoring *Division*
|
|
46
|
+
> (GMD) to the Global Monitoring *Laboratory* (GML) in 2020, and the file header
|
|
47
|
+
> format changed with it. This package was originally `noaa_esrl_gmd_file_reader`
|
|
48
|
+
> (2020); v2 renames it to **noaa-gml-file-reader** (import `noaa_gml_file_reader`)
|
|
49
|
+
> and reads the current header format. See the breaking-change note below.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install noaa-gml-file-reader
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python ≥ 3.10 and pandas ≥ 2.0. (Not yet on PyPI — install from source:
|
|
58
|
+
`pip install git+https://github.com/ErickShepherd/noaa-gml-file-reader`.)
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from noaa_gml_file_reader import read_data
|
|
64
|
+
|
|
65
|
+
df = read_data("co2_mlo_surface-flask_1_ccgg_month.txt")
|
|
66
|
+
print(df.head())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Real output (from the committed test fixture of the above Mauna Loa monthly file):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
site year month value
|
|
73
|
+
0 MLO 1969 8 322.50
|
|
74
|
+
1 MLO 1969 9 321.36
|
|
75
|
+
2 MLO 1969 10 320.74
|
|
76
|
+
3 MLO 1969 11 321.98
|
|
77
|
+
4 MLO 1969 12 323.78
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Columns come straight from the file's header; numeric columns are numeric
|
|
81
|
+
(`value` is `float64`, `year`/`month` are `int64`), and NOAA's missing-value
|
|
82
|
+
sentinels (`-999.99`, `-999.999`, `nan`) are parsed as `NaN`.
|
|
83
|
+
|
|
84
|
+
## Supported dialects
|
|
85
|
+
|
|
86
|
+
`read_data` auto-detects the header dialect — you don't need to know NOAA's
|
|
87
|
+
format history to read a file:
|
|
88
|
+
|
|
89
|
+
| dialect | header key | column names from | seen in |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| **current** | `# header_lines : N` | the bare column row that is the last header line | flask **event**, **in-situ** (hourly/daily) |
|
|
92
|
+
| **legacy** (2020) | `# number_of_header_lines: N` | the `# data_fields:` header line | flask **monthly** |
|
|
93
|
+
|
|
94
|
+
Supported products are those catalogued in [`docs/format-notes.md`](docs/format-notes.md)
|
|
95
|
+
(CO₂/CH₄ surface-flask event + monthly, CO₂ in-situ hourly, at MLO). The header
|
|
96
|
+
grammar is site- and gas-agnostic, so other stations/species in the same product
|
|
97
|
+
families parse the same way.
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
An unparseable file **raises** `UnrecognizedFormatError` (carrying the path and
|
|
102
|
+
the file's first line) rather than returning data:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from noaa_gml_file_reader import read_data, UnrecognizedFormatError
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
df = read_data("not-a-noaa-file.txt")
|
|
109
|
+
except UnrecognizedFormatError as err:
|
|
110
|
+
print(err)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## v2 breaking changes
|
|
114
|
+
|
|
115
|
+
v2 is a breaking release (hence the major bump):
|
|
116
|
+
|
|
117
|
+
- **Renamed** `noaa_esrl_gmd_file_reader` → `noaa_gml_file_reader`
|
|
118
|
+
(distribution `noaa-gml-file-reader`).
|
|
119
|
+
- **Raises `UnrecognizedFormatError`** on unrecognized/malformed input. v1
|
|
120
|
+
returned an *empty DataFrame* on any parse failure — silently indistinguishable
|
|
121
|
+
from "the file had no data" — which meant v1 failed silently on every current
|
|
122
|
+
(post-2020) NOAA file. If you relied on the empty-DataFrame behavior, catch the
|
|
123
|
+
exception instead.
|
|
124
|
+
- **Relicensed** AGPL-3.0 → MIT.
|
|
125
|
+
|
|
126
|
+
## Data & citation
|
|
127
|
+
|
|
128
|
+
The data are NOAA GML's, not this package's. Please cite the measurements per
|
|
129
|
+
NOAA GML's guidance — each product directory under
|
|
130
|
+
[gml.noaa.gov/aftp/data/trace_gases](https://gml.noaa.gov/aftp/data/trace_gases/)
|
|
131
|
+
ships a species-specific README with the citation text and data providers (the
|
|
132
|
+
file headers also carry contact and reciprocity information). This library only
|
|
133
|
+
parses the files; it does not download them.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
noaa_gml_file_reader/__init__.py,sha256=JaXTu57HuAH80ma20HRYK7gYgzWQDWzfj4-6GErhS80,618
|
|
2
|
+
noaa_gml_file_reader/_reader.py,sha256=fvbkVwAx5jGqml7mKe-8HXwK9M4EKWcfOHXpvYZkCVE,5219
|
|
3
|
+
noaa_gml_file_reader-2.0.0.dist-info/METADATA,sha256=BgLyDnLS02N5MT6w0NVf0ofyG5gliSAvHVdeXGvlQI4,5462
|
|
4
|
+
noaa_gml_file_reader-2.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
noaa_gml_file_reader-2.0.0.dist-info/licenses/LICENSE,sha256=5iG7G7gI7_T3kgp5Bwu5KKZCSAdDsChlL6e0v9xwMT8,1083
|
|
6
|
+
noaa_gml_file_reader-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Erick Edward Shepherd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|