grib2sail 0.1.0__py3-none-any.whl → 0.2.1__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.
grib2sail/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.0'
32
- __version_tuple__ = version_tuple = (0, 1, 0)
31
+ __version__ = version = '0.2.1'
32
+ __version_tuple__ = version_tuple = (0, 2, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
grib2sail/downloader.py CHANGED
@@ -1,102 +1,54 @@
1
- from rich.progress import Progress
2
- from pathlib import Path
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ import threading
3
3
  import requests
4
- import re
4
+ from rich.progress import Progress
5
5
 
6
6
  from grib2sail.logger import logger
7
+ from grib2sail.downloader_arom import handle_fetch_error_arom, download_arom
7
8
  import grib2sail.variables as v
8
- from grib2sail.token import get_arome_token
9
+
10
+ thread_local = threading.local()
11
+
12
+ def get_session():
13
+ if not hasattr(thread_local, 'session'):
14
+ thread_local.session = requests.Session()
15
+ return thread_local.session
9
16
 
10
17
  def download_gribs(m, s, d, lat, lon):
11
18
  if m.startswith('arome'):
12
- download_arome(m, s, d, lat, lon)
19
+ download_arom(m, s, d, lat, lon)
13
20
  else:
14
21
  logger.error_exit(f"Downloader failed: unexpected model: {m}")
15
22
 
16
- def download_arome(model, step, data, lat, lon):
17
- token = get_arome_token()
18
- logger.debug('Token for AROME API retrieived')
19
-
20
- # Coverages list all the individual layers categories to download
21
- coverages = []
22
- if v.DATAS[0] in data:
23
- coverages += [v.AROM_DATAS['wind_u'], v.AROM_DATAS['wind_v']]
24
- if v.DATAS[1] in data:
25
- coverages += [v.AROM_DATAS['wind_gust']]
26
- if v.DATAS[2] in data:
27
- coverages += [v.AROM_DATAS['pressure']]
28
- if v.DATAS[3] in data:
29
- coverages += [v.AROM_DATAS['cloud']]
30
-
31
- # Get latest available forecast date from arome /GetCapabilities api endpoint
23
+ def get_layers(model, urls, header):
24
+ # Downloading every layers
25
+ layers = [None] * len(urls)
26
+ with Progress() as progress:
27
+ # Showing a progress bar
28
+ task = progress.add_task('Downloading layers...', total=len(urls))
29
+
30
+ # Downloading the layer
31
+ with ThreadPoolExecutor(max_workers=10) as executor:
32
+ futures = [
33
+ executor.submit(fetch, i, url, header, model)
34
+ for i, url in enumerate(urls)
35
+ ]
36
+
37
+ for future in as_completed(futures):
38
+ idx, layer = future.result()
39
+ layers[idx] = layer
40
+ progress.advance(task)
41
+ return layers
42
+
43
+ def fetch(idx, url, headers, model):
32
44
  try:
33
- capa = requests.get(
34
- v.AROM_URLS[f"{model}_capa"],
35
- headers = {'Authorization': f"Bearer {token}"},
36
- timeout = 60,
37
- )
45
+ session = get_session()
46
+ r = session.get(url, headers=headers,timeout = 60)
47
+ r.raise_for_status()
48
+ return idx, r.content
38
49
  except Exception as e:
39
- logger.error_exit(f"Failed to contact METEO FRANCE servers: {e}")
40
-
41
- # Parse the GetCapabilities XML response to find the latest available coverage
42
- lines = [line for line in capa.text.splitlines() if coverages[0] in line]
43
- if lines:
44
- # Forecast available dates look like 1970-01-01T00:00:00Z
45
- # The last line holds the lastest available forecast run
46
- latestRun = re.search(
47
- r"\d{4}-\d{2}-\d{2}T\d{2}\.\d{2}\.\d{2}Z",
48
- lines[-1]
49
- )
50
- if latestRun:
51
- latestRun = latestRun.group()
50
+ if model in v.MODELS[:2]:
51
+ handle_fetch_error_arom(e)
52
52
  else:
53
- msg = "Error fetching AROM capabilities, couldn't find latest date"
54
- logger.error_exit(msg)
55
- else:
56
- msg = "Error fetching AROM capabilities, couldn't find latest run"
57
- logger.error_exit(msg)
58
-
59
- # Download all layers as individual grib files into one output file
60
- file = Path(f"{model}_{latestRun}_{step}.grib2")
61
- file.unlink(missing_ok=True)
62
- with open(file, "ab") as outfile, Progress() as progress:
63
- # Select forecast prevision time based on user input
64
- # 3600 means layer is the prevision for 1h after latestRun
65
- times = list(range(
66
- int(step[:-1]) * 3600,
67
- 172800+1,
68
- int(step[:-1]) * 3600)
69
- )
70
- logger.debug(f"forecast to downloads are {times}")
71
- # Showing a progress bar
72
- task = progress.add_task(
73
- 'Downloading layers...',
74
- total = len(coverages) * len(times)
75
- )
76
- for coverage in coverages:
77
- for time in times:
78
- paramCovId = f"&coverageid={coverage}{latestRun}"
79
- subTime = f"&subset=time({time})"
80
- subLat = f"&subset=lat({lat[0]},{lat[1]})"
81
- subLon = f"&subset=long({lon[0]},{lon[1]})"
82
- if 'SPECIFIC_HEIGHT' in coverage:
83
- subHeight = '&subset=height(10)'
84
- else:
85
- subHeight = ''
86
- paramSubset = subTime + subLat + subLon + subHeight
87
- url=v.AROM_URLS[f"{model}_cov"]+ paramCovId + paramSubset
88
- logger.debug(f"Downloading {url}")
89
- try:
90
- r = requests.get(
91
- url,
92
- headers = {'Authorization': f"Bearer {token}"},
93
- timeout = 60)
94
- r.raise_for_status()
95
- outfile.write(r.content)
96
- except requests.exceptions.HTTPError:
97
- logger.warning(
98
- f"Failed to download {url} status code is {r.status_code}"
99
- )
100
- except Exception as e:
101
- logger.error_exit(f"Download failed: {e}", to_clean=[file])
102
- progress.update(task, advance=1)
53
+ logger.error_exit(f"Download failed: {e}")
54
+ return idx, None
@@ -0,0 +1,115 @@
1
+ from pathlib import Path
2
+ import re
3
+ import requests
4
+ import time as t
5
+
6
+ import grib2sail.variables as v
7
+ import grib2sail.variables_arom as va
8
+ import grib2sail.downloader as d
9
+ from grib2sail.logger import logger
10
+ from grib2sail.token import get_arome_token
11
+
12
+ def download_arom(model, step, data, lat, lon):
13
+ token = get_arome_token()
14
+
15
+ # Coverages list all the individual layers categories to download
16
+ coverages = []
17
+ if v.DATAS[0] in data:
18
+ coverages += [va.AROM_DATAS['wind_u'], va.AROM_DATAS['wind_v']]
19
+ if v.DATAS[1] in data:
20
+ coverages += [va.AROM_DATAS['wind_gust']]
21
+ if v.DATAS[2] in data:
22
+ coverages += [va.AROM_DATAS['pressure']]
23
+ if v.DATAS[3] in data:
24
+ coverages += [va.AROM_DATAS['cloud']]
25
+
26
+ # Get latest available forecast date from arome /GetCapabilities api endpoint
27
+ logger.info('Finding latest available forecast')
28
+ session = d.get_session()
29
+ try:
30
+ capa = session.get(
31
+ va.AROM_URLS[f"{model}_capa"],
32
+ headers = {'Authorization': f"Bearer {token}"},
33
+ timeout = 60,
34
+ )
35
+ except Exception as e:
36
+ logger.error_exit(f"Failed to contact METEO FRANCE servers: {e}")
37
+
38
+ # Parse the GetCapabilities XML response to find the latest available coverage
39
+ lines = [line for line in capa.text.splitlines() if coverages[0] in line]
40
+ if lines:
41
+ # Forecast available dates look like 1970-01-01T00:00:00Z
42
+ # The last line holds the lastest available forecast run
43
+ latestRun = re.search(
44
+ r"\d{4}-\d{2}-\d{2}T\d{2}\.\d{2}\.\d{2}Z",
45
+ lines[-1]
46
+ )
47
+ if latestRun:
48
+ latestRun = latestRun.group()
49
+ else:
50
+ msg = "Error fetching AROM capabilities, couldn't find latest date"
51
+ logger.error_exit(msg)
52
+ else:
53
+ msg = "Error fetching AROM capabilities, couldn't find latest run"
54
+ logger.error_exit(msg)
55
+
56
+ # Select forecast prevision time based on user input
57
+ # 3600 means layer is the prevision for 1h after latestRun
58
+ times = list(range(
59
+ int(step[:-1]) * 3600,
60
+ 172800+1,
61
+ int(step[:-1]) * 3600)
62
+ )
63
+ logger.debug(f"Forecast to download are {times}")
64
+
65
+ # Generating the urls to retreive requested layers
66
+ header = {'Authorization': f"Bearer {token}"}
67
+ urls = []
68
+ for coverage in coverages:
69
+ for time in times:
70
+ paramCovId = f"&coverageid={coverage}{latestRun}"
71
+ subTime = f"&subset=time({time})"
72
+ subLat = f"&subset=lat({lat[0]},{lat[1]})"
73
+ subLon = f"&subset=long({lon[0]},{lon[1]})"
74
+ if 'SPECIFIC_HEIGHT' in coverage:
75
+ subHeight = '&subset=height(10)'
76
+ else:
77
+ subHeight = ''
78
+ paramSubset = subTime + subLat + subLon + subHeight
79
+ urls.append(va.AROM_URLS[f"{model}_cov"]+ paramCovId + paramSubset)
80
+
81
+ # Downloading the layers
82
+ layers = []
83
+ if len(urls) < 100:
84
+ layers = d.get_layers(model, urls, header)
85
+ else:
86
+ msg = f"The requested grib has {len(urls)} layers, but MeteoFrance"
87
+ msg += ' servers limit requests to 100 per minute. This program will'
88
+ msg += ' sleep 1 minute every 100 layer util the complete grib file'
89
+ msg += ' is downloaded. You might want to consider reducing the number'
90
+ msg += ' of layers by increasing the step or reducing the number of'
91
+ msg += ' data'
92
+ logger.warning(msg)
93
+ for i in range(0, len(urls), 100):
94
+ layers.extend(d.get_layers(model, urls[i:i+100], header))
95
+ if i+100 < len(urls):
96
+ logger.info('Sleeping 1 minute...')
97
+ t.sleep(60)
98
+
99
+ # Output the file once all the layers have been downloaded
100
+ file = Path(f"{model}_{latestRun}_{step}.grib2")
101
+ file.unlink(missing_ok=True)
102
+ with open(file, "wb") as outfile:
103
+ for layer in layers:
104
+ if layer:
105
+ outfile.write(layer)
106
+
107
+ def handle_fetch_error_arom(e):
108
+ if isinstance(e, requests.exceptions.HTTPError):
109
+ url = e.response.url
110
+ layer = re.search(r"coverageid=(.*?)__", url).group(1)
111
+ time = int(re.search(r"subset=time\(([^()]*)", url).group(1)) / 3600
112
+ logger.warning(f"Missing layer: {layer} at time: {int(time)}h")
113
+ logger.debug(f"Error was {e}")
114
+ else:
115
+ logger.error_exit(f"Download failed: {e}")
grib2sail/token.py CHANGED
@@ -1,16 +1,18 @@
1
1
  import os
2
2
  import keyring
3
3
  import getpass
4
- import requests
5
4
 
6
- from grib2sail import variables as v
5
+ import grib2sail.downloader as d
6
+ from grib2sail import variables_arom as va
7
7
  from grib2sail.logger import logger
8
8
 
9
9
  def get_arome_token():
10
+ logger.info('Authenticating to MeteoFrance')
10
11
  appId = get_arome_appid()
12
+ session = d.get_session()
11
13
  try:
12
- response = requests.post(
13
- v.AROM_URLS['token'],
14
+ response = session.post(
15
+ va.AROM_URLS['token'],
14
16
  data = { 'grant_type': 'client_credentials' },
15
17
  headers = { 'Authorization': f"Basic {appId}" },
16
18
  timeout = 60,
grib2sail/variables.py CHANGED
@@ -1,23 +1,3 @@
1
1
  MODELS = ['arome_antilles', 'arome001']
2
2
  STEPS = ['1h', '3h', '6h', '12h']
3
3
  DATAS = ['wind', 'wind_gust', 'pressure', 'cloud', 'rain']
4
-
5
- AROM_DATAS = {
6
- 'wind_u': 'U_COMPONENT_OF_WIND__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
7
- 'wind_v': 'V_COMPONENT_OF_WIND__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
8
- 'wind_gust': 'WIND_SPEED_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
9
- 'pressure': 'PRESSURE__MEAN_SEA_LEVEL___',
10
- 'cloud': 'TOTAL_CLOUD_COVER__GROUND_OR_WATER_SURFACE___'
11
- }
12
-
13
- AROM_URLS = {
14
- 'token': 'https://portail-api.meteofrance.fr/token',
15
- f"{MODELS[0]}_cov": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-OM-0025-ANTIL-WCS/GetCoverage?service=WCS&version=2.0.1&format=application/wmo-grib',
16
- f"{MODELS[1]}_cov": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-001-FRANCE-WCS/GetCoverage?service=WCS&version=2.0.1&format=application/wmo-grib',
17
- f"{MODELS[0]}_capa": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-OM-0025-ANTIL-WCS/GetCapabilities?service=WCS&version=1.3.0&language=eng',
18
- f"{MODELS[1]}_capa": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-001-FRANCE-WCS/GetCapabilities?service=WCS&version=1.3.0&language=eng',
19
- }
20
-
21
-
22
-
23
-
@@ -0,0 +1,17 @@
1
+ import grib2sail.variables as v
2
+
3
+ AROM_DATAS = {
4
+ 'wind_u': 'U_COMPONENT_OF_WIND__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
5
+ 'wind_v': 'V_COMPONENT_OF_WIND__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
6
+ 'wind_gust': 'WIND_SPEED_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND___',
7
+ 'pressure': 'PRESSURE__MEAN_SEA_LEVEL___',
8
+ 'cloud': 'TOTAL_CLOUD_COVER__GROUND_OR_WATER_SURFACE___'
9
+ }
10
+
11
+ AROM_URLS = {
12
+ 'token': 'https://portail-api.meteofrance.fr/token',
13
+ f"{v.MODELS[0]}_cov": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-OM-0025-ANTIL-WCS/GetCoverage?service=WCS&version=2.0.1&format=application/wmo-grib',
14
+ f"{v.MODELS[1]}_cov": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-001-FRANCE-WCS/GetCoverage?service=WCS&version=2.0.1&format=application/wmo-grib',
15
+ f"{v.MODELS[0]}_capa": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-OM-0025-ANTIL-WCS/GetCapabilities?service=WCS&version=1.3.0&language=eng',
16
+ f"{v.MODELS[1]}_capa": 'https://public-api.meteofrance.fr/public/arome/1.0/wcs/MF-NWP-HIGHRES-AROME-001-FRANCE-WCS/GetCapabilities?service=WCS&version=1.3.0&language=eng',
17
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grib2sail
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Grib files downloader for sailing purposes
5
5
  Author-email: Chinkara <poubelledechinkara@outlook.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -16,11 +16,17 @@ Dynamic: license-file
16
16
 
17
17
  # GRIB2Sail
18
18
 
19
- ![PyPI version](https://img.shields.io/pypi/v/grib2sail.svg)
20
- ![CI](https://img.shields.io/github/workflow/status/Ch1nkara/GRIB2Sail/CI)
21
- ![License](https://img.shields.io/badge/license-GPL%20v3-blue.svg)
22
-
23
- Grib files downloader for sailing purposes.
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/Ch1nkara/GRIB2Sail/main/docs/assets/grib2sail_logo.png" alt="GRIB2Sail" width="40%">
21
+ </p>
22
+ <p align="center">
23
+ <em>Grib files downloader for sailing purposes</em>
24
+ </p>
25
+ <p align="center">
26
+ <img src="https://img.shields.io/pypi/v/grib2sail.svg">
27
+ <img src="https://img.shields.io/github/actions/workflow/status/Ch1nkara/GRIB2Sail/release.yml">
28
+ <img src="https://img.shields.io/badge/license-GPL%20v3-blue.svg">
29
+ </p>
24
30
 
25
31
  Currently the supported models are:
26
32
  - AROME
@@ -37,7 +43,7 @@ To download GRIB from meteofrance's models (Aome), you must create a free
37
43
  account on meteofrance.fr. The procedure is as follow:
38
44
  1. Create an account on [the Météo-France API portal](https://portail-api.meteofrance.fr)
39
45
  2. Subscribe to the desired service (Arome)
40
- 3. Go to "My API" then, from your subscribe model: "Generate Token"
46
+ 3. Go to "My API" then, from your subscribed model: "Generate Token"
41
47
  4. Checkout the curl field at the bottom, it looks like :
42
48
  ```bash
43
49
  curl -k -X POST https://portal-api.meteofrance.fr/token -d "grant_type=client_credentials" -H "Authorization: Basic ABCDEF1234abcdef"
@@ -49,7 +55,7 @@ account on meteofrance.fr. The procedure is as follow:
49
55
 
50
56
  ## Usage
51
57
 
52
- To get the GRIB file contianing the wind, the wind_gust, the atmospheric
58
+ To get the GRIB file containing the wind, the wind_gust, the atmospheric
53
59
  pressure and the cloud coverage for the area between latitude 11.5N - 12.5N
54
60
  and longitude 62.5W - 61.5W with a 3 hour step from the AROME ANTILLE model
55
61
  run:
@@ -0,0 +1,15 @@
1
+ grib2sail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ grib2sail/__main__.py,sha256=U5PTpeJRtXGpcO8YPUdCD2hORF8T68IM6JW1-dDyYqE,37
3
+ grib2sail/_version.py,sha256=vYqoJTG51NOUmYyL0xt8asRK8vUT4lGAdal_EZ59mvw,704
4
+ grib2sail/cli.py,sha256=3IcGISWNF9XpDhSdSqb6z5bnc2YFmKlQKtTPH0M7hzI,2247
5
+ grib2sail/downloader.py,sha256=ZQDaPabDuKlb7CqGxwabI73T5pKC_jVdqcCyT6A6ch4,1566
6
+ grib2sail/downloader_arom.py,sha256=FA9IDvhcBiEY7uJuHTcuLY-Ku4US5uPV5wLks3maNuw,4038
7
+ grib2sail/logger.py,sha256=_erYrZlKPjhDu-reFkyylodFgSVtOcXJg5F1drYeQNY,456
8
+ grib2sail/token.py,sha256=gNYCBNEGmjMb_RGq79wvtflhKxqiiX4GXpqfMR3eiJ0,1443
9
+ grib2sail/variables.py,sha256=M0ZcusbURgPuI1ZbRjnCqRr1Ke-2057cz3EycCHYB2E,133
10
+ grib2sail/variables_arom.py,sha256=RNJZT7O5EFa4HtVYrUuH7EHwxZ9u_63F1vkBUekfOfo,1169
11
+ grib2sail-0.2.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
+ grib2sail-0.2.1.dist-info/METADATA,sha256=EmlLwu4SyOYz9_Sqo6VzcMFav-f5h3J4Tifdabt6x-U,2763
13
+ grib2sail-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ grib2sail-0.2.1.dist-info/top_level.txt,sha256=ubF1tLZ8ZWARUC9s6_0fRsuUKbVqOZmrp8UEGEBm8aE,10
15
+ grib2sail-0.2.1.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- grib2sail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- grib2sail/__main__.py,sha256=U5PTpeJRtXGpcO8YPUdCD2hORF8T68IM6JW1-dDyYqE,37
3
- grib2sail/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
4
- grib2sail/cli.py,sha256=3IcGISWNF9XpDhSdSqb6z5bnc2YFmKlQKtTPH0M7hzI,2247
5
- grib2sail/downloader.py,sha256=9dyBk02ZA1XgB0DXkUtEmo1fMNosPR-B7fs4wPbLkQA,3581
6
- grib2sail/logger.py,sha256=_erYrZlKPjhDu-reFkyylodFgSVtOcXJg5F1drYeQNY,456
7
- grib2sail/token.py,sha256=ciyJNhJuXCHlPEFOIJfqpE0XNJr3FwGbdJmhagwzy04,1346
8
- grib2sail/variables.py,sha256=JInLyk0ShWBffNjokDW7u2RAyRSUj9KZxRiQXVppwU4,1266
9
- grib2sail-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- grib2sail-0.1.0.dist-info/METADATA,sha256=7Oa_OtDxJgJyI0Q7e1TmaYGtVrVaF64gFKj1dXj8wRs,2527
11
- grib2sail-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- grib2sail-0.1.0.dist-info/top_level.txt,sha256=ubF1tLZ8ZWARUC9s6_0fRsuUKbVqOZmrp8UEGEBm8aE,10
13
- grib2sail-0.1.0.dist-info/RECORD,,