atmofetch 0.1.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.
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+ - run: pip install ruff
18
+ - name: Ruff check
19
+ run: ruff check src/ tests/
20
+ - name: Ruff format check
21
+ run: ruff format --check src/ tests/
22
+
23
+ typecheck:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.12"
30
+ - run: pip install -e ".[dev]"
31
+ - name: Mypy
32
+ run: mypy src/atmofetch --ignore-missing-imports
33
+
34
+ test:
35
+ runs-on: ubuntu-latest
36
+ strategy:
37
+ matrix:
38
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ - run: pip install -e ".[dev]"
45
+ - name: Run tests
46
+ run: pytest tests/ -v
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ *.egg
10
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AtmoFetch contributors
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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: atmofetch
3
+ Version: 0.1.0
4
+ Summary: Download meteorological data from publicly available repositories
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: beautifulsoup4>=4.12
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: lxml>=5.0
11
+ Requires-Dist: pandas>=2.0
12
+ Requires-Dist: tqdm>=4.60
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.4; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # atmofetch
21
+
22
+ Python package to download *in-situ* meteorological data from publicly available repositories:
23
+
24
+ - **OGIMET** ([ogimet.com](http://ogimet.com/index.phtml.en)) — up-to-date SYNOP dataset (hourly & daily)
25
+ - **University of Wyoming** ([weather.uwyo.edu](http://weather.uwyo.edu/upperair/)) — atmospheric vertical profiling (sounding) data
26
+ - **NOAA NCEI** ([ncei.noaa.gov](https://www.ncei.noaa.gov/pub/data/noaa/)) — Integrated Surface Hourly (ISH) meteorological data
27
+ - **NOAA GML** ([gml.noaa.gov](https://gml.noaa.gov/ccgg/trends/)) — Mauna Loa CO2 monthly measurements
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install atmofetch
33
+ ```
34
+
35
+ Or install from source:
36
+
37
+ ```bash
38
+ pip install -e ".[dev]"
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ### Download hourly NOAA ISH data
44
+
45
+ ```python
46
+ from atmofetch import meteo_noaa_hourly
47
+
48
+ df = meteo_noaa_hourly(station="037720-99999", year=2023)
49
+ print(df.head())
50
+ ```
51
+
52
+ ### Download daily OGIMET data
53
+
54
+ ```python
55
+ from atmofetch import meteo_ogimet
56
+
57
+ df = meteo_ogimet(interval="daily", station=72503, coords=True)
58
+ print(df.head())
59
+ ```
60
+
61
+ ### Download CO2 data from Mauna Loa
62
+
63
+ ```python
64
+ from atmofetch import meteo_noaa_co2
65
+
66
+ co2 = meteo_noaa_co2()
67
+ print(co2.tail())
68
+ ```
69
+
70
+ ### Download atmospheric sounding
71
+
72
+ ```python
73
+ from atmofetch import sounding_wyoming
74
+
75
+ profile, metadata = sounding_wyoming(wmo_id=45004, yy=2023, mm=7, dd=17, hh=12)
76
+ print(profile.head())
77
+ ```
78
+
79
+ ### Find nearest stations
80
+
81
+ ```python
82
+ from atmofetch import nearest_stations_noaa, nearest_stations_ogimet
83
+
84
+ # NOAA stations near London
85
+ noaa = nearest_stations_noaa(country="UNITED KINGDOM", point=(-0.1, 51.5))
86
+ print(noaa[["STATION NAME", "distance"]].head())
87
+
88
+ # OGIMET stations near Paris
89
+ ogimet = nearest_stations_ogimet(country="France", point=(2.35, 48.86))
90
+ print(ogimet.head())
91
+ ```
92
+
93
+ ### Calculate distance between two points
94
+
95
+ ```python
96
+ from atmofetch import spheroid_dist
97
+
98
+ km = spheroid_dist((18.63, 54.37), (17.02, 54.47))
99
+ print(f"Distance: {km:.1f} km")
100
+ ```
101
+
102
+ ## API Reference
103
+
104
+ ### Meteorological Data
105
+
106
+ | Function | Source | Description |
107
+ |---|---|---|
108
+ | `meteo_ogimet()` | OGIMET | Hourly or daily SYNOP data |
109
+ | `ogimet_hourly()` | OGIMET | Hourly SYNOP data |
110
+ | `ogimet_daily()` | OGIMET | Daily SYNOP summaries |
111
+ | `meteo_noaa_hourly()` | NOAA ISH | Hourly data (some stations >100 years) |
112
+ | `meteo_noaa_co2()` | NOAA GML | Monthly CO2 from Mauna Loa |
113
+ | `sounding_wyoming()` | U. Wyoming | Vertical atmospheric profiles (TEMP/BUFR) |
114
+
115
+ ### Station Discovery
116
+
117
+ | Function | Source | Description |
118
+ |---|---|---|
119
+ | `stations_ogimet()` | OGIMET | List all stations for a country |
120
+ | `nearest_stations_ogimet()` | OGIMET | Find nearest OGIMET stations |
121
+ | `nearest_stations_noaa()` | NOAA | Find nearest NOAA ISH stations |
122
+
123
+ ### Utilities
124
+
125
+ | Function | Description |
126
+ |---|---|
127
+ | `spheroid_dist()` | Distance (km) between two (lon, lat) points |
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,112 @@
1
+ # atmofetch
2
+
3
+ Python package to download *in-situ* meteorological data from publicly available repositories:
4
+
5
+ - **OGIMET** ([ogimet.com](http://ogimet.com/index.phtml.en)) — up-to-date SYNOP dataset (hourly & daily)
6
+ - **University of Wyoming** ([weather.uwyo.edu](http://weather.uwyo.edu/upperair/)) — atmospheric vertical profiling (sounding) data
7
+ - **NOAA NCEI** ([ncei.noaa.gov](https://www.ncei.noaa.gov/pub/data/noaa/)) — Integrated Surface Hourly (ISH) meteorological data
8
+ - **NOAA GML** ([gml.noaa.gov](https://gml.noaa.gov/ccgg/trends/)) — Mauna Loa CO2 monthly measurements
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install atmofetch
14
+ ```
15
+
16
+ Or install from source:
17
+
18
+ ```bash
19
+ pip install -e ".[dev]"
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### Download hourly NOAA ISH data
25
+
26
+ ```python
27
+ from atmofetch import meteo_noaa_hourly
28
+
29
+ df = meteo_noaa_hourly(station="037720-99999", year=2023)
30
+ print(df.head())
31
+ ```
32
+
33
+ ### Download daily OGIMET data
34
+
35
+ ```python
36
+ from atmofetch import meteo_ogimet
37
+
38
+ df = meteo_ogimet(interval="daily", station=72503, coords=True)
39
+ print(df.head())
40
+ ```
41
+
42
+ ### Download CO2 data from Mauna Loa
43
+
44
+ ```python
45
+ from atmofetch import meteo_noaa_co2
46
+
47
+ co2 = meteo_noaa_co2()
48
+ print(co2.tail())
49
+ ```
50
+
51
+ ### Download atmospheric sounding
52
+
53
+ ```python
54
+ from atmofetch import sounding_wyoming
55
+
56
+ profile, metadata = sounding_wyoming(wmo_id=45004, yy=2023, mm=7, dd=17, hh=12)
57
+ print(profile.head())
58
+ ```
59
+
60
+ ### Find nearest stations
61
+
62
+ ```python
63
+ from atmofetch import nearest_stations_noaa, nearest_stations_ogimet
64
+
65
+ # NOAA stations near London
66
+ noaa = nearest_stations_noaa(country="UNITED KINGDOM", point=(-0.1, 51.5))
67
+ print(noaa[["STATION NAME", "distance"]].head())
68
+
69
+ # OGIMET stations near Paris
70
+ ogimet = nearest_stations_ogimet(country="France", point=(2.35, 48.86))
71
+ print(ogimet.head())
72
+ ```
73
+
74
+ ### Calculate distance between two points
75
+
76
+ ```python
77
+ from atmofetch import spheroid_dist
78
+
79
+ km = spheroid_dist((18.63, 54.37), (17.02, 54.47))
80
+ print(f"Distance: {km:.1f} km")
81
+ ```
82
+
83
+ ## API Reference
84
+
85
+ ### Meteorological Data
86
+
87
+ | Function | Source | Description |
88
+ |---|---|---|
89
+ | `meteo_ogimet()` | OGIMET | Hourly or daily SYNOP data |
90
+ | `ogimet_hourly()` | OGIMET | Hourly SYNOP data |
91
+ | `ogimet_daily()` | OGIMET | Daily SYNOP summaries |
92
+ | `meteo_noaa_hourly()` | NOAA ISH | Hourly data (some stations >100 years) |
93
+ | `meteo_noaa_co2()` | NOAA GML | Monthly CO2 from Mauna Loa |
94
+ | `sounding_wyoming()` | U. Wyoming | Vertical atmospheric profiles (TEMP/BUFR) |
95
+
96
+ ### Station Discovery
97
+
98
+ | Function | Source | Description |
99
+ |---|---|---|
100
+ | `stations_ogimet()` | OGIMET | List all stations for a country |
101
+ | `nearest_stations_ogimet()` | OGIMET | Find nearest OGIMET stations |
102
+ | `nearest_stations_noaa()` | NOAA | Find nearest NOAA ISH stations |
103
+
104
+ ### Utilities
105
+
106
+ | Function | Description |
107
+ |---|---|
108
+ | `spheroid_dist()` | Distance (km) between two (lon, lat) points |
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "atmofetch"
7
+ version = "0.1.0"
8
+ description = "Download meteorological data from publicly available repositories"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "httpx>=0.27",
14
+ "pandas>=2.0",
15
+ "beautifulsoup4>=4.12",
16
+ "lxml>=5.0",
17
+ "tqdm>=4.60",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "pytest-httpx>=0.30",
24
+ "ruff>=0.4",
25
+ "mypy>=1.10",
26
+ ]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/atmofetch"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py310"
33
+ line-length = 100
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
@@ -0,0 +1,37 @@
1
+ """AtmoFetch - Download meteorological data from publicly available repositories.
2
+
3
+ Data sources:
4
+ - OGIMET (ogimet.com) — SYNOP station data (hourly & daily)
5
+ - University of Wyoming — atmospheric vertical profiling (sounding) data
6
+ - NOAA — Integrated Surface Hourly (ISH) and Mauna Loa CO2 data
7
+ """
8
+
9
+ from atmofetch.noaa import meteo_noaa_hourly, meteo_noaa_co2, nearest_stations_noaa
10
+ from atmofetch.ogimet import (
11
+ meteo_ogimet,
12
+ ogimet_daily,
13
+ ogimet_hourly,
14
+ stations_ogimet,
15
+ nearest_stations_ogimet,
16
+ )
17
+ from atmofetch.wyoming import sounding_wyoming
18
+ from atmofetch._utils.distance import spheroid_dist
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = [
23
+ # NOAA
24
+ "meteo_noaa_hourly",
25
+ "meteo_noaa_co2",
26
+ "nearest_stations_noaa",
27
+ # OGIMET
28
+ "meteo_ogimet",
29
+ "ogimet_daily",
30
+ "ogimet_hourly",
31
+ "stations_ogimet",
32
+ "nearest_stations_ogimet",
33
+ # Wyoming
34
+ "sounding_wyoming",
35
+ # Utilities
36
+ "spheroid_dist",
37
+ ]
@@ -0,0 +1,11 @@
1
+ from atmofetch._utils.distance import spheroid_dist
2
+ from atmofetch._utils.network import download, check_internet
3
+ from atmofetch._utils.coordinates import get_coord_from_string, precip_split
4
+
5
+ __all__ = [
6
+ "spheroid_dist",
7
+ "download",
8
+ "check_internet",
9
+ "get_coord_from_string",
10
+ "precip_split",
11
+ ]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+
9
+ def get_coord_from_string(txt: str, pattern: str = "Longitude") -> float | None:
10
+ """Extract a decimal-degree coordinate from an Ogimet metadata string.
11
+
12
+ Parameters
13
+ ----------
14
+ txt : raw metadata string (e.g. ``"Latitude: 52-25N Longitude: 016-50E ..."``)
15
+ pattern : ``"Longitude"`` or ``"Latitude"``
16
+ """
17
+ m = re.search(rf"{pattern}:\s*([\d]+)-([\d]+)(?:-([\d]+))?\s*([NSEW])", txt)
18
+ if m is None:
19
+ return None
20
+ deg, minutes, seconds, hemisphere = m.groups()
21
+ seconds = seconds or "0"
22
+ value = int(deg) + (int(minutes) * 5 / 3) / 100 + (int(seconds) * 5 / 3) / 100 / 60
23
+ if hemisphere in ("W", "S"):
24
+ value *= -1
25
+ return value
26
+
27
+
28
+ def precip_split(precip: pd.Series, pattern: str = "/12") -> pd.Series:
29
+ """Split Ogimet precipitation string into numeric values for a given hour window.
30
+
31
+ Parameters
32
+ ----------
33
+ precip : Series of strings like ``"1.2/6h0.0/12h3.4/24h"``
34
+ pattern : ``"/6"``, ``"/12"``, or ``"/24"``
35
+ """
36
+
37
+ def _extract(val: str | None) -> float | None:
38
+ if val is None or (isinstance(val, float) and np.isnan(val)):
39
+ return None
40
+ parts = str(val).split("h")
41
+ for part in parts:
42
+ if pattern in part:
43
+ numeric = part.replace(pattern, "")
44
+ try:
45
+ return float(numeric)
46
+ except ValueError:
47
+ return None
48
+ return None
49
+
50
+ return precip.apply(_extract)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+
6
+ def spheroid_dist(p1: tuple[float, float], p2: tuple[float, float]) -> float:
7
+ """Distance between two points on a spheroid using Vincenty's formula.
8
+
9
+ Parameters
10
+ ----------
11
+ p1 : (lon, lat) in decimal degrees
12
+ p2 : (lon, lat) in decimal degrees
13
+
14
+ Returns
15
+ -------
16
+ Distance in kilometres.
17
+ """
18
+ r = 6_371_009 # mean earth radius in metres
19
+ lon1, lat1, lon2, lat2 = (v * math.pi / 180 for v in (*p1, *p2))
20
+ diff_long = lon2 - lon1
21
+
22
+ num = (math.cos(lat2) * math.sin(diff_long)) ** 2 + (
23
+ math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(diff_long)
24
+ ) ** 2
25
+ denom = math.sin(lat1) * math.sin(lat2) + math.cos(lat1) * math.cos(lat2) * math.cos(diff_long)
26
+ d = math.atan2(math.sqrt(num), denom)
27
+ return d * r / 1000
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import httpx
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _TIMEOUT = 30.0
11
+
12
+ _OGIMET_HEADERS = {
13
+ "User-Agent": (
14
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0"
15
+ ),
16
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
17
+ "Accept-Language": "pl,en-US;q=0.7,en;q=0.3",
18
+ "Referer": "https://ogimet.com/resynops.phtml.en",
19
+ "Cookie": "cookieconsent_status=dismiss; ogimet_serverid=huracan|aNaPt|aNaPj",
20
+ }
21
+
22
+
23
+ def check_internet() -> bool:
24
+ try:
25
+ httpx.head("https://www.google.com", timeout=5)
26
+ return True
27
+ except httpx.HTTPError:
28
+ return False
29
+
30
+
31
+ def download(url: str, dest: Path | str | None = None, *, timeout: float = _TIMEOUT) -> bytes:
32
+ logger.info("Downloading %s", url)
33
+ resp = httpx.get(url, timeout=timeout, follow_redirects=True)
34
+ resp.raise_for_status()
35
+ if dest is not None:
36
+ Path(dest).write_bytes(resp.content)
37
+ return resp.content
38
+
39
+
40
+ def fetch_text(
41
+ url: str,
42
+ *,
43
+ headers: dict[str, str] | None = None,
44
+ timeout: float = _TIMEOUT,
45
+ ) -> str:
46
+ logger.info("Fetching %s", url)
47
+ resp = httpx.get(url, headers=headers, timeout=timeout, follow_redirects=True)
48
+ resp.raise_for_status()
49
+ return resp.text
50
+
51
+
52
+ def fetch_ogimet(url: str, *, timeout: float = _TIMEOUT) -> str:
53
+ return fetch_text(url, headers=_OGIMET_HEADERS, timeout=timeout)
@@ -0,0 +1,5 @@
1
+ from atmofetch.noaa.hourly import meteo_noaa_hourly
2
+ from atmofetch.noaa.co2 import meteo_noaa_co2
3
+ from atmofetch.noaa.stations import nearest_stations_noaa
4
+
5
+ __all__ = ["meteo_noaa_hourly", "meteo_noaa_co2", "nearest_stations_noaa"]
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import logging
5
+
6
+ import pandas as pd
7
+
8
+ from atmofetch._utils.network import fetch_text
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _CO2_URL = "https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_mm_mlo.txt"
13
+
14
+
15
+ def meteo_noaa_co2() -> pd.DataFrame:
16
+ """Download monthly CO2 measurements from Mauna Loa Observatory (NOAA).
17
+
18
+ Returns
19
+ -------
20
+ DataFrame with columns: yy, mm, yy_d, co2_avg, co2_interp, co2_seas, ndays, st_dev_days.
21
+ """
22
+ text = fetch_text(_CO2_URL)
23
+
24
+ lines = [line for line in text.splitlines() if not line.startswith("#")]
25
+ cleaned = "\n".join(lines)
26
+
27
+ df = pd.read_csv(
28
+ io.StringIO(cleaned),
29
+ sep=r"\s+",
30
+ header=None,
31
+ names=["yy", "mm", "yy_d", "co2_avg", "co2_interp", "co2_seas", "ndays", "st_dev_days"],
32
+ na_values=["-9.99", "-0.99"],
33
+ )
34
+ return df