source: flex_extract.git/source/python/mods/tools.py @ ca867de

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

refactored functions in EcFlexpart? and did some minor changes

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