1919from io import StringIO
2020from shutil import which
2121from typing import Literal , Optional , Union
22+ from urllib .parse import urlparse
2223
2324import cfgrib
2425import pandas as pd
@@ -397,6 +398,9 @@ def _check_idx(self, url: str, verbose: bool = False) -> tuple[bool, Optional[st
397398
398399 if verbose :
399400 print (f"🐜 { self .IDX_SUFFIX = } " )
401+
402+ # Initialize variable to avoid UnboundLocalError
403+ idx_exists = False
400404
401405 # Loop through IDX_SUFFIX options until we find one that exists
402406 for i in self .IDX_SUFFIX :
@@ -405,7 +409,12 @@ def _check_idx(self, url: str, verbose: bool = False) -> tuple[bool, Optional[st
405409 else :
406410 idx_url = url + i
407411
408- idx_exists = requests .head (idx_url ).ok
412+ try :
413+ idx_exists = requests .head (idx_url ).ok
414+ except Exception as e :
415+ if verbose :
416+ print (f"Unable to get index file from { idx_url } due to error { str (e )} " )
417+
409418 if verbose :
410419 print (f"🐜 { idx_url = } " )
411420 print (f"🐜 { idx_exists = } " )
@@ -431,8 +440,6 @@ def find_grib(self) -> tuple[Optional[Union[Path, str]], Optional[str]]:
431440 local_grib = self .get_localFilePath ()
432441 if local_grib .exists () and not self .overwrite :
433442 return local_grib , "local"
434- # NOTE: We will still get the idx files from a remote
435- # because they aren't stored locally, or are they? # TODO: If the idx file is local, then use that
436443
437444 # If priority list is set, we want to search SOURCES in that
438445 # priority order. If priority is None, then search all SOURCES
@@ -465,8 +472,37 @@ def find_grib(self) -> tuple[Optional[Union[Path, str]], Optional[str]]:
465472
466473 return (None , None )
467474
468- def find_idx (self ) -> tuple [Optional [Union [Path , str ]], Optional [str ]]:
475+ def find_idx (self , overwrite : bool = False ) -> tuple [Optional [Union [Path , str ]], Optional [str ]]:
469476 """Find an index file for the GRIB file."""
477+
478+ # But first, if overwrite is False, then check if the GRIB2 inventory file exists locally.
479+ if not overwrite :
480+ local_grib = self .get_localFilePath ()
481+ for suffix in self .IDX_SUFFIX :
482+ # There is no easy way to know if the index suffix needs to be appended or overwrite the grib suffix.
483+ # Hence there is a need to do various checks to verify if the index file exists locally or not.
484+
485+ # This is the case of GFS, where the grib file ends with ".grb2" and the index file is ".grb2.inv"
486+ # The index suffix needs to overwrite the grib file suffix, now ending in ".grb2.inv"
487+ if suffix .startswith (local_grib .suffix ):
488+ local_idx = local_grib .with_suffix (suffix )
489+
490+ # This is the case of GFS, where the grib file ends with ".f000" and the index file is ".idx"
491+ # The index suffix needs to be appended to the grib file suffix, now ending in ".f000.idx"
492+ else :
493+ local_idx = local_grib .with_suffix (local_grib .suffix + suffix )
494+
495+ if local_idx .exists () and not self .overwrite :
496+ return (local_idx , "local" )
497+
498+ # If the index file does not exists locally, we will need to try another variation of the index file name.
499+ # This is the case of IFS, where the grib file ends with ".grib2" and the index file is ".index"
500+ # The index suffix needs to overwrite the grib file suffix, now ending in ".index"
501+ else :
502+ local_idx = local_grib .with_suffix (suffix )
503+ if local_idx .exists () and not self .overwrite :
504+ return (local_idx , "local" )
505+
470506 # If priority list is set, we want to search SOURCES in that
471507 # priority order. If priority is None, then search all SOURCES
472508 # in the order given by the model template file.
@@ -591,10 +627,27 @@ def get_localFilePath(
591627
592628 return localFilePath
593629
630+ def get_localIndexFilePath (self ) -> Path :
631+ """Get full path to the local index file."""
632+
633+ # Get the local file path, which creates the directory structure
634+ local_file_path = self .get_localFilePath ()
635+
636+ # Get the directory in the local file path
637+ dir_path = os .path .dirname (local_file_path )
638+
639+ # Get the filename from the index URL path
640+ index_filename = os .path .basename (urlparse (self .idx ).path )
641+
642+ # Create new path with the index file name
643+ index_file_path = os .path .join (dir_path , index_filename )
644+
645+ return index_file_path
646+
594647 @functools .cached_property
595648 def index_as_dataframe (self ) -> pd .DataFrame :
596649 """Read and cache the full index file."""
597- if self .grib_source == "local" and wgrib2 :
650+ if self .idx_source is None and self . grib_source == "local" and wgrib2 :
598651 # Generate IDX inventory with wgrib2
599652 self .idx = StringIO (wgrib2_idx (self .get_localFilePath ()))
600653 self .idx_source = "generated"
@@ -625,6 +678,7 @@ def index_as_dataframe(self) -> pd.DataFrame:
625678 read_this_idx = self .idx
626679 else :
627680 read_this_idx = None
681+ print (f"Downloading inventory file from { self .idx = } " )
628682 response = requests .get (self .idx )
629683 if response .status_code != 200 :
630684 response .raise_for_status ()
@@ -635,9 +689,18 @@ def index_as_dataframe(self) -> pd.DataFrame:
635689 f"You will need to remake the Herbie object (H = `Herbie()`)\n "
636690 f"or delete this cached property: `del H.index_as_dataframe()`"
637691 )
692+
638693 read_this_idx = StringIO (response .text )
639694 response .close ()
640695
696+ index_filepath = self .get_localIndexFilePath ()
697+ os .makedirs (os .path .dirname (index_filepath ), exist_ok = True )
698+
699+ with open (index_filepath , "w" ) as file :
700+ file .write (read_this_idx .read ())
701+ # reset the cursor to the beggining of the StringIO
702+ read_this_idx .seek (0 )
703+
641704 df = pd .read_csv (
642705 read_this_idx ,
643706 sep = ":" ,
@@ -697,9 +760,34 @@ def index_as_dataframe(self) -> pd.DataFrame:
697760 # eccodes keywords explained here:
698761 # https://confluence.ecmwf.int/display/UDOC/Identification+keywords
699762
700- r = requests .get (self .idx )
701- idxs = [json .loads (x ) for x in r .text .split ("\n " ) if x ]
702- r .close ()
763+ if self .idx_source in ["local" ]:
764+ with open (self .idx , "r" ) as file :
765+ read_this_idx = StringIO (file .read ())
766+ else :
767+ print (f"Downloading inventory file from { self .idx = } " )
768+ response = requests .get (self .idx )
769+ if response .status_code != 200 :
770+ response .raise_for_status ()
771+ response .close ()
772+ raise ValueError (
773+ f"\n Cant open index file { self .idx } \n "
774+ f"Download the full file first (with `H.download()`).\n "
775+ f"You will need to remake the Herbie object (H = `Herbie()`)\n "
776+ f"or delete this cached property: `del H.index_as_dataframe()`"
777+ )
778+
779+ read_this_idx = StringIO (response .text )
780+ response .close ()
781+
782+ index_filepath = self .get_localIndexFilePath ()
783+ os .makedirs (os .path .dirname (index_filepath ), exist_ok = True )
784+
785+ with open (index_filepath , "w" ) as file :
786+ file .write (read_this_idx .read ())
787+ # reset the cursor to the beggining of the StringIO
788+ read_this_idx .seek (0 )
789+
790+ idxs = [json .loads (x ) for x in read_this_idx .getvalue ().split ("\n " ) if x ]
703791 df = pd .DataFrame (idxs )
704792
705793 # Format the DataFrame
@@ -1005,7 +1093,7 @@ def subset(search, outFile):
10051093 if self .overwrite and self .grib_source .startswith ("local" ):
10061094 # Search for the grib files on the remote archives again
10071095 self .grib , self .grib_source = self .find_grib (overwrite = True )
1008- self .idx , self .idx_source = self .find_idx ()
1096+ self .idx , self .idx_source = self .find_idx (overwrite = True )
10091097 print (f"Overwrite local file with file from [{ self .grib_source } ]" )
10101098
10111099 # Check that data exists
0 commit comments