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
+ [![CI](https://github.com/ErickShepherd/noaa-gml-file-reader/actions/workflows/ci.yml/badge.svg)](https://github.com/ErickShepherd/noaa-gml-file-reader/actions/workflows/ci.yml)
28
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.