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):
141def update_model_setup(model, update_default_setup):
142
143    for key, value in update_default_setup.items():
144        setattr(model.setup, key, value)
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]}")