source: flex_extract.git/test/InstallTar/flex_extract_v7.1_ecgate/source/python/mods/tools.py @ 96e1533

ctbtodev
Last change on this file since 96e1533 was 96e1533, checked in by Anne Philipp <anne.philipp@…>, 5 years ago

redefined test data dir and completed unittests for tools module

  • Property mode set to 100644
File size: 17.2 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#*******************************************************************************
4# @Author: Anne Philipp (University of Vienna)
5#
6# @Date: May 2018
7#
8# @Change History:
9#    October 2014 - Anne Fouilloux (University of Oslo)
10#        - created functions silent_remove and product (taken from ECMWF)
11#
12#    November 2015 - Leopold Haimberger (University of Vienna)
13#        - created functions: interpret_args_and_control, clean_up
14#          my_error, normal_exit, init128, to_param_id
15#
16#    April 2018 - Anne Philipp (University of Vienna):
17#        - applied PEP8 style guide
18#        - added documentation
19#        - moved all functions from file Flexparttools to this file tools
20#        - added function get_list_as_string
21#        - seperated args and control interpretation
22#
23# @License:
24#    (C) Copyright 2014-2018.
25#
26#    This software is licensed under the terms of the Apache Licence Version 2.0
27#    which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
28#
29# @Modul Description:
30#    This module contains a couple of helpful functions which are
31#    used in different places in flex_extract.
32#
33# @Module Content:
34#    - get_cmdline_arguments
35#    - clean_up
36#    - my_error
37#    - normal_exit
38#    - product
39#    - silent_remove
40#    - init128
41#    - to_param_id
42#    - get_list_as_string
43#    - make_dir
44#
45#*******************************************************************************
46
47# ------------------------------------------------------------------------------
48# MODULES
49# ------------------------------------------------------------------------------
50import os
51import errno
52import sys
53import glob
54import subprocess
55import traceback
56from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
57
58# ------------------------------------------------------------------------------
59# FUNCTIONS
60# ------------------------------------------------------------------------------
61
62def get_cmdline_arguments():
63    '''
64    @Description:
65        Decomposes the command line arguments and assigns them to variables.
66        Apply default values for non mentioned arguments.
67
68    @Input:
69        <nothing>
70
71    @Return:
72        args: instance of ArgumentParser
73            Contains the commandline arguments from script/program call.
74    '''
75
76    parser = ArgumentParser(description='Retrieve FLEXPART input from \
77                                ECMWF MARS archive',
78                            formatter_class=ArgumentDefaultsHelpFormatter)
79
80    # the most important arguments
81    parser.add_argument("--start_date", dest="start_date", default=None,
82                        help="start date YYYYMMDD")
83    parser.add_argument("--end_date", dest="end_date", default=None,
84                        help="end_date YYYYMMDD")
85    parser.add_argument("--date_chunk", dest="date_chunk", default=None,
86                        help="# of days to be retrieved at once")
87
88    # some arguments that override the default in the CONTROL file
89    parser.add_argument("--basetime", dest="basetime", default=None,
90                        help="base such as 00/12 (for half day retrievals)")
91    parser.add_argument("--step", dest="step", default=None,
92                        help="steps such as 00/to/48")
93    parser.add_argument("--levelist", dest="levelist", default=None,
94                        help="Vertical levels to be retrieved, e.g. 30/to/60")
95    parser.add_argument("--area", dest="area", default=None,
96                        help="area defined as north/west/south/east")
97
98    # set the working directories
99    parser.add_argument("--inputdir", dest="inputdir", default=None,
100                        help="root directory for storing intermediate files")
101    parser.add_argument("--outputdir", dest="outputdir", default=None,
102                        help="root directory for storing output files")
103    parser.add_argument("--flexpart_root_scripts", dest="flexpart_root_scripts",
104                        default=None,
105                        help="FLEXPART root directory (to find grib2flexpart \
106                        and COMMAND file)\n Normally flex_extract resides in \
107                        the scripts directory of the FLEXPART distribution")
108
109    # this is only used by prepare_flexpart.py to rerun a postprocessing step
110    parser.add_argument("--ppid", dest="ppid", default=None,
111                        help="specify parent process id for \
112                        rerun of prepare_flexpart")
113
114    # arguments for job submission to ECMWF, only needed by submit.py
115    parser.add_argument("--job_template", dest='job_template',
116                        default="job.temp",
117                        help="job template file for submission to ECMWF")
118    parser.add_argument("--queue", dest="queue", default=None,
119                        help="queue for submission to ECMWF \
120                        (e.g. ecgate or cca )")
121    parser.add_argument("--controlfile", dest="controlfile",
122                        default='CONTROL.temp',
123                        help="file with CONTROL parameters")
124    parser.add_argument("--debug", dest="debug", default=None,
125                        help="debug mode - leave temporary files intact")
126    parser.add_argument("--request", dest="request", default=None,
127                        help="list all mars request in file mars_requests.dat \
128                        and skip submission to mars")
129
130    args = parser.parse_args()
131
132    return args
133
134def read_ecenv(filename):
135    '''
136    @Description:
137        Reads the file into a dictionary where the key values are the parameter
138        names.
139
140    @Input:
141        filename: string
142            Path to file where the ECMWV environment parameters are stored.
143
144    @Return:
145        envs: dict
146            Contains the environment parameter ecuid, ecgid, gateway
147            and destination for ECMWF server environments.
148    '''
149    envs= {}
150
151    with open(filename, 'r') as f:
152        for line in f:
153            data = line.strip().split()
154            envs[str(data[0])] = str(data[1])
155
156    return envs
157
158def clean_up(c):
159    '''
160    @Description:
161        Remove all files from intermediate directory
162        (inputdir from CONTROL file).
163
164    @Input:
165        c: instance of class ControlFile
166            Contains all the parameters of CONTROL file, which are e.g.:
167            DAY1(start_date), DAY2(end_date), DTIME, MAXSTEP, TYPE, TIME,
168            STEP, CLASS(marsclass), STREAM, NUMBER, EXPVER, GRID, LEFT,
169            LOWER, UPPER, RIGHT, LEVEL, LEVELIST, RESOL, GAUSS, ACCURACY,
170            OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA, SMOOTH, FORMAT,
171            ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS, ECFSDIR,
172            MAILOPS, MAILFAIL, GRIB2FLEXPART, FLEXPARTDIR, BASETIME
173            DATE_CHUNK, DEBUG, INPUTDIR, OUTPUTDIR, FLEXPART_ROOT_SCRIPTS
174
175            For more information about format and content of the parameter
176            see documentation.
177
178    @Return:
179        <nothing>
180    '''
181
182    print("clean_up")
183
184    cleanlist = glob.glob(c.inputdir + "/*")
185    for clist in cleanlist:
186        if c.prefix not in clist:
187            silent_remove(clist)
188        if c.ecapi is False and (c.ectrans == '1' or c.ecstorage == '1'):
189            silent_remove(clist)
190
191    print("Done")
192
193    return
194
195
196def my_error(users, message='ERROR'):
197    '''
198    @Description:
199        Prints a specified error message which can be passed to the function
200        before exiting the program.
201
202    @Input:
203        user: list of strings
204            Contains all email addresses which should be notified.
205            It might also contain just the ecmwf user name which wil trigger
206            mailing to the associated email address for this user.
207
208        message: string, optional
209            Error message. Default value is "ERROR".
210
211    @Return:
212        <nothing>
213    '''
214
215    print(message)
216
217    # comment if user does not want email notification directly from python
218    for user in users:
219        if '${USER}' in user:
220            user = os.getenv('USER')
221        try:
222            p = subprocess.Popen(['mail', '-s flex_extract_v7.1 ERROR',
223                                  os.path.expandvars(user)],
224                                 stdin=subprocess.PIPE,
225                                 stdout=subprocess.PIPE,
226                                 stderr=subprocess.PIPE,
227                                 bufsize=1)
228            trace = '\n'.join(traceback.format_stack())
229            pout = p.communicate(input=message + '\n\n' + trace)[0]
230        except ValueError as e:
231            print('ERROR: ', e)
232            sys.exit('Email could not be sent!')
233        else:
234            print('Email sent to ' + os.path.expandvars(user) + ' ' +
235                  pout.decode())
236
237    sys.exit(1)
238
239    return
240
241
242def normal_exit(users, message='Done!'):
243    '''
244    @Description:
245        Prints a specific exit message which can be passed to the function.
246
247    @Input:
248        user: list of strings
249            Contains all email addresses which should be notified.
250            It might also contain just the ecmwf user name which wil trigger
251            mailing to the associated email address for this user.
252
253        message: string, optional
254            Message for exiting program. Default value is "Done!".
255
256    @Return:
257        <nothing>
258
259    '''
260    print(message)
261
262    # comment if user does not want notification directly from python
263    for user in users:
264        if '${USER}' in user:
265            user = os.getenv('USER')
266        try:
267            p = subprocess.Popen(['mail', '-s flex_extract_v7.1 normal exit',
268                                  os.path.expandvars(user)],
269                                 stdin=subprocess.PIPE,
270                                 stdout=subprocess.PIPE,
271                                 stderr=subprocess.PIPE,
272                                 bufsize=1)
273            pout = p.communicate(input=message+'\n\n')[0]
274        except ValueError as e:
275            print('ERROR: ', e)
276            print('Email could not be sent!')
277        else:
278            print('Email sent to ' + os.path.expandvars(user) + ' ' +
279                  pout.decode())
280
281    return
282
283
284def product(*args, **kwds):
285    '''
286    @Description:
287        This method is taken from an example at the ECMWF wiki website.
288        https://software.ecmwf.int/wiki/display/GRIB/index.py; 2018-03-16
289
290        This method combines the single characters of the passed arguments
291        with each other. So that each character of each argument value
292        will be combined with each character of the other arguments as a tuple.
293
294        Example:
295        product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
296        product(range(2), repeat = 3) --> 000 001 010 011 100 101 110 111
297
298    @Input:
299        *args: tuple
300            Positional arguments (arbitrary number).
301
302        **kwds: dictionary
303            Contains all the keyword arguments from *args.
304
305    @Return:
306        prod: tuple
307            Return will be done with "yield". A tuple of combined arguments.
308            See example in description above.
309    '''
310    pools = map(tuple, args) * kwds.get('repeat', 1)
311    result = [[]]
312    for pool in pools:
313        result = [x + [y] for x in result for y in pool]
314    for prod in result:
315        yield tuple(prod)
316
317    return
318
319
320def silent_remove(filename):
321    '''
322    @Description:
323        Remove file if it exists.
324        The function does not fail if the file does not exist.
325
326    @Input:
327        filename: string
328            The name of the file to be removed without notification.
329
330    @Return:
331        <nothing>
332    '''
333    try:
334        os.remove(filename)
335    except OSError as e:
336        if e.errno != errno.ENOENT:
337            # errno.ENOENT  =  no such file or directory
338            raise  # re-raise exception if a different error occured
339
340    return
341
342
343def init128(filepath):
344    '''
345    @Description:
346        Opens and reads the grib file with table 128 information.
347
348    @Input:
349        filepath: string
350            Path to file of ECMWF grib table number 128.
351
352    @Return:
353        table128: dictionary
354            Contains the ECMWF grib table 128 information.
355            The key is the parameter number and the value is the
356            short name of the parameter.
357    '''
358    table128 = dict()
359    with open(filepath) as f:
360        fdata = f.read().split('\n')
361    for data in fdata:
362        if data[0] != '!':
363            table128[data[0:3]] = data[59:64].strip()
364
365    return table128
366
367
368def to_param_id(pars, table):
369    '''
370    @Description:
371        Transform parameter names to parameter ids
372        with ECMWF grib table 128.
373
374    @Input:
375        pars: string
376            Addpar argument from CONTROL file in the format of
377            parameter names instead of ids. The parameter short
378            names are sepearted with "/" and they are passed as
379            one single string.
380
381        table: dictionary
382            Contains the ECMWF grib table 128 information.
383            The key is the parameter number and the value is the
384            short name of the parameter.
385
386    @Return:
387        ipar: list of integer
388            List of addpar parameters from CONTROL file transformed to
389            parameter ids in the format of integer.
390    '''
391    cpar = pars.upper().split('/')
392    ipar = []
393    for par in cpar:
394        for k, v in table.iteritems():
395            if par == k or par == v:
396                ipar.append(int(k))
397                break
398        else:
399            print('Warning: par ' + par + ' not found in table 128')
400
401    return ipar
402
403def get_list_as_string(list_obj, concatenate_sign=', '):
404    '''
405    @Description:
406        Converts a list of arbitrary content into a single string.
407
408    @Input:
409        list_obj: list
410            A list with arbitrary content.
411
412        concatenate_sign: string, optional
413            A string which is used to concatenate the single
414            list elements. Default value is ", ".
415
416    @Return:
417        str_of_list: string
418            The content of the list as a single string.
419    '''
420
421    str_of_list = concatenate_sign.join(str(l) for l in list_obj)
422
423    return str_of_list
424
425def make_dir(directory):
426    '''
427    @Description:
428        Creates a directory and gives a warning if the directory
429        already exists. The program stops only if there is another problem.
430
431    @Input:
432        directory: string
433            The directory name including the path which should be created.
434
435    @Return:
436        <nothing>
437    '''
438    try:
439        os.makedirs(directory)
440    except OSError as e:
441        if e.errno != errno.EEXIST:
442            # errno.EEXIST = directory already exists
443            raise # re-raise exception if a different error occured
444        else:
445            print('WARNING: Directory {0} already exists!'.format(directory))
446
447    return
448
449def put_file_to_ecserver(ecd, filename, target, ecuid, ecgid):
450    '''
451    @Description:
452        Uses the ecaccess-file-put command to send a file to the ECMWF servers.
453
454        NOTE:
455        The return value is just for testing reasons. It does not have
456        to be used from the calling function since the whole error handling
457        is done in here.
458
459    @Input:
460        ecd: string
461            The path were the file is stored.
462
463        filename: string
464            The name of the file to send to the ECMWF server.
465
466        target: string
467            The target queue where the file should be sent to.
468
469        ecuid: string
470            The user id on ECMWF server.
471
472        ecgid: string
473            The group id on ECMWF server.
474
475    @Return:
476        rcode: string
477            Resulting code of command execution. If successful the string
478            will be empty.
479    '''
480
481    try:
482        rcode = subprocess.check_output(['ecaccess-file-put',
483                                          ecd + '/' + filename,
484                                          target + ':/home/ms/' +
485                                          ecgid + '/' + ecuid +
486                                          '/' + filename],
487                                         stderr=subprocess.STDOUT)
488    except subprocess.CalledProcessError as e:
489        print('... ERROR CODE:\n ... ' + str(e.returncode))
490        print('... ERROR MESSAGE:\n ... ' + str(e))
491
492        print('\n... Do you have a valid ecaccess certification key?')
493        sys.exit('... ECACCESS-FILE-PUT FAILED!')
494
495    return rcode
496
497def submit_job_to_ecserver(target, jobname):
498    '''
499    @Description:
500        Uses ecaccess-job-submit command to submit a job to the ECMWF server.
501
502        NOTE:
503        The return value is just for testing reasons. It does not have
504        to be used from the calling function since the whole error handling
505        is done in here.
506
507    @Input:
508        target: string
509            The target where the file should be sent to, e.g. the queue.
510
511        jobname: string
512            The name of the jobfile to be submitted to the ECMWF server.
513
514    @Return:
515        rcode: string
516            Resulting code of command execution. If successful the string
517            will contain an integer number, representing the id of the job
518            at the ecmwf server.
519    '''
520
521    try:
522        rcode = subprocess.check_output(['ecaccess-job-submit',
523                                         '-queueName', target,
524                                         jobname])
525    except subprocess.CalledProcessError as e:
526        print('... ERROR CODE:\n ... ' + str(e.returncode))
527        print('... ERROR MESSAGE:\n ... ' + str(e))
528
529
530        print('\n... Do you have a valid ecaccess certification key?')
531        sys.exit('... ECACCESS-JOB-SUBMIT FAILED!')
532
533    return rcode
Note: See TracBrowser for help on using the repository browser.
hosted by ZAMG