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.
- atmofetch-0.1.0/.github/workflows/ci.yml +46 -0
- atmofetch-0.1.0/.gitignore +10 -0
- atmofetch-0.1.0/LICENSE +21 -0
- atmofetch-0.1.0/PKG-INFO +131 -0
- atmofetch-0.1.0/README.md +112 -0
- atmofetch-0.1.0/pyproject.toml +36 -0
- atmofetch-0.1.0/src/atmofetch/__init__.py +37 -0
- atmofetch-0.1.0/src/atmofetch/_utils/__init__.py +11 -0
- atmofetch-0.1.0/src/atmofetch/_utils/coordinates.py +50 -0
- atmofetch-0.1.0/src/atmofetch/_utils/distance.py +27 -0
- atmofetch-0.1.0/src/atmofetch/_utils/network.py +53 -0
- atmofetch-0.1.0/src/atmofetch/noaa/__init__.py +5 -0
- atmofetch-0.1.0/src/atmofetch/noaa/co2.py +34 -0
- atmofetch-0.1.0/src/atmofetch/noaa/hourly.py +157 -0
- atmofetch-0.1.0/src/atmofetch/noaa/stations.py +95 -0
- atmofetch-0.1.0/src/atmofetch/ogimet/__init__.py +12 -0
- atmofetch-0.1.0/src/atmofetch/ogimet/daily.py +249 -0
- atmofetch-0.1.0/src/atmofetch/ogimet/dispatcher.py +42 -0
- atmofetch-0.1.0/src/atmofetch/ogimet/hourly.py +222 -0
- atmofetch-0.1.0/src/atmofetch/ogimet/stations.py +175 -0
- atmofetch-0.1.0/src/atmofetch/wyoming/__init__.py +3 -0
- atmofetch-0.1.0/src/atmofetch/wyoming/sounding.py +145 -0
- atmofetch-0.1.0/tests/__init__.py +0 -0
- atmofetch-0.1.0/tests/test_noaa.py +35 -0
- atmofetch-0.1.0/tests/test_ogimet.py +40 -0
- atmofetch-0.1.0/tests/test_package.py +33 -0
- atmofetch-0.1.0/tests/test_utils.py +60 -0
- atmofetch-0.1.0/tests/test_wyoming.py +5 -0
|
@@ -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
|
atmofetch-0.1.0/LICENSE
ADDED
|
@@ -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.
|
atmofetch-0.1.0/PKG-INFO
ADDED
|
@@ -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,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
|