smashbox.read_inputdata.smashmodel
Created on Fri Sep 27 12:58:19 2024
@author: maxime
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3""" 4Created on Fri Sep 27 12:58:19 2024 5 6@author: maxime 7""" 8 9import smash 10import yaml 11 12import os 13import pandas as pd 14import datetime 15import shutil 16import glob 17import random 18from . import read_data 19 20update_default_setup = { 21 "prcp_date_pattern": "", 22 "pet_date_pattern": "", 23 "snow_date_pattern": "", 24 "temp_date_pattern": "", 25 "prcp_directories": {}, 26 "pet_directories": {}, 27 "snow_directories": {}, 28 "temp_directories": {}, 29 "continuous_pet": {}, 30 "timezone": "UTC", 31} 32 33 34def standardize_update_default_setup_options(model, update_default_setup): 35 36 for var in [ 37 "prcp_date_pattern", 38 "snow_date_pattern", 39 "temp_date_pattern", 40 "pet_date_pattern", 41 ]: 42 if update_default_setup[var] == "": 43 if model.setup.dt == 86_400: 44 update_default_setup[var] = "%Y%m%d" 45 else: 46 update_default_setup[var] = "%Y%m%d%H%M" 47 48 if model.setup.daily_interannual_pet: 49 update_default_setup["pet_date_pattern"] = "%Y%m%d" 50 51 52def standardize_model_date_pattern(key: str, value: str) -> str: 53 if len(value) > 0: 54 if not isinstance(value, str): 55 raise TypeError(f"{key} model setup must be a str") 56 57 if not value.startswith("%"): 58 raise ValueError(f"{value} must start by character %") 59 60 sample = value.split("%") 61 sample.pop(0) 62 63 for s in sample: 64 if not isinstance(s, str) and len(s) > 1: 65 raise ValueError( 66 f"%{s} in {value} must start by character % following by a unique letter: Ex: %Y%m%d%H%M" 67 ) 68 69 return value 70 71 72def standardize_model_data_directories(key: str, value: dict) -> str: 73 74 if not isinstance(value, dict): 75 raise TypeError(f"{key} model setup must be a dict") 76 77 key_to_pop = [] 78 for subkey, subvalue in value.items(): 79 80 if isinstance(subkey, str): 81 try: 82 int(subkey) 83 except: 84 raise ValueError( 85 f"{subkey}, must represent an integer as the dict key (order priority of the data)." 86 ) 87 elif not isinstance(subkey, int): 88 raise ValueError( 89 f"{subkey}, must be an integer as the dict key (order priority of the data)." 90 ) 91 92 if not isinstance(subvalue, str): 93 raise ValueError(f"{subvalue}, must be a str, a pathlike.") 94 95 if len(subvalue) == 0: 96 key_to_pop.append(subkey) 97 98 if len(subvalue) > 0 and not os.path.exists(subvalue): 99 raise ValueError(f"'{subvalue}', is not an existing path.") 100 101 # remove empty keys 102 for subkey in key_to_pop: 103 value.pop(subkey) 104 105 return value 106 107 108def standardize_model_continuous_pet(key: str, value: dict) -> str: 109 110 if not isinstance(value, dict): 111 raise TypeError(f"{key} model setup must be a dict") 112 113 true_count = 0 114 for subkey, subvalue in value.items(): 115 116 if isinstance(subkey, str): 117 try: 118 int(subkey) 119 except: 120 raise ValueError( 121 f"{subkey}, must represent an integer as the dict key (order priority of the data)." 122 ) 123 elif not isinstance(subkey, int): 124 raise ValueError( 125 f"{subkey}, must be an integer as the dict key (order priority of the pet data)." 126 ) 127 128 if not isinstance(subvalue, bool): 129 raise ValueError(f"{subvalue}, must be a bool.") 130 131 if subvalue == True: 132 true_count = true_count + 1 133 134 if true_count > 1: 135 raise ValueError("Only one True data is allowed in continuous pet") 136 137 return value 138 139 140def update_model_setup(model, update_default_setup): 141 142 for key, value in update_default_setup.items(): 143 setattr(model.setup, key, value) 144 145 146# Build a smash model from a setup and a mesh with external new options 147# path_setup: path to the setup file 148# path_mesh: path to the mesh file 149# setup_options: dictionnary to pass setup options that will overwrite options in path_setup 150# new options can be defined in path_setup: 151# prcp_directories: dict 152# pet_directories: dict 153# snow_directories: dict 154# temp_directories: dict 155# prcp_date_pattern : str 156# pet_date_pattern : str 157# snow_date_pattern : str 158# temp_date_pattern: str 159# continuous_pet : dict of bool 160# timezone: str 161 162 163def SmashModel(input_setup, input_mesh, setup_options={}): 164 """ 165 166 Description 167 ----------- 168 169 Build a model Smash like smash.Model() but extend capabilities of the atmos data reading 170 171 Paramerters 172 ----------- 173 174 input_setup: str() | dict() 175 the path to the setup.yaml file or the setup dictionary 176 177 input_mesh: str() | dict() 178 the path to the mesh.hdf5 file or the mesh dictionary 179 180 setup_options: dict() 181 dictionnary of setup options that will erase setup.yaml 182 183 Return 184 ------ 185 186 smash.Model() 187 Un objet model smash. 188 189 """ 190 191 if isinstance(input_setup, str): 192 setup = smash.io.read_setup(input_setup) 193 elif isinstance(input_setup, dict): 194 setup = input_setup.copy() 195 else: 196 raise ValueError("Wrong setup data type: input_setup must be a path or a dict") 197 198 sending_options = setup_options.copy() 199 200 for key, value in setup_options.items(): 201 setup.update({key: value}) 202 203 # filter setup options not allowed in smash to prevent warnings 204 for key in update_default_setup: 205 if key in setup: 206 setup.pop(key) 207 208 if isinstance(input_mesh, str): 209 mesh = smash.io.read_mesh(input_mesh) 210 elif isinstance(input_mesh, dict): 211 mesh = input_mesh.copy() 212 else: 213 raise ValueError("Wrong mesh data type: setup must be a path or a dict") 214 215 # force to prevent reading data 216 if "read_prcp" in setup: 217 setup["read_prcp"] = False 218 if "read_pet" in setup: 219 setup["read_pet"] = False 220 if "read_temp" in setup: 221 setup["read_temp"] = False 222 if "read_snow" in setup: 223 setup["read_snow"] = False 224 if "compute_mean_atmos" in setup: 225 setup["compute_mean_atmos"] = False 226 227 print( 228 f"</> Building model from {setup['start_time']} to {setup['end_time']} with time-step {setup['dt']}" 229 ) 230 231 # build the model 232 model = smash.Model(setup, mesh) 233 234 # Updtade model.setup with new options 235 standardize_update_default_setup_options(model, update_default_setup) 236 update_model_setup(model, update_default_setup) 237 238 # read all options in setup, not only those for smash: 239 if isinstance(input_setup, str): 240 with open(input_setup, "r") as file: 241 setup_overwrite = yaml.safe_load(file) 242 elif isinstance(input_setup, dict): 243 setup_overwrite = input_setup.copy() 244 else: 245 raise ValueError("Wrong setup data type: input_setup must be a path or a dict") 246 247 # overwrite setup_overwrite with setup_options 248 for key, value in setup_options.items(): 249 setup_overwrite.update({key: value}) 250 251 # Overight options with the config file 252 for key, value in setup_overwrite.items(): 253 if hasattr(model.setup, key): 254 setattr(model.setup, key, value) 255 256 # print(setup_overwrite) 257 # print(model.setup) 258 # standardize new user options 259 standardize_model_date_pattern("prcp_date_pattern", model.setup.prcp_date_pattern) 260 standardize_model_date_pattern("pet_date_pattern", model.setup.pet_date_pattern) 261 standardize_model_date_pattern("snow_date_pattern", model.setup.snow_date_pattern) 262 standardize_model_date_pattern("temp_date_pattern", model.setup.temp_date_pattern) 263 standardize_model_data_directories("prcp_directories", model.setup.prcp_directories) 264 standardize_model_data_directories("pet_directories", model.setup.pet_directories) 265 standardize_model_data_directories("temp_directories", model.setup.temp_directories) 266 standardize_model_data_directories("snow_directories", model.setup.snow_directories) 267 standardize_model_continuous_pet("continuous_pet", model.setup.continuous_pet) 268 269 # here we must manage etpc... => must be moved into read_data 270 # Get ready with Continuous ETP 271 path_to_smashbox = os.path.join( 272 os.path.expandvars("$HOME"), ".smashbox", "simlink_pet" 273 ) 274 275 if not os.path.exists(path_to_smashbox): 276 os.makedirs(path_to_smashbox) 277 278 path_to_symlink_etp = None 279 280 if "continuous_pet" in setup_overwrite: 281 282 if "pet_directories" in setup_overwrite: 283 dir_etp = setup_overwrite["pet_directories"] 284 elif "pet_directory" in setup_overwrite: 285 dir_etp = {1: setup_overwrite["pet_directory"]} 286 else: 287 dir_etp = {} 288 289 continuous_pet = setup_overwrite["continuous_pet"] 290 list_dir = os.listdir(path_to_smashbox) 291 path_to_symlink_etp = os.path.join( 292 path_to_smashbox, 293 f"symlinked_etpc_{len(list_dir)+1}_{int(random.random()*1000000)}", 294 ) 295 print("</> Path to symlinked pet is set to ", path_to_symlink_etp) 296 else: 297 continuous_pet = {} 298 299 for key, value in continuous_pet.items(): 300 if value: 301 prepare_etp_c( 302 path_to_symlink_etp=path_to_symlink_etp, 303 start_time=model.setup.start_time, 304 end_time=model.setup.end_time, 305 pet_date_pattern=model.setup.pet_date_pattern, 306 fmt=".tif", 307 dir_etp=dir_etp[key], 308 ) 309 dir_etp[key] = path_to_symlink_etp 310 sending_options.update({"pet_directories": dir_etp}) 311 break # only one source of continuous PET 312 313 # print(model.setup) 314 # read atmos data with updated func 315 print("</> Reading atmospheric data ...") 316 read_data.read_atmos_data(model, setup_options=sending_options) 317 318 # clean path to symplink pet files 319 if path_to_symlink_etp is not None: 320 if os.path.exists(path_to_symlink_etp): 321 print("</> Removing path to symplink pet ", path_to_symlink_etp) 322 shutil.rmtree(path_to_symlink_etp) 323 324 return model 325 326 327def prepare_etp_c( 328 path_to_symlink_etp="symlinked_etp_c", 329 start_time=None, 330 end_time=None, 331 pet_date_pattern="%Y%m%d", 332 fmt=".tif", 333 dir_etp="", 334): 335 336 if (start_time is None) or (end_time is None): 337 print("start_time or end_time are empty... ") 338 return None 339 340 if dir_etp == "": 341 print("dir_etp empty...") 342 return None 343 344 date_range = pd.date_range( 345 start=datetime.datetime.fromisoformat(start_time), 346 end=datetime.datetime.fromisoformat(end_time), 347 freq=datetime.timedelta(days=1), 348 ) 349 350 print("</> Prepare continuous PET ...") 351 352 if os.path.exists(path_to_symlink_etp): 353 shutil.rmtree(path_to_symlink_etp) 354 355 os.makedirs(path_to_symlink_etp) 356 357 warn_list = [] 358 for date in date_range: 359 360 date_etp_c_to_read = (date - datetime.timedelta(days=1)).strftime( 361 pet_date_pattern 362 ) 363 date_etp_c_j0 = (date).strftime("%m%d") 364 date_etp_c_j1 = (date + datetime.timedelta(days=1)).strftime("%m%d") 365 366 file_etp_c = glob.glob(f"{dir_etp}/**/*{date_etp_c_to_read}*.tif", recursive=True) 367 368 if len(file_etp_c) > 0: 369 file_etp_c = file_etp_c[0] 370 371 if os.path.exists(file_etp_c): 372 mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j0}{fmt}") 373 374 if os.path.exists(mylink): 375 os.remove(mylink) 376 377 os.symlink(file_etp_c, mylink) 378 379 mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j1}{fmt}") 380 381 if os.path.exists(mylink): 382 os.remove(mylink) 383 384 os.symlink(file_etp_c, mylink) 385 386 warn_list.append( 387 "Linked " 388 + f"ETPjc_{date_etp_c_j0}{fmt} and ETPjc_{date_etp_c_j1}{fmt} " 389 + f"to file {os.path.basename(file_etp_c)}" 390 ) 391 392 if len(warn_list) > 1: 393 print(f"</> {warn_list[0]}, ..., {warn_list[-1]}") 394 elif len(warn_list) > 0: 395 print(f"</> {warn_list[0]}")
update_default_setup =
{'prcp_date_pattern': '', 'pet_date_pattern': '', 'snow_date_pattern': '', 'temp_date_pattern': '', 'prcp_directories': {}, 'pet_directories': {}, 'snow_directories': {}, 'temp_directories': {}, 'continuous_pet': {}, 'timezone': 'UTC'}
def
standardize_update_default_setup_options(model, update_default_setup):
35def standardize_update_default_setup_options(model, update_default_setup): 36 37 for var in [ 38 "prcp_date_pattern", 39 "snow_date_pattern", 40 "temp_date_pattern", 41 "pet_date_pattern", 42 ]: 43 if update_default_setup[var] == "": 44 if model.setup.dt == 86_400: 45 update_default_setup[var] = "%Y%m%d" 46 else: 47 update_default_setup[var] = "%Y%m%d%H%M" 48 49 if model.setup.daily_interannual_pet: 50 update_default_setup["pet_date_pattern"] = "%Y%m%d"
def
standardize_model_date_pattern(key: str, value: str) -> str:
53def standardize_model_date_pattern(key: str, value: str) -> str: 54 if len(value) > 0: 55 if not isinstance(value, str): 56 raise TypeError(f"{key} model setup must be a str") 57 58 if not value.startswith("%"): 59 raise ValueError(f"{value} must start by character %") 60 61 sample = value.split("%") 62 sample.pop(0) 63 64 for s in sample: 65 if not isinstance(s, str) and len(s) > 1: 66 raise ValueError( 67 f"%{s} in {value} must start by character % following by a unique letter: Ex: %Y%m%d%H%M" 68 ) 69 70 return value
def
standardize_model_data_directories(key: str, value: dict) -> str:
73def standardize_model_data_directories(key: str, value: dict) -> str: 74 75 if not isinstance(value, dict): 76 raise TypeError(f"{key} model setup must be a dict") 77 78 key_to_pop = [] 79 for subkey, subvalue in value.items(): 80 81 if isinstance(subkey, str): 82 try: 83 int(subkey) 84 except: 85 raise ValueError( 86 f"{subkey}, must represent an integer as the dict key (order priority of the data)." 87 ) 88 elif not isinstance(subkey, int): 89 raise ValueError( 90 f"{subkey}, must be an integer as the dict key (order priority of the data)." 91 ) 92 93 if not isinstance(subvalue, str): 94 raise ValueError(f"{subvalue}, must be a str, a pathlike.") 95 96 if len(subvalue) == 0: 97 key_to_pop.append(subkey) 98 99 if len(subvalue) > 0 and not os.path.exists(subvalue): 100 raise ValueError(f"'{subvalue}', is not an existing path.") 101 102 # remove empty keys 103 for subkey in key_to_pop: 104 value.pop(subkey) 105 106 return value
def
standardize_model_continuous_pet(key: str, value: dict) -> str:
109def standardize_model_continuous_pet(key: str, value: dict) -> str: 110 111 if not isinstance(value, dict): 112 raise TypeError(f"{key} model setup must be a dict") 113 114 true_count = 0 115 for subkey, subvalue in value.items(): 116 117 if isinstance(subkey, str): 118 try: 119 int(subkey) 120 except: 121 raise ValueError( 122 f"{subkey}, must represent an integer as the dict key (order priority of the data)." 123 ) 124 elif not isinstance(subkey, int): 125 raise ValueError( 126 f"{subkey}, must be an integer as the dict key (order priority of the pet data)." 127 ) 128 129 if not isinstance(subvalue, bool): 130 raise ValueError(f"{subvalue}, must be a bool.") 131 132 if subvalue == True: 133 true_count = true_count + 1 134 135 if true_count > 1: 136 raise ValueError("Only one True data is allowed in continuous pet") 137 138 return value
def
update_model_setup(model, update_default_setup):
def
SmashModel(input_setup, input_mesh, setup_options={}):
164def SmashModel(input_setup, input_mesh, setup_options={}): 165 """ 166 167 Description 168 ----------- 169 170 Build a model Smash like smash.Model() but extend capabilities of the atmos data reading 171 172 Paramerters 173 ----------- 174 175 input_setup: str() | dict() 176 the path to the setup.yaml file or the setup dictionary 177 178 input_mesh: str() | dict() 179 the path to the mesh.hdf5 file or the mesh dictionary 180 181 setup_options: dict() 182 dictionnary of setup options that will erase setup.yaml 183 184 Return 185 ------ 186 187 smash.Model() 188 Un objet model smash. 189 190 """ 191 192 if isinstance(input_setup, str): 193 setup = smash.io.read_setup(input_setup) 194 elif isinstance(input_setup, dict): 195 setup = input_setup.copy() 196 else: 197 raise ValueError("Wrong setup data type: input_setup must be a path or a dict") 198 199 sending_options = setup_options.copy() 200 201 for key, value in setup_options.items(): 202 setup.update({key: value}) 203 204 # filter setup options not allowed in smash to prevent warnings 205 for key in update_default_setup: 206 if key in setup: 207 setup.pop(key) 208 209 if isinstance(input_mesh, str): 210 mesh = smash.io.read_mesh(input_mesh) 211 elif isinstance(input_mesh, dict): 212 mesh = input_mesh.copy() 213 else: 214 raise ValueError("Wrong mesh data type: setup must be a path or a dict") 215 216 # force to prevent reading data 217 if "read_prcp" in setup: 218 setup["read_prcp"] = False 219 if "read_pet" in setup: 220 setup["read_pet"] = False 221 if "read_temp" in setup: 222 setup["read_temp"] = False 223 if "read_snow" in setup: 224 setup["read_snow"] = False 225 if "compute_mean_atmos" in setup: 226 setup["compute_mean_atmos"] = False 227 228 print( 229 f"</> Building model from {setup['start_time']} to {setup['end_time']} with time-step {setup['dt']}" 230 ) 231 232 # build the model 233 model = smash.Model(setup, mesh) 234 235 # Updtade model.setup with new options 236 standardize_update_default_setup_options(model, update_default_setup) 237 update_model_setup(model, update_default_setup) 238 239 # read all options in setup, not only those for smash: 240 if isinstance(input_setup, str): 241 with open(input_setup, "r") as file: 242 setup_overwrite = yaml.safe_load(file) 243 elif isinstance(input_setup, dict): 244 setup_overwrite = input_setup.copy() 245 else: 246 raise ValueError("Wrong setup data type: input_setup must be a path or a dict") 247 248 # overwrite setup_overwrite with setup_options 249 for key, value in setup_options.items(): 250 setup_overwrite.update({key: value}) 251 252 # Overight options with the config file 253 for key, value in setup_overwrite.items(): 254 if hasattr(model.setup, key): 255 setattr(model.setup, key, value) 256 257 # print(setup_overwrite) 258 # print(model.setup) 259 # standardize new user options 260 standardize_model_date_pattern("prcp_date_pattern", model.setup.prcp_date_pattern) 261 standardize_model_date_pattern("pet_date_pattern", model.setup.pet_date_pattern) 262 standardize_model_date_pattern("snow_date_pattern", model.setup.snow_date_pattern) 263 standardize_model_date_pattern("temp_date_pattern", model.setup.temp_date_pattern) 264 standardize_model_data_directories("prcp_directories", model.setup.prcp_directories) 265 standardize_model_data_directories("pet_directories", model.setup.pet_directories) 266 standardize_model_data_directories("temp_directories", model.setup.temp_directories) 267 standardize_model_data_directories("snow_directories", model.setup.snow_directories) 268 standardize_model_continuous_pet("continuous_pet", model.setup.continuous_pet) 269 270 # here we must manage etpc... => must be moved into read_data 271 # Get ready with Continuous ETP 272 path_to_smashbox = os.path.join( 273 os.path.expandvars("$HOME"), ".smashbox", "simlink_pet" 274 ) 275 276 if not os.path.exists(path_to_smashbox): 277 os.makedirs(path_to_smashbox) 278 279 path_to_symlink_etp = None 280 281 if "continuous_pet" in setup_overwrite: 282 283 if "pet_directories" in setup_overwrite: 284 dir_etp = setup_overwrite["pet_directories"] 285 elif "pet_directory" in setup_overwrite: 286 dir_etp = {1: setup_overwrite["pet_directory"]} 287 else: 288 dir_etp = {} 289 290 continuous_pet = setup_overwrite["continuous_pet"] 291 list_dir = os.listdir(path_to_smashbox) 292 path_to_symlink_etp = os.path.join( 293 path_to_smashbox, 294 f"symlinked_etpc_{len(list_dir)+1}_{int(random.random()*1000000)}", 295 ) 296 print("</> Path to symlinked pet is set to ", path_to_symlink_etp) 297 else: 298 continuous_pet = {} 299 300 for key, value in continuous_pet.items(): 301 if value: 302 prepare_etp_c( 303 path_to_symlink_etp=path_to_symlink_etp, 304 start_time=model.setup.start_time, 305 end_time=model.setup.end_time, 306 pet_date_pattern=model.setup.pet_date_pattern, 307 fmt=".tif", 308 dir_etp=dir_etp[key], 309 ) 310 dir_etp[key] = path_to_symlink_etp 311 sending_options.update({"pet_directories": dir_etp}) 312 break # only one source of continuous PET 313 314 # print(model.setup) 315 # read atmos data with updated func 316 print("</> Reading atmospheric data ...") 317 read_data.read_atmos_data(model, setup_options=sending_options) 318 319 # clean path to symplink pet files 320 if path_to_symlink_etp is not None: 321 if os.path.exists(path_to_symlink_etp): 322 print("</> Removing path to symplink pet ", path_to_symlink_etp) 323 shutil.rmtree(path_to_symlink_etp) 324 325 return model
Description
Build a model Smash like smash.Model() but extend capabilities of the atmos data reading
Paramerters
input_setup: str() | dict() the path to the setup.yaml file or the setup dictionary
input_mesh: str() | dict() the path to the mesh.hdf5 file or the mesh dictionary
setup_options: dict() dictionnary of setup options that will erase setup.yaml
Return
smash.Model() Un objet model smash.
def
prepare_etp_c( path_to_symlink_etp='symlinked_etp_c', start_time=None, end_time=None, pet_date_pattern='%Y%m%d', fmt='.tif', dir_etp=''):
328def prepare_etp_c( 329 path_to_symlink_etp="symlinked_etp_c", 330 start_time=None, 331 end_time=None, 332 pet_date_pattern="%Y%m%d", 333 fmt=".tif", 334 dir_etp="", 335): 336 337 if (start_time is None) or (end_time is None): 338 print("start_time or end_time are empty... ") 339 return None 340 341 if dir_etp == "": 342 print("dir_etp empty...") 343 return None 344 345 date_range = pd.date_range( 346 start=datetime.datetime.fromisoformat(start_time), 347 end=datetime.datetime.fromisoformat(end_time), 348 freq=datetime.timedelta(days=1), 349 ) 350 351 print("</> Prepare continuous PET ...") 352 353 if os.path.exists(path_to_symlink_etp): 354 shutil.rmtree(path_to_symlink_etp) 355 356 os.makedirs(path_to_symlink_etp) 357 358 warn_list = [] 359 for date in date_range: 360 361 date_etp_c_to_read = (date - datetime.timedelta(days=1)).strftime( 362 pet_date_pattern 363 ) 364 date_etp_c_j0 = (date).strftime("%m%d") 365 date_etp_c_j1 = (date + datetime.timedelta(days=1)).strftime("%m%d") 366 367 file_etp_c = glob.glob(f"{dir_etp}/**/*{date_etp_c_to_read}*.tif", recursive=True) 368 369 if len(file_etp_c) > 0: 370 file_etp_c = file_etp_c[0] 371 372 if os.path.exists(file_etp_c): 373 mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j0}{fmt}") 374 375 if os.path.exists(mylink): 376 os.remove(mylink) 377 378 os.symlink(file_etp_c, mylink) 379 380 mylink = os.path.join(path_to_symlink_etp, f"ETPjc_{date_etp_c_j1}{fmt}") 381 382 if os.path.exists(mylink): 383 os.remove(mylink) 384 385 os.symlink(file_etp_c, mylink) 386 387 warn_list.append( 388 "Linked " 389 + f"ETPjc_{date_etp_c_j0}{fmt} and ETPjc_{date_etp_c_j1}{fmt} " 390 + f"to file {os.path.basename(file_etp_c)}" 391 ) 392 393 if len(warn_list) > 1: 394 print(f"</> {warn_list[0]}, ..., {warn_list[-1]}") 395 elif len(warn_list) > 0: 396 print(f"</> {warn_list[0]}")