Loading config/tutorial/tutorial.rc +6 −1 Viewed Changes for config/tutorial/tutorial.rc: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -308,6 +308,8 @@ cso.tutorial.inquire-table-dataspace.url : https://stac.data ! collection name: cso.tutorial.inquire-table-dataspace.collections : sentinel-5p-l2-no2-rpro \ sentinel-5p-l2-no2-offl !!~ near-real time processing: !cso.tutorial.inquire-table-dataspace.collections : sentinel-5p-l2-no2-nrti ! target area; !!~ empty for no limitation: Loading Loading @@ -375,6 +377,8 @@ my.tutorial.selection : C03 ! cso.tutorial.convert.selection : (%{collection} == '03') and (%{processing} == 'RPRO') ; \ (%{collection} == '03') and (%{processing} == 'OFFL') !!~ for near-real-time processing: !cso.tutorial.convert.selection : %{collection} == '03' ! input directory; ! files are searched here or downloaded to if not present yet; Loading Loading @@ -425,7 +429,8 @@ cso.tutorial.convert.filter.quality.units : 1 ! - times are taken from mid of selection, rounded to hours ! - use '%{processing}' for the processing name ! - use '%{orbit}' for orbit number cso.tutorial.convert.output.filename : ${my.work}/CSO-data/${my.region}/S5p/NO2/${my.tutorial.selection}/%Y/%m/S5p_%{processing}_NO2_%{orbit}.nc ! - include start time to have different files if orbit is stored in patches cso.tutorial.convert.output.filename : ${my.work}/CSO-data/${my.region}/S5p/NO2/${my.tutorial.selection}/%Y/%m/S5p_%{processing}_NO2_%{orbit}_%Y%m%dT%H%M%S.nc ! pack variables on output: cso.tutorial.convert.output.packed : True Loading src/cso/cso_colhub.py +70 −9 Viewed Changes for src/cso/cso_colhub.py: 70 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -29,6 +29,8 @@ # # 2026-04, Arjo Segers # Initialize columns of listing file. # Added production time of data files to listings. # Select file with latest production time if multiple are found. # ######################################################################## Loading Loading @@ -185,7 +187,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): # initiallize for (re)creation, # define extra columns to ensure correct content even for empty files: listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","href"] ) listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","production_time","href"] ) # archive directories archive_dirs = self.GetSetting("dir").split() Loading Loading @@ -242,7 +244,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest.split("_") except: logging.error( Loading @@ -267,6 +269,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): data["processing"] = processing data["collection"] = collection data["processor_version"] = processor_version data["production_time"] = production_time data["href"] = os.path.join(root, fname) # update record: listing.UpdateRecord(fname, data, indent=f"{indent} ") Loading Loading @@ -413,8 +416,10 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # info .. logging.info(f"{indent}create %s ..." % lst_file) # initiallize for (re)creation: listing = cso_file.CSO_Listing() # initiallize for (re)creation, # define extra columns to ensure correct content even for empty files: listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","production_time","href"] ) # table with all available files: listfile_all = self.GetSetting("all.file") Loading Loading @@ -460,6 +465,8 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): logging.info(f"{indent}loop over available orbits ...") # loop: for orbit in orbits: ## testing ... #if orbit != '40182' : continue # info .. logging.info(f"{indent} orbit '{orbit}' ...") Loading @@ -469,11 +476,66 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # nothing selected? if len(xlst) == 0: continue # elif len(xlst) == 1: # single record: rec = xlst.GetRecord(irec=0) # else: # multiple records ... # init name of selected file: selected_filename = None # # 1st adhoc fix ... # check if these are the same data but file was recreated later (and forgotten to be removed ...) # S5P_RPRO_L2__CO_____20250715T142651_20250715T161018_40182_03_020800_20251103T112819.nc # S5P_RPRO_L2__CO_____20250715T142651_20250715T161018_40182_03_020800_20251030T133312.nc # init basenames: basenames = [] # loop over filenames: for filename in xlst.df.index: # basename without production time: basename = filename.rsplit(sep="_",maxsplit=1)[0] # store: if basename not in basenames: basenames.append( basename ) #endif # first? if selected_filename is None: # store first values: selected_filename = filename else: # newer? if filename > selected_filename: selected_filename = filename #endif #endif #endfor # filenames # should be the same .. if len(basenames) == 1: # info ... logging.info(f"{indent} selected single file based on latest production time:") logging.info(f"{indent} {selected_filename}") else: # info ... logging.warning(f"{indent} checked on production time, but found multiple base names:") for basename in basenames: logging.warning(f"{indent} {basename}") #endfor # reset selected name, check below: selected_filename = None #endif # # none of the adhoc fix lead to selection of a single file ... if selected_filename is None: logging.error(f"multiple records selected, something wrong with files or selection?") raise Exception #endif # # extract record: rec = xlst.GetRecord( name=selected_filename ) # endif # loop over selected records: for irec in range(len(xlst)): # current: rec = xlst.GetRecord(irec) ## info .. # logging.info( f"{indent} {fname}" ) # set flag: Loading @@ -497,7 +559,6 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # add record to list: listing.UpdateRecord(rec["filename"], rec) # endif # endfor ## TESTING ... # if len(listing) > 0 : Loading src/cso/cso_dataspace.py +14 −1 Viewed Changes for src/cso/cso_dataspace.py: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -55,6 +55,8 @@ # Fixed definition of bounding box for global selection. # Added increasing delays to avoid rate limit errors from STAC catalogue inquiry. # Check on different paths to 'href' attribute in stac inquiry. # Trap files that could not be downloaded. # Added production time of data files to listings. # Loading Loading @@ -378,7 +380,7 @@ class CSO_DataSpace_Inquire(utopya.UtopyaRc): platform_name, processing, rest = product_id.split("_", 2) product_type = rest[0:10] parts = rest[11:].split("_") start_time, end_time, orbit, collection, processor_version, prod_time = parts start_time, end_time, orbit, collection, processor_version, production_time = parts # convert: tfmt = "%Y%m%dT%H%M%S" Loading Loading @@ -427,6 +429,7 @@ class CSO_DataSpace_Inquire(utopya.UtopyaRc): "processing": [processing], "collection": [collection], "processor_version": [processor_version], "production_time": [production_time], "filename": [filename], "href": [href], } Loading Loading @@ -783,8 +786,12 @@ class CSO_DataSpace_Downloader(object): # check ... if npfile != 1: print(f"ERROR - found {npfile} files in S3 bucket for product: {product}") if npfile == 0: raise Exception(f"file not found in S3 bucket") else: raise Exception #endif #endif # loop over (single) files: for pfile in pfiles: Loading Loading @@ -893,8 +900,14 @@ class CSO_DataSpace_Downloader(object): # unknown ... except Exception as err: # check on known errors .. if "file not found in S3 bucket" in str(err): logging.warning(f"{indent} WARNING - could not download: {href}") break else: logging.error(f"{indent}unknown error:") logging.error(f"{indent} {str(err)}") #endif # endtry Loading src/cso/cso_file.py +21 −6 Viewed Changes for src/cso/cso_file.py: 21 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ # 2026-04, Arjo Segers # Added optional "columns" argument to initialize empty listing object. # # 2026-05, Arjo Segers # Support `name` argument for GetRecord method. # ######################################################################## ### Loading Loading @@ -1407,11 +1410,24 @@ class CSO_Listing(object): # * def GetRecord(self, irec): def GetRecord(self, irec=None, name=None): """ Return record for corresponding 0-based row index. Return record for corresponding 0-based row index or index name. """ # modules: import numpy # by name? if name is not None: indx, = numpy.where( self.df.index == name ) if len(indx) == 0: logging.error(f"value `{name}` not found in index") raise Exception #endif irec = indx[0] #endif # extract: rec = self.df.iloc[irec].copy() # add index values: Loading Loading @@ -1604,8 +1620,7 @@ class CSO_Listing(object): (%{processor_version} == '020400') & (%{processing} == 'RPRO') ; ... This is evaluted after the previously described selections. The result should be None or exactly one record. Eventually skip files in ``blacklists``. Eventually skip files in ``blacklist``. """ Loading Loading @@ -1650,7 +1665,7 @@ class CSO_Listing(object): # endif # select: df = df[df[key] == value] # endfor # endfor # key/value pairs # * Loading Loading @@ -1734,7 +1749,7 @@ class CSO_Listing(object): df = df.loc[selected] # endif # endif # endif # expr defined # * Loading src/cso/cso_s5p.py +85 −43 Viewed Changes for src/cso/cso_s5p.py: 85 added lines, 43 removed lines. Original line number Diff line number Diff line Loading @@ -81,6 +81,13 @@ # 2026-03, Arjo Segers # Updated selection of download source for Copernicus Dataspace. # # 2026-04, Arjo Segers # Trap download errors. # Select input file with latest processor version or production time if multiple are selected. # # 2026-05, Arjo Segers # Convert multiple files if orbit is stored in patches. Check on duplicate output files. # ######################################################################## ### Loading Loading @@ -2865,7 +2872,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # loop over orbits: for orbit in orbits: # info ... logging.info(indent + ' orbit "%s" ...' % orbit) logging.info(f"{indent} orbit '{orbit}' ...") ## TESTING .. #if orbit != "21861": Loading @@ -2879,14 +2886,18 @@ class CSO_S5p_Convert(utopya.UtopyaRc): ) # no orbit found? next: if len(odf) == 0: # next orbit: continue elif len(odf) > 1: logging.error(f"found multiple records matching selection;" + " use finer selection, or something wrong in inquiry table(s)?" ) raise Exception #endif # keep list of target files to check on duplicates .. output_filenames = [] # loop over records: for irec in range(len(odf)): # info ... logging.info(f"{indent} record {irec+1}/{len(odf)} ...") # selected record: rec = odf.GetRecord(0) rec = odf.GetRecord(irec) # start time of orbit: t0 = rec["start_time"] Loading @@ -2895,16 +2906,28 @@ class CSO_S5p_Convert(utopya.UtopyaRc): output_filename = t0.strftime(output_filename__template) for key in rec.keys(): if type(rec[key]) == str: output_filename = output_filename.replace("%{" + key + "}", rec[key]) output_filename = output_filename.replace(f"%{{{key}}}", rec[key]) # endif # endfor # info ... logging.info(" target file: %s" % output_filename) logging.info(f"{indent} target file: {output_filename}") # check .. if output_filename in output_filenames: logging.error(f"output file already created for this record:") logging.error(f" {output_filename}") logging.error(f"Orbit stored in more than one file?") logging.error(f"Then ensure to include a start-time in the output filename template:") logging.error(f" {output_filename__template}") raise Exception #endif # store: output_filenames.append( output_filename ) # selected productions? if len(whitelist) > 0: if os.path.basename(output_filename) not in whitelist: logging.info(" not in whitelist, skip ...") logging.info("{indent} not in whitelist, skip ...") continue # endif # endif Loading @@ -2922,7 +2945,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # error file present? if with_error_files and os.path.isfile(output_errfile): # info .. logging.info(" error file present:") logging.info(f"{indent} error file present:") # read: with open(output_errfile, "r") as f: lines = f.readlines() Loading @@ -2932,38 +2955,38 @@ class CSO_S5p_Convert(utopya.UtopyaRc): logging.info(" %s" % line.strip()) # endfor # info .. logging.info(" do not try to create again ...:") logging.info(f"{indent} do not try to create again ...:") # do not create .. create = False # # ~ message file present? elif os.path.isfile(output_msgfile): # info .. logging.info(" message file present:") logging.info(f"{indent} message file present:") # read: with open(output_msgfile, "r") as f: lines = f.readlines() # endwith # info.. for line in lines: logging.info(" %s" % line.strip()) logging.info(f"{indent} {line.strip()}") # endfor # info .. logging.info(" do not try to create again ...:") logging.info(f"{indent} do not try to create again ...:") # do not create .. create = False # # ~ not present yet, create file? elif not os.path.isfile(output_filename): # info ... logging.info(" create new file ...") logging.info(f"{indent} create new file ...") # always create: create = True # # ~ renew? elif renew: # info ... logging.info(" renew file ...") logging.info(f"{indent} renew file ...") # always create: create = True # Loading @@ -2977,7 +3000,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): orbit_file = csf.GetAttr("orbit_file", quiet=True) if orbit_file is None: # info ... logging.info(' replace existing file, no "orbit_file" attribute yet ...') logging.info(f"{indent} replace existing file, no 'orbit_file' attribute yet ...") # attribute not found, re-create to have this attribute: create = True elif fname <= orbit_file: Loading @@ -2987,9 +3010,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): create = False else: # info ... logging.info( " replace existing file, based on older processing: %s" % orbit_file ) logging.info(f"{indent} replace existing file, based on older processing: {orbit_file}") # replace: create = True # endif Loading @@ -3007,7 +3028,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # this is the input file: input_file = href # info .. logging.info(" input file: %s" % input_file) logging.info(f"{indent} input file: {input_file}") else: # input dir: Loading @@ -3015,18 +3036,18 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # replace templates: for key in rec.keys(): if type(rec[key]) == str: input_dir = input_dir.replace("%{" + key + "}", rec[key]) input_dir = input_dir.replace(f"%{{{key}}}", rec[key]) # endif # endfor # full path: input_file = os.path.join(input_dir, rec["filename"]) # info .. logging.info(" input file: %s" % input_file) logging.info(f"{indent} input file: {input_file}") # check .. if not os.path.isfile(input_file): # info .. logging.info(" not present yet, download ...") logging.info(f"{indent} not present yet, download ...") # download url: href = rec["href"] # initialize download? Loading Loading @@ -3162,11 +3183,13 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # endif # create # endif # irec (patches in orbit) ## testing ... # logging.warning( 'break after first orbit' ) # break # endfor # input files # endfor # orbits # info ... logging.info(f"{indent}") Loading Loading @@ -3326,7 +3349,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): df.sort_values("filename", inplace=True) # info ... logging.info(f"{indent}number of files : %i" % len(df)) logging.info(f"{indent}number of files : {len(df)}") # list of ';' seperated selection expression: # (%{processor_version} == '020400') & (%{processing} == 'RPRO') ; ... Loading @@ -3334,14 +3357,14 @@ class CSO_S5p_Download(utopya.UtopyaRc): # replace templates: # (xrec['processor_version'] == '020400') & (xrec['processing'] == 'RPRO') ; ... for key in df.keys(): line = line.replace("%{" + key + "}", "xrec['" + key + "']") line = line.replace(f"%{{{key}}}", f"xrec['{key}']") # endfor # split: selections = line.split(";") # info .. logging.info("selection criteria (first with matching orbit is used):") for selection in selections: logging.info(" %s" % selection.strip()) logging.info(f" {selection.strip()}") # endif # skip some? Loading @@ -3356,7 +3379,9 @@ class CSO_S5p_Download(utopya.UtopyaRc): logging.info(f"{indent}found %i orbits with overlap of time range .." % len(xdf)) # orbit labels: orbits = xdf["orbit"].unique() orbits = list( xdf["orbit"].unique() ) # sorted version: orbits.sort() # no download initialized yet: downloader = None Loading @@ -3366,11 +3391,14 @@ class CSO_S5p_Download(utopya.UtopyaRc): # loop over orbits: for orbit in orbits: # info ... logging.info(indent + ' orbit "%s" ...' % orbit) logging.info(f"{indent} orbit '{orbit}'") # search for other records for same orbit: odf = xdf[xdf["orbit"] == orbit] # sort on processor version and production time, newest first: odf.sort_values(by=["processor_version","production_time"],ascending=False) # storage for status label: "selected", "blacklisted", ... filestatus = {} # no match yet .. Loading @@ -3380,6 +3408,9 @@ class CSO_S5p_Download(utopya.UtopyaRc): for selection in selections: # make empty again: selected = [] # latest processor version and production time: pversion = None ptime = None # loop over records: for indx, xrec in odf.iterrows(): # skip? Loading @@ -3389,20 +3420,32 @@ class CSO_S5p_Download(utopya.UtopyaRc): # endif # evaluate expression including 'xrec[key]' values: if eval(selection): # already selected a record? then check on processor version and production time: if len(selected) > 0: if xrec["processor_version"] < pversion: filestatus[xrec["filename"]] = "older processor version" continue #endif if xrec["production_time"] < ptime: filestatus[xrec["filename"]] = "older production time" continue #endif #endif # store: selected.append(xrec["filename"]) filestatus[xrec["filename"]] = "selected" rec = xrec pversion = xrec["processor_version"] ptime = xrec["production_time"] # endif # endfor # records # exactly one? then leave: if len(selected) == 1: break elif len(selected) > 1: logging.error( "found more than one orbit file matching selection: %s" % selection ) logging.warning(f"{indent} found more than one orbit file matching selection: {selection}") for fname in selected: logging.error(" %s" % fname) logging.warning(f"{indent} {fname}") # endfor raise Exception # endif # number found Loading Loading @@ -3460,7 +3503,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): # initialize download? if downloader is None: # init downloader based on url: if "dataspace.copernicus.eu" in href: if href.startswith("s3://eodata/"): # download from Copernicus DataSpace: downloader = cso_dataspace.CSO_DataSpace_Downloader() # Loading @@ -3469,7 +3512,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): downloader = cso_pal.CSO_PAL_Downloader() # else: logging.error("no downloader class defined for url: {href}") logging.error(f"no downloader class defined for url: {href}") raise Exception # endif # endif Loading @@ -3481,8 +3524,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): # download might have failed .. if not os.path.isfile(input_file): logging.error(f"missing input file") logging.error(f" {input_file}") logging.error(f"missing input file: {input_file}") raise Exception # endif Loading Loading @@ -3925,7 +3967,7 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest[13:].split("_") else: product_id = rest[0:10] Loading @@ -3935,7 +3977,7 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest[11:].split("_") # endif except: Loading @@ -3958,13 +4000,13 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): data["orbit"] = orbit data["collection"] = collection data["processor_version"] = processor_version if len(prod_time) == 8: if len(production_time) == 8: data["processing_time"] = datetime.datetime.strptime( prod_time, "%Y%m%d" production_time, "%Y%m%d" ) else: data["processing_time"] = datetime.datetime.strptime( prod_time, tfmt production_time, tfmt ) # endif # update record: Loading Loading
config/tutorial/tutorial.rc +6 −1 Viewed Changes for config/tutorial/tutorial.rc: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -308,6 +308,8 @@ cso.tutorial.inquire-table-dataspace.url : https://stac.data ! collection name: cso.tutorial.inquire-table-dataspace.collections : sentinel-5p-l2-no2-rpro \ sentinel-5p-l2-no2-offl !!~ near-real time processing: !cso.tutorial.inquire-table-dataspace.collections : sentinel-5p-l2-no2-nrti ! target area; !!~ empty for no limitation: Loading Loading @@ -375,6 +377,8 @@ my.tutorial.selection : C03 ! cso.tutorial.convert.selection : (%{collection} == '03') and (%{processing} == 'RPRO') ; \ (%{collection} == '03') and (%{processing} == 'OFFL') !!~ for near-real-time processing: !cso.tutorial.convert.selection : %{collection} == '03' ! input directory; ! files are searched here or downloaded to if not present yet; Loading Loading @@ -425,7 +429,8 @@ cso.tutorial.convert.filter.quality.units : 1 ! - times are taken from mid of selection, rounded to hours ! - use '%{processing}' for the processing name ! - use '%{orbit}' for orbit number cso.tutorial.convert.output.filename : ${my.work}/CSO-data/${my.region}/S5p/NO2/${my.tutorial.selection}/%Y/%m/S5p_%{processing}_NO2_%{orbit}.nc ! - include start time to have different files if orbit is stored in patches cso.tutorial.convert.output.filename : ${my.work}/CSO-data/${my.region}/S5p/NO2/${my.tutorial.selection}/%Y/%m/S5p_%{processing}_NO2_%{orbit}_%Y%m%dT%H%M%S.nc ! pack variables on output: cso.tutorial.convert.output.packed : True Loading
src/cso/cso_colhub.py +70 −9 Viewed Changes for src/cso/cso_colhub.py: 70 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -29,6 +29,8 @@ # # 2026-04, Arjo Segers # Initialize columns of listing file. # Added production time of data files to listings. # Select file with latest production time if multiple are found. # ######################################################################## Loading Loading @@ -185,7 +187,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): # initiallize for (re)creation, # define extra columns to ensure correct content even for empty files: listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","href"] ) listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","production_time","href"] ) # archive directories archive_dirs = self.GetSetting("dir").split() Loading Loading @@ -242,7 +244,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest.split("_") except: logging.error( Loading @@ -267,6 +269,7 @@ class CSO_ColHubMirror_Inquire(utopya.UtopyaRc): data["processing"] = processing data["collection"] = collection data["processor_version"] = processor_version data["production_time"] = production_time data["href"] = os.path.join(root, fname) # update record: listing.UpdateRecord(fname, data, indent=f"{indent} ") Loading Loading @@ -413,8 +416,10 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # info .. logging.info(f"{indent}create %s ..." % lst_file) # initiallize for (re)creation: listing = cso_file.CSO_Listing() # initiallize for (re)creation, # define extra columns to ensure correct content even for empty files: listing = cso_file.CSO_Listing( columns=["orbit","processing","collection","processor_version","production_time","href"] ) # table with all available files: listfile_all = self.GetSetting("all.file") Loading Loading @@ -460,6 +465,8 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): logging.info(f"{indent}loop over available orbits ...") # loop: for orbit in orbits: ## testing ... #if orbit != '40182' : continue # info .. logging.info(f"{indent} orbit '{orbit}' ...") Loading @@ -469,11 +476,66 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # nothing selected? if len(xlst) == 0: continue # elif len(xlst) == 1: # single record: rec = xlst.GetRecord(irec=0) # else: # multiple records ... # init name of selected file: selected_filename = None # # 1st adhoc fix ... # check if these are the same data but file was recreated later (and forgotten to be removed ...) # S5P_RPRO_L2__CO_____20250715T142651_20250715T161018_40182_03_020800_20251103T112819.nc # S5P_RPRO_L2__CO_____20250715T142651_20250715T161018_40182_03_020800_20251030T133312.nc # init basenames: basenames = [] # loop over filenames: for filename in xlst.df.index: # basename without production time: basename = filename.rsplit(sep="_",maxsplit=1)[0] # store: if basename not in basenames: basenames.append( basename ) #endif # first? if selected_filename is None: # store first values: selected_filename = filename else: # newer? if filename > selected_filename: selected_filename = filename #endif #endif #endfor # filenames # should be the same .. if len(basenames) == 1: # info ... logging.info(f"{indent} selected single file based on latest production time:") logging.info(f"{indent} {selected_filename}") else: # info ... logging.warning(f"{indent} checked on production time, but found multiple base names:") for basename in basenames: logging.warning(f"{indent} {basename}") #endfor # reset selected name, check below: selected_filename = None #endif # # none of the adhoc fix lead to selection of a single file ... if selected_filename is None: logging.error(f"multiple records selected, something wrong with files or selection?") raise Exception #endif # # extract record: rec = xlst.GetRecord( name=selected_filename ) # endif # loop over selected records: for irec in range(len(xlst)): # current: rec = xlst.GetRecord(irec) ## info .. # logging.info( f"{indent} {fname}" ) # set flag: Loading @@ -497,7 +559,6 @@ class CSO_ColHubMirror_Missing(utopya.UtopyaRc): # add record to list: listing.UpdateRecord(rec["filename"], rec) # endif # endfor ## TESTING ... # if len(listing) > 0 : Loading
src/cso/cso_dataspace.py +14 −1 Viewed Changes for src/cso/cso_dataspace.py: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -55,6 +55,8 @@ # Fixed definition of bounding box for global selection. # Added increasing delays to avoid rate limit errors from STAC catalogue inquiry. # Check on different paths to 'href' attribute in stac inquiry. # Trap files that could not be downloaded. # Added production time of data files to listings. # Loading Loading @@ -378,7 +380,7 @@ class CSO_DataSpace_Inquire(utopya.UtopyaRc): platform_name, processing, rest = product_id.split("_", 2) product_type = rest[0:10] parts = rest[11:].split("_") start_time, end_time, orbit, collection, processor_version, prod_time = parts start_time, end_time, orbit, collection, processor_version, production_time = parts # convert: tfmt = "%Y%m%dT%H%M%S" Loading Loading @@ -427,6 +429,7 @@ class CSO_DataSpace_Inquire(utopya.UtopyaRc): "processing": [processing], "collection": [collection], "processor_version": [processor_version], "production_time": [production_time], "filename": [filename], "href": [href], } Loading Loading @@ -783,8 +786,12 @@ class CSO_DataSpace_Downloader(object): # check ... if npfile != 1: print(f"ERROR - found {npfile} files in S3 bucket for product: {product}") if npfile == 0: raise Exception(f"file not found in S3 bucket") else: raise Exception #endif #endif # loop over (single) files: for pfile in pfiles: Loading Loading @@ -893,8 +900,14 @@ class CSO_DataSpace_Downloader(object): # unknown ... except Exception as err: # check on known errors .. if "file not found in S3 bucket" in str(err): logging.warning(f"{indent} WARNING - could not download: {href}") break else: logging.error(f"{indent}unknown error:") logging.error(f"{indent} {str(err)}") #endif # endtry Loading
src/cso/cso_file.py +21 −6 Viewed Changes for src/cso/cso_file.py: 21 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ # 2026-04, Arjo Segers # Added optional "columns" argument to initialize empty listing object. # # 2026-05, Arjo Segers # Support `name` argument for GetRecord method. # ######################################################################## ### Loading Loading @@ -1407,11 +1410,24 @@ class CSO_Listing(object): # * def GetRecord(self, irec): def GetRecord(self, irec=None, name=None): """ Return record for corresponding 0-based row index. Return record for corresponding 0-based row index or index name. """ # modules: import numpy # by name? if name is not None: indx, = numpy.where( self.df.index == name ) if len(indx) == 0: logging.error(f"value `{name}` not found in index") raise Exception #endif irec = indx[0] #endif # extract: rec = self.df.iloc[irec].copy() # add index values: Loading Loading @@ -1604,8 +1620,7 @@ class CSO_Listing(object): (%{processor_version} == '020400') & (%{processing} == 'RPRO') ; ... This is evaluted after the previously described selections. The result should be None or exactly one record. Eventually skip files in ``blacklists``. Eventually skip files in ``blacklist``. """ Loading Loading @@ -1650,7 +1665,7 @@ class CSO_Listing(object): # endif # select: df = df[df[key] == value] # endfor # endfor # key/value pairs # * Loading Loading @@ -1734,7 +1749,7 @@ class CSO_Listing(object): df = df.loc[selected] # endif # endif # endif # expr defined # * Loading
src/cso/cso_s5p.py +85 −43 Viewed Changes for src/cso/cso_s5p.py: 85 added lines, 43 removed lines. Original line number Diff line number Diff line Loading @@ -81,6 +81,13 @@ # 2026-03, Arjo Segers # Updated selection of download source for Copernicus Dataspace. # # 2026-04, Arjo Segers # Trap download errors. # Select input file with latest processor version or production time if multiple are selected. # # 2026-05, Arjo Segers # Convert multiple files if orbit is stored in patches. Check on duplicate output files. # ######################################################################## ### Loading Loading @@ -2865,7 +2872,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # loop over orbits: for orbit in orbits: # info ... logging.info(indent + ' orbit "%s" ...' % orbit) logging.info(f"{indent} orbit '{orbit}' ...") ## TESTING .. #if orbit != "21861": Loading @@ -2879,14 +2886,18 @@ class CSO_S5p_Convert(utopya.UtopyaRc): ) # no orbit found? next: if len(odf) == 0: # next orbit: continue elif len(odf) > 1: logging.error(f"found multiple records matching selection;" + " use finer selection, or something wrong in inquiry table(s)?" ) raise Exception #endif # keep list of target files to check on duplicates .. output_filenames = [] # loop over records: for irec in range(len(odf)): # info ... logging.info(f"{indent} record {irec+1}/{len(odf)} ...") # selected record: rec = odf.GetRecord(0) rec = odf.GetRecord(irec) # start time of orbit: t0 = rec["start_time"] Loading @@ -2895,16 +2906,28 @@ class CSO_S5p_Convert(utopya.UtopyaRc): output_filename = t0.strftime(output_filename__template) for key in rec.keys(): if type(rec[key]) == str: output_filename = output_filename.replace("%{" + key + "}", rec[key]) output_filename = output_filename.replace(f"%{{{key}}}", rec[key]) # endif # endfor # info ... logging.info(" target file: %s" % output_filename) logging.info(f"{indent} target file: {output_filename}") # check .. if output_filename in output_filenames: logging.error(f"output file already created for this record:") logging.error(f" {output_filename}") logging.error(f"Orbit stored in more than one file?") logging.error(f"Then ensure to include a start-time in the output filename template:") logging.error(f" {output_filename__template}") raise Exception #endif # store: output_filenames.append( output_filename ) # selected productions? if len(whitelist) > 0: if os.path.basename(output_filename) not in whitelist: logging.info(" not in whitelist, skip ...") logging.info("{indent} not in whitelist, skip ...") continue # endif # endif Loading @@ -2922,7 +2945,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # error file present? if with_error_files and os.path.isfile(output_errfile): # info .. logging.info(" error file present:") logging.info(f"{indent} error file present:") # read: with open(output_errfile, "r") as f: lines = f.readlines() Loading @@ -2932,38 +2955,38 @@ class CSO_S5p_Convert(utopya.UtopyaRc): logging.info(" %s" % line.strip()) # endfor # info .. logging.info(" do not try to create again ...:") logging.info(f"{indent} do not try to create again ...:") # do not create .. create = False # # ~ message file present? elif os.path.isfile(output_msgfile): # info .. logging.info(" message file present:") logging.info(f"{indent} message file present:") # read: with open(output_msgfile, "r") as f: lines = f.readlines() # endwith # info.. for line in lines: logging.info(" %s" % line.strip()) logging.info(f"{indent} {line.strip()}") # endfor # info .. logging.info(" do not try to create again ...:") logging.info(f"{indent} do not try to create again ...:") # do not create .. create = False # # ~ not present yet, create file? elif not os.path.isfile(output_filename): # info ... logging.info(" create new file ...") logging.info(f"{indent} create new file ...") # always create: create = True # # ~ renew? elif renew: # info ... logging.info(" renew file ...") logging.info(f"{indent} renew file ...") # always create: create = True # Loading @@ -2977,7 +3000,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): orbit_file = csf.GetAttr("orbit_file", quiet=True) if orbit_file is None: # info ... logging.info(' replace existing file, no "orbit_file" attribute yet ...') logging.info(f"{indent} replace existing file, no 'orbit_file' attribute yet ...") # attribute not found, re-create to have this attribute: create = True elif fname <= orbit_file: Loading @@ -2987,9 +3010,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): create = False else: # info ... logging.info( " replace existing file, based on older processing: %s" % orbit_file ) logging.info(f"{indent} replace existing file, based on older processing: {orbit_file}") # replace: create = True # endif Loading @@ -3007,7 +3028,7 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # this is the input file: input_file = href # info .. logging.info(" input file: %s" % input_file) logging.info(f"{indent} input file: {input_file}") else: # input dir: Loading @@ -3015,18 +3036,18 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # replace templates: for key in rec.keys(): if type(rec[key]) == str: input_dir = input_dir.replace("%{" + key + "}", rec[key]) input_dir = input_dir.replace(f"%{{{key}}}", rec[key]) # endif # endfor # full path: input_file = os.path.join(input_dir, rec["filename"]) # info .. logging.info(" input file: %s" % input_file) logging.info(f"{indent} input file: {input_file}") # check .. if not os.path.isfile(input_file): # info .. logging.info(" not present yet, download ...") logging.info(f"{indent} not present yet, download ...") # download url: href = rec["href"] # initialize download? Loading Loading @@ -3162,11 +3183,13 @@ class CSO_S5p_Convert(utopya.UtopyaRc): # endif # create # endif # irec (patches in orbit) ## testing ... # logging.warning( 'break after first orbit' ) # break # endfor # input files # endfor # orbits # info ... logging.info(f"{indent}") Loading Loading @@ -3326,7 +3349,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): df.sort_values("filename", inplace=True) # info ... logging.info(f"{indent}number of files : %i" % len(df)) logging.info(f"{indent}number of files : {len(df)}") # list of ';' seperated selection expression: # (%{processor_version} == '020400') & (%{processing} == 'RPRO') ; ... Loading @@ -3334,14 +3357,14 @@ class CSO_S5p_Download(utopya.UtopyaRc): # replace templates: # (xrec['processor_version'] == '020400') & (xrec['processing'] == 'RPRO') ; ... for key in df.keys(): line = line.replace("%{" + key + "}", "xrec['" + key + "']") line = line.replace(f"%{{{key}}}", f"xrec['{key}']") # endfor # split: selections = line.split(";") # info .. logging.info("selection criteria (first with matching orbit is used):") for selection in selections: logging.info(" %s" % selection.strip()) logging.info(f" {selection.strip()}") # endif # skip some? Loading @@ -3356,7 +3379,9 @@ class CSO_S5p_Download(utopya.UtopyaRc): logging.info(f"{indent}found %i orbits with overlap of time range .." % len(xdf)) # orbit labels: orbits = xdf["orbit"].unique() orbits = list( xdf["orbit"].unique() ) # sorted version: orbits.sort() # no download initialized yet: downloader = None Loading @@ -3366,11 +3391,14 @@ class CSO_S5p_Download(utopya.UtopyaRc): # loop over orbits: for orbit in orbits: # info ... logging.info(indent + ' orbit "%s" ...' % orbit) logging.info(f"{indent} orbit '{orbit}'") # search for other records for same orbit: odf = xdf[xdf["orbit"] == orbit] # sort on processor version and production time, newest first: odf.sort_values(by=["processor_version","production_time"],ascending=False) # storage for status label: "selected", "blacklisted", ... filestatus = {} # no match yet .. Loading @@ -3380,6 +3408,9 @@ class CSO_S5p_Download(utopya.UtopyaRc): for selection in selections: # make empty again: selected = [] # latest processor version and production time: pversion = None ptime = None # loop over records: for indx, xrec in odf.iterrows(): # skip? Loading @@ -3389,20 +3420,32 @@ class CSO_S5p_Download(utopya.UtopyaRc): # endif # evaluate expression including 'xrec[key]' values: if eval(selection): # already selected a record? then check on processor version and production time: if len(selected) > 0: if xrec["processor_version"] < pversion: filestatus[xrec["filename"]] = "older processor version" continue #endif if xrec["production_time"] < ptime: filestatus[xrec["filename"]] = "older production time" continue #endif #endif # store: selected.append(xrec["filename"]) filestatus[xrec["filename"]] = "selected" rec = xrec pversion = xrec["processor_version"] ptime = xrec["production_time"] # endif # endfor # records # exactly one? then leave: if len(selected) == 1: break elif len(selected) > 1: logging.error( "found more than one orbit file matching selection: %s" % selection ) logging.warning(f"{indent} found more than one orbit file matching selection: {selection}") for fname in selected: logging.error(" %s" % fname) logging.warning(f"{indent} {fname}") # endfor raise Exception # endif # number found Loading Loading @@ -3460,7 +3503,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): # initialize download? if downloader is None: # init downloader based on url: if "dataspace.copernicus.eu" in href: if href.startswith("s3://eodata/"): # download from Copernicus DataSpace: downloader = cso_dataspace.CSO_DataSpace_Downloader() # Loading @@ -3469,7 +3512,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): downloader = cso_pal.CSO_PAL_Downloader() # else: logging.error("no downloader class defined for url: {href}") logging.error(f"no downloader class defined for url: {href}") raise Exception # endif # endif Loading @@ -3481,8 +3524,7 @@ class CSO_S5p_Download(utopya.UtopyaRc): # download might have failed .. if not os.path.isfile(input_file): logging.error(f"missing input file") logging.error(f" {input_file}") logging.error(f"missing input file: {input_file}") raise Exception # endif Loading Loading @@ -3925,7 +3967,7 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest[13:].split("_") else: product_id = rest[0:10] Loading @@ -3935,7 +3977,7 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): orbit, collection, processor_version, prod_time, production_time, ) = rest[11:].split("_") # endif except: Loading @@ -3958,13 +4000,13 @@ class CSO_S5p_Download_Listing(utopya.UtopyaRc): data["orbit"] = orbit data["collection"] = collection data["processor_version"] = processor_version if len(prod_time) == 8: if len(production_time) == 8: data["processing_time"] = datetime.datetime.strptime( prod_time, "%Y%m%d" production_time, "%Y%m%d" ) else: data["processing_time"] = datetime.datetime.strptime( prod_time, tfmt production_time, tfmt ) # endif # update record: Loading