source: CCCC/trunk/ceda_cc/c4.py @ 204

Subversion URL: http://proj.badc.rl.ac.uk/svn/exarch/CCCC/trunk/ceda_cc/c4.py@204
Revision 204, 20.6 KB checked in by mjuckes, 7 years ago (diff)

updated SPECS specification

Line 
1
2import sys
3
4## callout to summary.py: if this option is selected, imports of libraries are not needed.
5if __name__ == '__main__' and sys.argv[1] == '--sum':
6      import summary
7      summary.main()
8      raise SystemExit(0)
9
10# Standard library imports
11import os, string, time, logging, sys, glob, pkgutil
12import shutil
13## pkgutil is used in file_utils
14# Third party imports
15
16## Local imports with 3rd party dependencies
17#### netcdf --- currently only support for cmds2 -- re-arranged to facilitate support for alternative modules
18
19import file_utils
20
21from file_utils import fileMetadata, ncLib
22
23# Local imports
24import utils_c4 as utils
25import config_c4 as config
26
27reload( utils )
28
29from xceptions import baseException
30
31from fcc_utils2 import tupsort
32
33
34#driving_model_ensemble_member = <CMIP5Ensemble_member>
35#rcm_version_id = <RCMVersionID>                     
36
37class dummy(object):
38   pass
39
40pathTmplDict = { 'CORDEX':'%(project)s/%(product)s/%(domain)s/%(institute)s/%(driving_model)s/%(experiment)s/%(ensemble)s/%(model)s/%(model_version)s/%(frequency)s/%(variable)s/files/%%(version)s/',   \
41                 'SPECS':'%(project)s/%(product)s/%(institute)s/%(model)s/%(experiment)s/%(start_date)s/%(frequency)s/%(realm)s/%(table)s/%(variable)s/%(ensemble)s/files/%%(version)s/', \
42                 'CMIP5':'%(project)s/%(product)s/%(institute)s/%(model)s/%(experiment)s/%(frequency)s/%(realm)s/%(table)s/%(ensemble)s/files/%%(version)s/%(variable)s/', \
43                 '__def__':'%(project)s/%(product)s/%(institute)s/%(model)s/%(experiment)s/%(frequency)s/%(realm)s/%(variable)s/%(ensemble)s/files/%%(version)s/', \
44               }
45
46class recorder(object):
47
48  def __init__(self,project,fileName,type='map',dummy=False):
49    self.dummy = dummy
50    self.file = fileName
51    self.type = type
52    self.pathTmpl = '%(project)s/%(product)s/%(domain)s/%(institute)s/%(driving_model)s/%(experiment)s/%(ensemble)s/%(model)s/%(model_version)s/%(frequency)s/%(variable)s/files/%%(version)s/'
53    self.pathTmpl = pathTmplDict.get(project,pathTmplDict['__def__'])
54    self.records = {}
55    self.tidtupl = []
56
57  def open(self):
58    if self.type == 'map':
59      self.fh = open( self.file, 'a' )
60    else:
61      self.sh = shelve.open( self.file )
62
63  def close(self):
64    if self.type == 'map':
65      self.fh.close()
66    else:
67      self.sh.close()
68
69  def add(self,fpath,drs,safe=True):
70    assert self.type == 'map','Can only do map files at present'
71    assert type(drs) == type( {} ), '2nd user argument to method add should be a dictionary [%s]' % type(drs)
72    tpath = self.pathTmpl % drs
73    if not self.dummy:
74      assert os.path.isfile( fpath ), 'File %s not found' % fpath
75      fdate = time.ctime(os.path.getmtime(fpath))
76      sz = os.stat(fpath).st_size
77    else:
78      fdate = "na"
79      sz = 0
80    record = '%s | OK | %s | modTime = %s | target = %s ' % (fpath,sz,fdate,tpath)
81    fn = string.split( fpath, '/' )[-1]
82    for k in ['creation_date','tracking_id']:
83      if k in drs.keys():
84        record += ' | %s = %s' % (k,drs[k])
85        if k == 'tracking_id':
86          self.tidtupl.append( (fn,drs[k]) )
87
88    self.records[fn] = record
89 
90  def modify(self,fn,msg):
91    assert fn in self.records.keys(),'Attempt to modify non-existent record %s, %s' % [fn,str(self.records.keys()[0:10])]
92    if string.find( self.records[fn], '| OK |') == -1:
93      ##print 'File %s already flagged with errors' % fn
94      return
95    s = string.replace( self.records[fn], '| OK |', '| %s |' % msg )
96    ##print '--> ',s
97    self.records[fn] = s
98
99  def checktids(self):
100## sort by tracking id
101    self.tidtupl.sort( cmp=tupsort(k=1).cmp )
102    nd = 0
103    fnl = []
104    for k in range(len(self.tidtupl)-1):
105      if self.tidtupl[k][1] == self.tidtupl[k+1][1]:
106        print 'Duplicate tracking_id: %s, %s:: %s' % (self.tidtupl[k][0],self.tidtupl[k+1][0],self.tidtupl[k][1])
107        nd += 1
108        if len(fnl) == 0 or fnl[-1] != self.tidtupl[k][0]:
109          fnl.append( self.tidtupl[k][0])
110        fnl.append( self.tidtupl[k+1][0])
111    if nd == 0:
112      print 'No duplicate tracking ids found in %s files' % len(self.tidtupl)
113    else:
114      print '%s duplicate tracking ids' % nd
115      for f in fnl:
116        self.modify( f, 'ERROR: duplicate tid' )
117
118  def dumpAll(self,safe=True):
119    keys = self.records.keys()
120    keys.sort()
121    for k in keys:
122      self.dump( self.records[k], safe=safe )
123
124  def dump( self, record, safe=True ):
125    if safe:
126      self.open()
127    self.fh.write( record + '\n' )
128    if safe:
129      self.close()
130
131  def addErr(self,fpath,reason,safe=True):
132    record = '%s | %s' % (fpath, reason)
133    fn = string.split( fpath, '/' )[-1]
134    self.records[fn] = record
135
136class checker(object):
137  def __init__(self, pcfg, cls,reader,abortMessageCount=-1):
138    self.info = dummy()
139    self.info.pcfg = pcfg
140    self.info.abortMessageCount = abortMessageCount
141    self.calendar = 'None'
142    self.ncReader = reader
143    self.cfn = utils.checkFileName( parent=self.info,cls=cls)
144    self.cga = utils.checkGlobalAttributes( parent=self.info,cls=cls)
145    self.cgd = utils.checkStandardDims( parent=self.info,cls=cls)
146    self.cgg = utils.checkGrids( parent=self.info,cls=cls)
147    self.cls = cls
148
149    # Define vocabs based on project
150    ##self.vocabs = getVocabs(pcgf)
151    self.vocabs = pcfg.vocabs
152
153  def checkFile(self,fpath,log=None,attributeMappings=[]):
154    self.calendar = 'None'
155    self.info.log = log
156
157    fn = string.split( fpath, '/' )[-1]
158
159    if attributeMappings != []:
160      self.ncReader.loadNc( fpath )
161      self.ncReader.applyMap( attributeMappings, self.cfn.globalAttributesInFn, log=log )
162      ncRed = True
163      thisFn = self.ncReader.fn
164    else:
165      ncRed = False
166      thisFn = fn
167
168    self.cfn.check( thisFn )
169    if not self.cfn.completed:
170      self.completed = False
171      return
172    if not self.info.pcfg.project[:2] == '__':
173      if not os.path.isfile( fpath ):
174        print 'File %s not found [2]' % fpath
175        self.completed = False
176        return
177
178    if not ncRed:
179      ##print fpath
180      self.ncReader.loadNc( fpath )
181    self.ga = self.ncReader.ga
182    self.va = self.ncReader.va
183    self.da = self.ncReader.da
184
185    if self.cfn.freq != None:
186      vGroup = self.cfn.freq
187    else:
188      vGroup = self.info.pcfg.mipVocabVgmap.get(self.cfn.group,self.cfn.group)
189    self.cga.check( self.ga, self.va, self.cfn.var, vGroup, self.vocabs, self.cfn.fnParts )
190    if not self.cga.completed:
191      self.completed = False
192      return
193
194    ##self.cgd.plevRequired = config.plevRequired
195    ##self.cgd.plevValues = config.plevValues
196    ##self.cgd.heightRequired = config.heightRequired
197    ##self.cgd.heightValues = config.heightValues
198    ##self.cgd.heightRange = config.heightRange
199    self.cgd.check( self.cfn.var, self.cfn.freq, self.da, self.va, self.cga.isInstantaneous )
200    self.calendar = self.cgd.calendar
201    if not self.cgd.completed:
202      self.completed = False
203      return
204
205    if self.info.pcfg.doCheckGrids:
206      ##self.cgg.rotatedPoleGrids = config.rotatedPoleGrids
207      ##self.cgg.interpolatedGrids = config.interpolatedGrids
208      self.cgg.check( self.cfn.var, self.cfn.domain, self.da, self.va )
209   
210      if not self.cgg.completed:
211        self.completed = False
212        return
213    self.completed = True
214    self.drs = self.cga.getDrs()
215    self.drs['project'] = self.info.pcfg.project
216    self.errorCount = self.cfn.errorCount + self.cga.errorCount + self.cgd.errorCount + self.cgg.errorCount
217
218class c4_init(object):
219
220  def __init__(self,args=None):
221    self.logByFile = True
222    self.policyFileLogfileMode = 'w'
223    self.policyBatchLogfileMode = 'np'
224    if args==None:
225       args = sys.argv[1:]
226    nn = 0
227
228    self.attributeMappingFile = None
229    self.recordFile = 'Rec.txt'
230    self.logDir = 'logs_02'
231    self.errs = []
232   
233    # Set default project to "CORDEX"
234    self.project = "CORDEX"
235    self.holdExceptions = False
236    forceLogOrg = None
237    argsIn = args[:]
238
239    # The --copy-config option must be the first argument if it is present.
240    if args[0] == '--copy-config':
241       if len(args) < 2:
242         self.commandHints( argsIn )
243       args.pop(0)
244       dest_dir = args.pop(0)
245       config.copy_config(dest_dir)
246       print 'Configuration directory copied to %s.  Set CC_CONFIG_DIR to use this configuration.' % dest_dir
247       print
248       raise SystemExit(0)
249    elif args[0] == '-h':
250       print 'Help command not implemented yet'
251       raise SystemExit(0)
252
253    self.summarymode = args[0] == '--sum'
254    if self.summarymode:
255      return
256
257    argu = []
258    while len(args) > 0:
259      next = args.pop(0)
260      if next == '-f':
261        flist = [args.pop(0),]
262        self.logByFile = False
263      elif next == '--log':
264        x = args.pop(0)
265        assert x in ['single','multi','s','m'], 'unrecognised logging option (--log): %s' % (x)
266        if x in ['multi','m']:
267           forceLogOrg = 'multi'
268        elif x in ['single','s']:
269           forceLogOrg = 'single'
270      elif next == '--flfmode':
271        lfmk = args.pop(0)
272        assert lfmk in ['a','n','np','w','wo'], 'Unrecognised file logfile mode (--flfmode): %s' % lfmk
273        self.policyFileLogfileMode = lfmk
274      elif next == '--blfmode':
275        lfmk = args.pop(0)
276        assert lfmk in ['a','n','np','w','wo'], 'Unrecognised batch logfile mode (--blfmode): %s' % lfmk
277        self.policyBatchLogfileMode = lfmk
278      elif next == '-d':
279        fdir = args.pop(0)
280        flist = glob.glob( '%s/*.nc' % fdir  )
281      elif next == '-D':
282        flist  = []
283        fdir = args.pop(0)
284        for root, dirs, files in os.walk( fdir, followlinks=True ):
285          for f in files:
286            fpath = '%s/%s' % (root,f)
287            if (os.path.isfile( fpath ) or os.path.islink( fpath )) and f[-3:] == '.nc':
288              flist.append( fpath )
289      elif next == '-R':
290        self.recordFile = args.pop(0)
291      elif next == '--ld':
292        self.logDir = args.pop(0)
293      elif next in ['--catchAllExceptions','--cae']:
294        self.holdExceptions = True
295      elif next == '--aMap':
296        self.attributeMappingFile = args.pop(0)
297        assert os.path.isfile( self.attributeMappingFile ), 'The token "--aMap" should be followed by the path or name of a file'
298      elif next == "-p":
299        self.project = args.pop(0)
300      else:
301       print 'Unused argument: %s' % next
302       argu.append( next )
303       nn+=1
304    if nn != 0:
305      print 'Unused arguments: ', argu
306      self.commandHints( argsIn )
307
308    if self.project == 'CMIP5':
309      fl0 = []
310      for f in flist:
311        if string.find( f, '/latest/' ) != -1:
312          fl0.append(f)
313      flist = fl0
314
315    if forceLogOrg != None:
316      if forceLogOrg == 'single':
317        self.logByFile = False
318      else:
319        self.logByFile = True
320
321    if self.project[:2] == '__':
322       flist = []
323       ss = 'abcdefgijk'
324       ss = 'abcdefgijklmnopqrstuvwxyz'
325       ss = 'abc'
326       for i in range(10):
327         v = 'v%s' % i
328         for a in ss:
329           for b in ss:
330             flist.append( '%s_day_%s_%s_1900-1909.nc' % (v,a,b) )
331    flist.sort()
332    fnl = []
333    for f in flist:
334      fn = string.split(f, '/')[-1]
335      fnl.append(fn)
336    nd = 0
337    dupl = []
338    for k in range(len(fnl)-1):
339      if fnl[k] == fnl[k-1]:
340        nd += 1
341        dupl.append( fnl[k] )
342    self.dupDict = {}
343    for f in dupl:
344      self.dupDict[f] = 0
345    if nd != 0:
346      self.errs.append( 'Duplicate file names encountered: %s' % nd )
347      self.errs.append( dupl )
348    self.flist = flist
349    self.fnl = fnl
350    if not os.path.isdir(   self.logDir ):
351       os.mkdir(   self.logDir )
352
353    tstring1 = '%4.4i%2.2i%2.2i_%2.2i%2.2i%2.2i' % time.gmtime()[0:6]
354    self.batchLogfile = '%s/qcBatchLog_%s.txt' % (  self.logDir,tstring1)
355## default appending to myapp.log; mode='w' forces a new file (deleting old contents).
356    self.logger = logging.getLogger('c4logger')
357    if self.policyBatchLogfileMode in ['n','np']:
358        assert not os.path.isfile( self.batchLogfile ), '%s exists and policy set to new file' % self.batchLogfile
359    m = self.policyBatchLogfileMode[0]
360    if m == 'n':
361      m = 'w'
362    if m == 'a':
363      self.hdlr = logging.FileHandler(self.batchLogfile)
364    else:
365      self.hdlr = logging.FileHandler(self.batchLogfile,mode=m)
366    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
367    self.hdlr.setFormatter(formatter)
368    self.logger.setLevel(logging.INFO)
369    self.logger.addHandler(self.hdlr)
370
371    self.attributeMappings = []
372    self.attributeMappingsLog = None
373    if self.attributeMappingFile != None:
374      for l in open( self.attributeMappingFile ).readlines():
375        if l[0] != '#':
376          bb = string.split( string.strip(l), '|' ) 
377          assert len(bb) ==2, "Error in experimental module attributeMapping -- configuration line not scanned [%s]" % str(l)
378          bits = string.split( bb[0], ';' )
379          cl = []
380          for b in bits:
381            cl.append( string.split(b, '=' ) )
382          self.attributeMappings.append( ('am001',cl, string.split(bb[1],'=') ) )
383      self.attributeMappingsLog = open( 'attributeMappingsLog.txt', 'w' )
384
385  def commandHints(self, args):
386    if args[0] in ['-h','--sum']:
387      print 'Arguments look OK'
388    elif args[0] == '--copy-config':
389      print 'Usage [configuration copy]: ceda_cc --copy-config <target directory path>'
390    else:
391      if not( '-f' in args or '-d' in args or '-D' in args):
392        print 'No file or target directory specified'
393        print """USAGE:
394ceda_cc -p <project> [-f <NetCDF file>|-d <directory containing files>|-D <root of directory tree>] [other options]
395
396With the "-D" option, all files in the directory tree beneath the given diretory will be checked. With the "-d" option, only files in the given directory will be checked.
397"""
398    raise SystemExit(0)
399   
400
401  def getFileLog( self, fn, flf=None ):
402    if flf == None:
403      tstring2 = '%4.4i%2.2i%2.2i' % time.gmtime()[0:3]
404      if fn in self.dupDict.keys():
405        tag = '__%2.2i' % self.dupDict[fn]
406        self.dupDict[fn] += 1
407      else:
408        tag = ''
409      self.fileLogfile = '%s/%s%s__qclog_%s.txt' % (self.logDir,fn[:-3],tag,tstring2)
410      if self.policyFileLogfileMode in ['n','np']:
411        assert not os.path.isfile( self.fileLogfile ), '%s exists and policy set to new file' % self.fileLogfile
412      m = self.policyFileLogfileMode[0]
413      if m == 'n':
414        m = 'w'
415    else:
416      m = 'a'
417      self.fileLogfile = flf
418
419    self.fLogger = logging.getLogger('fileLog_%s_%s' % (fn,m))
420    if m == 'a':
421      self.fHdlr = logging.FileHandler(self.fileLogfile)
422    else:
423      self.fHdlr = logging.FileHandler(self.fileLogfile,mode=m)
424    fileFormatter = logging.Formatter('%(message)s')
425    self.fHdlr.setFormatter(fileFormatter)
426    self.fLogger.addHandler(self.fHdlr)
427    self.fLogger.setLevel(logging.INFO)
428    return self.fLogger
429
430  def closeFileLog(self):
431    self.fLogger.removeHandler(self.fHdlr)
432    self.fHdlr.close()
433    if self.policyFileLogfileMode in ['wo','np']:
434      os.popen( 'chmod %s %s;' % (444, self.fileLogfile) )
435
436  def closeBatchLog(self):
437    self.logger.removeHandler(self.hdlr)
438    self.hdlr.close()
439    if self.policyBatchLogfileMode in ['wo','np']:
440      os.popen( 'chmod %s %s;' % (444, self.batchLogfile) )
441
442
443class main(object):
444
445  def __init__(self,args=None,abortMessageCount=-1,printInfo=False,monitorFileHandles = False):
446    logDict = {}
447    ecount = 0
448    c4i = c4_init(args=args)
449     
450    isDummy  = c4i.project[:2] == '__'
451    if (ncLib == None) and (not isDummy):
452       raise baseException( 'Cannot proceed with non-dummy [%s] project without a netcdf API' % (c4i.project) )
453    pcfg = config.projectConfig( c4i.project )
454    ncReader = fileMetadata(dummy=isDummy, attributeMappingsLog=c4i.attributeMappingsLog)
455    self.cc = checker(pcfg, c4i.project, ncReader,abortMessageCount=abortMessageCount)
456    rec = recorder( c4i.project, c4i.recordFile, dummy=isDummy )
457    if monitorFileHandles:
458      self.monitor = utils.sysMonitor()
459    else:
460      self.monitor = None
461
462    cal = None
463    c4i.logger.info( 'Starting batch -- number of file: %s' % (len(c4i.flist)) )
464    if len( c4i.errs ) > 0:
465      for i in range(0,len( c4i.errs ), 2 ):
466        c4i.logger.info( c4i.errs[i] )
467 
468    self.cc.info.amapListDraft = []
469    cbv = utils.checkByVar( parent=self.cc.info,cls=c4i.project,monitor=self.monitor)
470    cbv.impt( c4i.flist )
471    if printInfo:
472      print cbv.info
473
474    fileLogOpen = False
475    self.resList =  []
476    stdoutsum = 2000
477    npass = 0
478    kf = 0
479    for f in c4i.flist:
480      kf += 1
481      rv = False
482      ec = None
483      if monitorFileHandles:
484        nofhStart = self.monitor.get_open_fds()
485      fn = string.split(f,'/')[-1]
486      c4i.logger.info( 'Starting: %s' % fn )
487      try:
488  ### need to have a unique name, otherwise get mixing of logs despite close statement below.
489  ### if duplicate file names are present, this will be recorded in the main log, tag appended to file level log name (not yet tested).
490        if c4i.logByFile:
491          fLogger = c4i.getFileLog( fn )
492          logDict[fn] = c4i.fileLogfile
493          c4i.logger.info( 'Log file: %s' % c4i.fileLogfile )
494          fileLogOpen = True
495        else:
496          fLogger = c4i.logger
497 
498        fLogger.info( 'Starting file %s' % fn )
499## default appending to myapp.log; mode='w' forces a new file (deleting old contents).
500        self.cc.checkFile( f, log=fLogger,attributeMappings=c4i.attributeMappings )
501
502        if self.cc.completed:
503          if cal not in (None, 'None') and self.cc.cgd.varGroup != "fx":
504            if cal != self.cc.calendar:
505              cal_change_err_msg = 'Error: change in calendar attribute %s --> %s' % (cal, self.cc.calendar)
506              c4i.logger.info(cal_change_err_msg)
507              fLogger.info(cal_change_err_msg)
508              self.cc.errorCount += 1
509
510          cal = self.cc.calendar
511          ec = self.cc.errorCount
512        rv =  ec == 0
513        if rv:
514          npass += 1
515        self.resList.append( (rv,ec) )
516
517        if c4i.logByFile:
518          if self.cc.completed:
519            fLogger.info( 'Done -- error count %s' % self.cc.errorCount )
520          else:
521            fLogger.info( 'Done -- checks not completed' )
522          c4i.closeFileLog( )
523          fileLogOpen = False
524
525        if self.cc.completed:
526          c4i.logger.info( 'Done -- error count %s' % self.cc.errorCount ) 
527          ecount += self.cc.errorCount
528          if self.cc.errorCount == 0:
529            rec.add( f, self.cc.drs )
530          else:
531            rec.addErr( f, 'ERRORS FOUND | errorCount = %s' % self.cc.errorCount )
532        else:
533          ecount += 20
534          c4i.logger.info( 'Done -- testing aborted because of severity of errors' )
535          rec.addErr( f, 'ERRORS FOUND AND CHECKS ABORTED' )
536      except:
537        c4i.logger.error("Exception has occured" ,exc_info=1)
538        if fileLogOpen:
539          fLogger.error("C4.100.001: [exception]: FAILED:: Exception has occured" ,exc_info=1)
540          c4i.closeFileLog( )
541          fileLogOpen = False
542        rec.addErr( f, 'ERROR: Exception' )
543        if not c4i.holdExceptions:
544          raise
545      if stdoutsum > 0 and kf%stdoutsum == 0:
546         print '%s files checked; %s passed this round' % (kf,npass)
547      if monitorFileHandles:
548        nofhEnd = self.monitor.get_open_fds()
549        if nofhEnd > nofhStart:
550           print 'Open file handles: %s --- %s' % (nofhStart, nofhEnd)
551 
552    self.cc.info.log = c4i.logger
553   
554    if c4i.project not in ['SPECS','CCMI','CMIP5']:
555       cbv.c4i = c4i
556       cbv.setLogDict( logDict )
557       cbv.check( recorder=rec, calendar=self.cc.calendar)
558       try:
559         ecount += cbv.errorCount
560       except:
561         ecount = None
562    ncReader.close()
563    if type( self.cc.info.amapListDraft ) == type( [] ) and len(  self.cc.info.amapListDraft ) > 0:
564      ll =  self.cc.info.amapListDraft
565      ll.sort()
566      oo = open( 'amapDraft.txt', 'w' )
567      oo.write( ll[0] + '\n' )
568      for i in range( 1,len(ll) ):
569        if ll[i] != ll[i-1]:
570          oo.write( ll[i] + '\n' )
571      oo.close()
572    if c4i.project in ['SPECS','CCMI','CMIP5']:
573      rec.checktids()
574    rec.dumpAll()
575    if printInfo:
576      print 'Error count %s' % ecount
577    ##c4i.hdlr.close()
578    c4i.closeBatchLog()
579    self.ok = all( map( lambda x: x[0], self.resList ) )
580
581
582   
583
584
585def main_entry():
586   """
587   Wrapper around main() for use with setuptools.
588
589   """
590   main(printInfo=True)
591
592if __name__ == '__main__':
593  if sys.argv[1] == '--sum':
594      import summary
595      summary.main()
596      raise SystemExit(0)
597  main_entry()
598
599
600##else:
601  ##f1 = '/data/u10/cordex/AFR-44/SMHI/ECMWF-ERAINT/evaluation/SMHI-RCA4/v1/day/clh/clh_AFR-44_ECMWF-ERAINT_evaluation_r1i1p1_SMHI-RCA4_v1_day_19810101-19851231.nc'
602  ##f2 = '/data/u10/cordex/AFR-44/SMHI/ECMWF-ERAINT/evaluation/SMHI-RCA4/v1/sem/tas/tas_AFR-44_ECMWF-ERAINT_evaluation_r1i1p1_SMHI-RCA4_v1_sem_200012-201011.nc'
603  ##f3 = '/data/u10/cordex/AFR-44i/SMHI/ECMWF-ERAINT/evaluation/SMHI-RCA4/v1/mon/tas/tas_AFR-44i_ECMWF-ERAINT_evaluation_r1i1p1_SMHI-RCA4_v1_mon_199101-200012.nc'
604  ##cc.checkFile( f3 )
Note: See TracBrowser for help on using the repository browser.