source: CMIP6dreqbuild/trunk/src/framework/ingest/util_checkUpd.py @ 884

Subversion URL: http://proj.badc.rl.ac.uk/svn/exarch/CMIP6dreqbuild/trunk/src/framework/ingest/util_checkUpd.py@884
Revision 884, 40.1 KB checked in by mjuckes, 3 years ago (diff)

added key VIACSAB integration code

Line 
1##
2## this will check for changes cleanly ... need a better view of what dreq_consol_tables is doing before this can be used.
3##
4
5from xceptions import baseException
6import os, stat, shelve, uuid, collections
7import dreq_cfg
8import dreq_utils
9import util_varGroups
10from utils_wb import workbook, uniCleanFunc
11import utils
12import util_gen
13omitThese = {}
14omitThese['CMIP5'] = ['aero','Omon','Oyr','OImon','cfMon','cfDay','cfSites','cf3hr','cfOff']
15
16targTabs = ['CMIP5_cfOff', 'CMIP5_cfMon', 'CMIP5_6hrPlev', 'CMIP5_day', 'OMIP.Oyr', 'CMIP5_cfSites', 'OMIP.Omon', 'CMIP5_aero', 'CMIP5_LImon', 'CMIP5_Amon', 'CMIP5_Oclim', 'CMIP5_6hrLev', 'CMIP5_Lmon', 'CMIP5_3hr', 'CMIP5_fx', 'CMIP5_cfDay', 'CMIP5_cf3hr', 'OMIP.fx', 'OMIP.day']
17targTabs = ['CFMIP.cfOff', 'CFMIP.cfMon', 'CMIP5_6hrPlev', 'CMIP5_day', 'OMIP.Oyr', 'CFMIP.cfSites', 'OMIP.Omon', 'CMIP5_aero', 'CMIP5_LImon', 'CMIP5_Amon', 'CMIP5_Oclim', 'CMIP5_6hrLev', 'CMIP5_Lmon', 'CMIP5_3hr', 'CMIP5_fx', 'CFMIP.cfDay', 'CFMIP.cf3hr', 'OMIP.fx', 'OMIP.day']
18targTabs = ['CFMIP.cfOff', 'CFMIP.cfMon', 'CMIP5_6hrPlev', 'CMIP5_day', 'OMIP.Oyr', 'CFMIP.cfSites', 'OMIP.Omon', 'CMIP5_aero', 'CMIP5_Amon', 'CMIP5_Oclim', 'CMIP5_6hrLev', 'CMIP5_Lmon', 'CMIP5_3hr', 'CMIP5_fx', 'CFMIP.cfDay', 'CFMIP.cf3hr', 'OMIP.fx', 'OMIP.day']
19
20nt__grphd = collections.namedtuple( 'grphd', ['withPriority','thisl', 'iv', 'it','start','tv'] )
21nt__grptbl = collections.namedtuple( 'grptbl', ['grp','var','srcTable','freq','description','shape','levels','timeProc','mask','priority','mip','id'] )
22
23class omipGsScan(object):
24  hphys = ['priority', 'long_name', 'units', 'comment', 'questions & notes', 'output variable name', 'CF standard_name', 'CF standard name AP', 'Standard name status AP', 'Comments AP', 'unconfirmed or proposed standard name', 'unformatted units', 'Units AP', 'cell_methods', 'valid min', 'valid max', 'mean absolute min', 'mean absolute max', 'positive', 'type', 'CMOR dimensions', 'CMOR variable name', 'realm', 'frequency', 'cell_measures', 'flag_values', 'flag_meanings']
25  hphysx = ['CF standard name AP', 'Standard name status AP', 'Comments AP','Units AP']
26  def __init__(self):
27    self.hdgs = []
28    self.extr = []
29    for x in self.hphys:
30      if x not in self.hphysx:
31        self.hdgs.append( x )
32    self.mode = None
33    self.data = None
34    self.icf = 6
35    self.iu = 8
36
37  def parseh(self,ih):
38    if len(ih) == len(self.hdgs):
39      self.mode, self.data = (0,None)
40      log.info( 'INFO.omip.00010: mode 0: %s' % str(ih) )
41      return (0,None)
42    if all( [ih[x] == '' for x in range(8)] ):
43      return (0,None)
44    if len(ih) == len(self.hphys):
45      if all( [self.hphys[i] == ih[i] for i in range( len(ih) )] ):
46        ix = [ih.index(x) for x in self.hdgs]
47        cfrule = [ ih.index(x) for x in ['CF standard name AP','CF standard_name'] ]
48        unrule =  [ ih.index(x) for x in ['Units AP','unformatted units'] ]
49        self.mode, self.data = (1,(ix,cfrule,unrule) )
50        log.info( 'INFO.omip.00011: mode 1: %s' % str(ih) )
51        return (1,(ix,cfrule,unrule) )
52      else:
53        print 'ERROR: unable to process: ',ih
54        assert False, 'Should not be here'
55    else:
56      log.info( 'WARN.omip.00021: Bad header length: %s' % str(ih) )
57      return (0,None)
58
59  def filter(self,ir):
60    assert self.mode != None, 'Not initialised (with parseh)'
61    if self.mode == 0:
62      return ir
63    if self.mode == 1:
64      orec = [ir[x] for x in self.data[0] ]
65      for x in self.data[1]:
66        if ir[x] != '':
67          orec[self.icf] = ir[x]
68          break
69      for x in self.data[2]:
70        if ir[x] != '':
71          orec[self.iu] = ir[x]
72          break
73      return orec
74     
75         
76     
77
78class snChange(object):
79  def __init__(self):
80    fn = 'ingest/standardNameChanges.xls'
81    wb = workbook( fn )
82    s1 = wb.book.sheet_by_name(u'Sheet1')
83    self.map = collections.defaultdict(dict)
84    self.omit = collections.defaultdict(set)
85    self.trigger = collections.defaultdict(dict)
86    self.ng = collections.defaultdict(set)
87    for i in range( s1.nrows ):
88      rr = [x.value.strip() for x in s1.row(i)]
89      tab = rr[0]
90      sn0 = rr[3]
91      var = rr[2]
92      if rr[1] == '':
93        sn1 = rr[4]
94        if sn1 == 'xxx':
95          self.omit[tab].add(sn0)
96        elif sn1[0] == '@':
97          self.trigger[tab][sn0] = sn1[1:]
98        else:
99          self.map[tab][sn0] = rr[4:]
100      else:
101        ng = rr[1]
102        self.ng[(tab,ng)].add(sn0)
103
104    for t in self.trigger:
105      for s in self.trigger[t]:
106        assert (t,self.trigger[t][s]) in self.ng, 'snChange: Inconsistent directives in %s' % fn
107
108
109class maps(object):
110  ttranstem  = {'icesheetmon':'LImon','icesheetyear':'LIyr','icesheetfx':'LIfx'}
111
112logFarm = utils.dreqLog('logs')
113log = logFarm.getLog( 'soUpd' )
114
115def getCmip5Sns():
116    ##fname = '/data/work/documents/CMIP5_standard_output.xls'
117    fname = 'ingest/CMIP5_standard_output.xls'
118    wb = workbook( fname )
119    wb.sns.sort()
120    omit1 = [u'dims', u'general', u'other output',u'CFMIP output',u'Odecxx']
121    sns = []
122    for s in wb.sns:
123      if s not in omit1:
124        sns.append(s)
125
126    return sns
127
128def rval(x):
129  if x.ctype == 1:
130    return x.value.strip()
131  else:
132    return x.value
133
134def cleanStr(x):
135  if x.ctype == 1:
136    return uniCleanFunc(x.value).strip()
137  else:
138    return str(x.value)
139
140def getRowValues( ll, minLen=0, maxLen=0):
141  oo = []
142  for i in ll:
143    oo.append( i.value )
144  if len(oo) >= minLen:
145    return oo[:minLen]
146  for i in range(minLen+1):
147    if len(oo) == minLen:
148      return oo
149    oo.append( '' )
150  if maxLen > 0:
151    return oo[:maxLen]
152  return oo
153
154class templateStat(object):
155  ibase = '/home/martin/2014/wip/dreq/input'
156  def __init__(self,sdir='inSh'):
157    cfg = dreq_cfg.rqcfg()
158    ##cfg.ff['CMIP5'] = ['/data/work/documents/CMIP5_standard_output.xls']
159    cfg.ff['CMIP5'] = ['CMIP5_standard_output.xls']
160    self.changed = []
161    self.new = []
162
163    k2 = cfg.ff.keys()
164    self.sh = shelve.open( '%s/sh__templateStat' % (sdir) )
165    for k in sorted( cfg.ee.keys() ):
166      self.verify( k,[cfg.ee[k],] )
167    for k in sorted( cfg.ff.keys() ):
168      self.verify( k,cfg.ff[k] )
169    self.sh.close()
170
171  def verify( self, k, fl ):
172    for f in fl:
173      fpath = '%s/%s/%s' % (self.ibase,k,f)
174      if not os.path.isfile( fpath ):
175        self.msg( 'ERROR.001.0001: file not found: %s' % fpath )
176        if fpath in self.sh:
177          self.msg( 'ERROR.001.0002: file lost or moved: %s' % fpath )
178      else:
179        stt = os.stat( fpath )
180        self.ctime = stt[stat.ST_CTIME]
181        self.fsize = stt[stat.ST_SIZE]
182        if fpath in self.sh:
183          fhist = self.sh[fpath]
184          if fhist[-1] != (self.ctime,self.fsize):
185            self.msg( 'INFO.001.0002: CHANGED: %s: %s (%s)' % (fpath,self.ctime,self.fsize) )
186            fhist.append( (self.ctime,self.fsize) )
187            self.sh[fpath] = fhist
188            self.changed.append( (k,fpath,self.ctime,self.fsize) )
189          else:
190            self.msg( 'INFO.001.0001: SAME: %s: %s (%s)' % (fpath,self.ctime,self.fsize) )
191        else:
192          self.msg( 'INFO.001.0003: NEW: %s: %s (%s)' % (fpath,self.ctime,self.fsize) )
193          self.new.append( (k,fpath,self.ctime,self.fsize) )
194          self.sh[fpath] = [(self.ctime,self.fsize)]
195
196  def msg(self, txt ):
197    print txt
198
199class varGroups(object):
200  rq = dreq_cfg.rqcfg()
201  freqmap = {'daily':'day', 'Annual':'yr', 'Timestep':'subhr',  '1day':'day', '1mon':'mon', 
202             'month':'mon', 'year':'yr', 'monthly':'mon', 'Day':'day', '6h':'6hr', 
203             '3 hourly':'3hr', '3 Hourly':'3hr'  }
204
205  def __init__(self):
206
207    omit = ['ALL VARIABLES', 'Objectives','Experiments','Experiment Groups','Request scoping','New variables','__lists__','aerchemmip-changelog']
208    self.cmip5sns = getCmip5Sns()
209    keys = sorted( self.rq.ee.keys() )
210    vdate = '20160309'
211##
212## this file has reference uids ... should be redundant ....
213##
214    vkdir = '/data/tmp/svn3/exarch/CMIP6dreqbuild/trunk/srcMisc'
215    self.vark = dreq_utils.varKeys( '%s/dreq_consol_tables_shelve_v%s' % (vkdir,vdate))
216    ee = {}
217    self.nvd = {}
218    self.idx = 0
219    self.shCols = ['Short name of group', 'Variable short name', 'Table', 'Frequency', 'Description extension (optional)', 'Shape', 'Levels', 'Time mean, point or climatology', 'Mask (optional)', 'Priority', 'MIP','uid','Prev. Var Name']
220##
221## array for information and debugging
222##
223    for mip in keys:
224        self.mip = mip
225        self.actions = collections.defaultdict( int )
226        self.shInfo = {'prov': '%s request template' % mip, 'label': mip, 'title': '%s request'}
227        fn = self.rq.ee[mip]
228        path = '%s%s/%s' % (self.rq.dir0,mip,fn)
229        self.path = path
230        stt = os.stat( path )
231        self.ctime = stt[stat.ST_CTIME]
232        self.fsize = stt[stat.ST_SIZE]
233        wb = workbook( path )
234        ss = []
235        for s in wb.sns:
236          if s not in omit:
237            ss.append(s)
238        self.sh = shelve.open( 'inSh/sh__grp_%s' % mip, 'n' )
239        self.sh['__cols__'] = self.shCols
240        self.sh['__info__'] = self.shInfo
241        self.sh['__src__'] = {'path':path, 'size':self.fsize, 'time':self.ctime}
242        ghr = None
243        for s in ss:
244          if s[:5] != 'CMIP5':
245           sh = wb.book.sheet_by_name( s )
246##
247## parse headers of sheet into named tuple "gh"
248##
249           self.gh = self.parseGrpHead(sh,path,mip,s)
250           assert ghr == None or ghr[:5] == self.gh[:5], 'Multiple sheet structures in single workbook .. NOT SUPPORTED: %s\n%s\n%s' % (path,self.gh,ghr)
251           ghr = self.gh
252           self.scanSheet( sh, self.gh, path, mip, s )
253        ee = {}
254        for k in ['withPriority', 'thisl', 'iv', 'it', 'start']:
255          ee = self.gh.__dict__[k]
256        self.sh['__recStr__'] =  ee
257        self.sh.close()
258
259        self.sh = shelve.open( 'inSh/sh__newVar_%s' % mip, 'n' )
260        self.sh['__cols__'] = ['Short name', 'CF standard_name', 'standard name status', 'Native grid', 'units', 'Long Name', 'description/comments', 'Priority', 'associated observational dataset']
261        if 'New variables' in wb.sns:
262          sh = wb.book.sheet_by_name( 'New variables' )
263          self.scanVars(sh)
264
265        self.sh.close()
266
267        for k in sorted( self.actions.keys() ):
268          print 'ACTIONS [%s]: %s (%s)' % (mip,k,self.actions[k])
269
270  def scanVars( self, sh ):
271     rh = [str(x.value) for x in sh.row(2)]
272     mode = 'nominal'
273
274     lumip_variant = ['Short name', 'CF proposed standard name for variable separated by land use type', 'CF standard_name', 'standard name status', 'Native grid', 'units', 'Long Name', 'description/comments', 'Priority', 'associated observational dataset']
275     if len(rh) >=10 and rh[:10] == lumip_variant:
276       mode = 'lumip'
277     elif len(rh) < 9 or rh[:9] != self.sh['__cols__']:
278        print 'ERROR .. change in column headings %s:\n ********** %s\n +++++++++++ %s' % (self.path,str(rh),str( self.sh['__cols__']) )
279
280     for i in range(3,sh.nrows):
281        r = sh.row(i)
282        if r[0].value == "**end**":
283          break
284
285        v = str(r[0].value)
286        l = r[4].value
287        novar = v == '' and l == ''
288        if not novar:
289          rr = [x.value for x in r]
290          assert v not in self.sh, 'duplicate variable definition %s in %s .. %s' % (v,self.path, str(r))
291          nbl = 0
292          for x in rr:
293            if x != '':
294              nbl += 1
295          if nbl < 3:
296            self.actions['Skipping mainly blank new variable record'] += 1
297          else:
298            if v.find('_') != -1:
299              v = v.replace( '_', '')
300              self.actions['Replacing underscore in new variable name'] += 1
301            if mode == 'lumip':
302              rr = rr[0:1] + rr[2:]
303              self.actions['LUMIP new variable adjustment'] += 1
304            self.sh[v] = rr
305             
306  def scanSheet(self, sh, gh, path, k, s ):
307       """Skips efforts to find a priority which were incorporated into the scan in dreq_consol_tables"""
308       irsh = 5
309       cc = collections.defaultdict( list )
310       for i in range(gh.start,sh.nrows):
311         rowIndex = i
312         thisr = sh.row(i)
313         v0 = str( thisr[0].value ) + '__'
314         if v0[0] != '#':
315           lll = getRowValues( thisr, minLen=gh.thisl, maxLen=gh.thisl )
316           if gh.iv == 0:
317                lll = getRowValues( thisr, minLen=gh.thisl, maxLen=gh.thisl )
318                lll[1] = lll[0]
319                lll[0] = gh.tv
320           else:
321                assert gh.iv == 1, 'gh.iv should be 0 or 1: %s' % gh.iv
322
323           if lll[1] == '*':
324              log.info( 'WARN.copy.00002: copy directive found ... copy not implemented: %s,%s:: %s' % (k,s,str(lll)) )
325           else:
326             if gh.thisl == 9:
327               lll.append( 105 )
328
329             assert len(lll) == 10,'bad record length ....'
330###
331### add mip name and space ...
332###
333             lll += [k,'']
334
335             self.ntr = nt__grptbl._make( lll )
336             if lll[2].find( '(' ) != -1:
337               bb = [x.strip() for x in lll[2].split( '(' )]
338               lll[2] = bb[0]
339               oldv = bb[1][:-1]
340             else:
341               oldv = ''
342##
343             if lll[0] in ['icesheetmon','icesheetyear','icesheetfx']:
344               otab = lll[0]
345               oshp = lll[5]
346##
347##  reduce doubly specified variables to single
348##
349               if lll[4] == 'Greenland':
350                 self.actions['skip Greenland'] += 1
351                 break
352               if lll[4] == 'Antarctica':
353                 lll[4] = ''
354##
355##  create two copies of all variables
356##
357               for targ in ['ant','gre']:
358                 lll[0] = maps.ttranstem[otab] + targ
359                 ku = self.vark.lookuprec( lll )
360                 lll[11] = ku
361                 lll[5] = oshp + targ
362                 self.saveRec( ku, lll + [oldv,rowIndex,] )
363               self.actions['duplicate ant/gre record'] += 1
364             else:
365               ku = self.vark.lookuprec( lll )
366               lll[11] = ku
367               self.saveRec( ku, lll + [oldv,rowIndex,] )
368
369  def saveRec( self, key, rr ):
370       nbl = 0
371       for x in rr:
372           if x != '':
373             nbl += 1
374##
375## 3 non-blank elements added above ...
376##
377       if nbl < 5:
378         self.actions['skipped mainly blank record %s' % self.gh.tv] += 1
379       else:
380         freq = self.freqmap.get( rr[3], rr[3] )
381         if freq != rr[3]:
382           self.actions['frequency map'] += 1
383           rr[3] = freq
384         if freq == '':
385           self.actions['blank frequency found in table %s' % self.gh.tv] += 1
386         grp = rr[0]
387         tbl = rr[2]
388         if grp == '':
389           print 'ERROR .. blank group: %s.%s:' % (self.mip,self.gh.tv), rr
390         if tbl == '':
391           print 'ERROR .. blank table: %s.%s:' % (self.mip,self.gh.tv), rr
392         if tbl in ['New Variables','NEW']:
393           rr[2] = 'new'
394           self.actions['table map'] += 1
395         var = rr[1]
396         if var.find('_') != -1:
397           rr[1] = var.replace('_','')
398           self.actions['underscore in variable name removed'] += 1
399         self.sh[key] = tuple( rr )
400
401  def parseGrpHead(self,sh,path,k,s):
402    rh1 = ['Short name', 'Standard Name', 'Table', 'Frequency', 'Description extension (optional)', 'Shape', 'Levels', 'Time mean, point or climatology', 'Mask (optional)']
403    rh2 = ['Short name of group', 'Variable short name', 'Table', 'Frequency', 'Description extension (optional)', 'Shape', 'Levels', 'Time mean, point or climatology', 'Mask (optional)']
404
405##
406## initial loop over rows in variable group sheet
407##
408    ll = []
409    for i in range(sh.nrows):
410             thisr = sh.row(i)
411             tv = thisr[0].value
412             if tv[:10] == 'Short name':
413               ll.append(i)
414
415    assert len(ll) in [1,2], 'Could not parse sheet  %s, %s, %s: %s' % (path,k,s,len(ll))
416    withPriority = False
417    hr = sh.row( ll[-1] )
418    if len(ll) == 1:
419             iv = 1
420             it = 0
421             ok = len( hr ) >= 9 and all( map( lambda x: hr[x].value.strip() == rh2[x], range(9) ) )
422             assert ok, '001: Sheet heading not recognised: %s (%s)' % (str(hr),path)
423             if len(hr) > 9 and hr[9].value == u'Priority':
424               withPriority = True
425               thisl = 10
426             else:
427               thisl = 9
428             tv = s
429    else:
430             ok = len( hr ) >= 9 and all( map( lambda x: hr[x].value.strip() == rh1[x], range(9) ) )
431             assert ok, '002: Sheet heading not recognised: %s' % str(hr)
432             iv = 0
433             it = -1
434             tv = sh.row(2)[1].value
435             thisl = 9
436    if tv == '':
437       print 'ERROR: blank group encountered: %s, %s' % (self.mip,s)
438    return nt__grphd( withPriority, thisl, iv, it, ll[-1]+1, tv)
439
440class stdo(object):
441  rq = dreq_cfg.rqcfg()
442
443  def __init__(self,dq=None):
444    self.actions = collections.defaultdict( int )
445##
446##
447## scanning CMIP5 after OMIP and CFMIP allows CMIP5 records to use variable ids from OMIP and CFMIP records,
448## which then enable subsetting done by other mips using CMIP5 tables to be interpreted
449##
450## BUT need to refer to CMIP5 tables from CFMIP
451##
452## need to redo with double pass here ... first for reference and then for mods.
453##
454    mips = ['OMIP','CFMIP','ISMIP6','CMIP5'] + [k for k in self.rq.ff.keys() if k not in ['OMIP','CFMIP','ISMIP6'] ]
455    ##self.rq.ff['CMIP5'] = ['/data/work/documents/CMIP5_standard_output.xls']
456    self.rq.ff['CMIP5'] = ['@ingest/CMIP5_standard_output.xls']
457    self.dq = dq
458
459    self.cmip5Tables = {}
460    self.cmip5TableVars = {}
461    self.refCmvSetup()
462    self.tabInfo = util_gen.tableInfo()
463    self.uidLookup = collections.defaultdict(dict)
464    self.uidLookupSn = collections.defaultdict(dict)
465
466    self.snch = snChange()
467    self.urefcmv = util_varGroups.refCmv()
468
469    for mip in self.refCmv:
470      self.shref = shelve.open( 'inSh/sh__refso_%s' % mip, 'n' )
471      for fn in self.rq.ff[mip]:
472        self.scanFile(mip,fn,refMode=True)
473
474      for snnx in self.uidLookupSn:
475          for t in self.uidLookupSn[ snnx ]:
476            id,v,m = self.uidLookupSn[ snnx ][t]
477            if m == mip:
478              if id not in self.shref:
479                log.info( 'ERROR.lookupsn.0001: %s,%s,%s,%s,%s' % (mip,str(t),id,v,m) )
480               
481      self.shref.close()
482
483    addAerChem = True
484    if addAerChem:
485      self.shref = shelve.open( 'inSh/sh__refso_AerChemMIP', 'n' )
486      self.addAerChem()
487      self.shref.close()
488
489    for mip in mips:
490     for fn in self.rq.ff[mip]:
491       self.scanFile(mip,fn)
492
493    for k in sorted( self.actions.keys() ):
494          print 'ACTIONS: %s (%s)' % (k,self.actions[k])
495
496  def scanFile(self,mip,fn,refMode=False):
497
498## look for names of subsections, for OMIP.
499      if fn in self.rq.fff:
500        self.sss = self.rq.fff[fn]
501      else:
502        self.sss = mip
503
504      self.shInfo = {'prov': '%s standard_output' % mip, 'label': mip, 'title': '%s reviewed CMIP5 tables'}
505      self.shCols = ['priority', 'long name', 'units', 'comment', 'questions & notes', 'output variable name', 'CF Standard name', 'unconfirmed or proposed standard name', 'unformatted units', 'cell_methods', 'valid min', 'valid max', 'mean absolute min', 'mean absolute max', 'positive', 'type', 'CMOR dimensions', 'CMOR variable name', 'realm', 'frequency', 'cell_measures', 'flag_values', 'flag_meanings','empty','pchange','note','table','section','row']
506      self.istnm = self.shCols.index( 'CF Standard name' )
507      self.idims = self.shCols.index( 'CMOR dimensions' )
508
509      if fn[0] == '/':
510        self.fpath = fn
511      if fn[0] == '@':
512        self.fpath = fn[1:]
513      else:
514        self.fpath = '%s%s/%s' % (self.rq.dir0,mip,fn)
515
516      log.info( 'STARTING: %s' % self.fpath ) 
517
518      wb = workbook( self.fpath )
519      if not refMode and mip in self.refCmv:
520        self.sh = shelve.open( 'inSh/sh__so_%s' % mip )
521      else:
522        self.sh = shelve.open( 'inSh/sh__so_%s' % mip, 'n' )
523        self.sh['__cols__'] = self.shCols
524        self.sh['__info__'] = self.shInfo
525      stt = os.stat( self.fpath )
526      self.ctime = stt[stat.ST_CTIME]
527      self.fsize = stt[stat.ST_SIZE]
528      self.sh['__src__'] = {'path':self.fpath, 'size':self.fsize, 'time':self.ctime}
529
530      for sn in wb.sns:
531        if mip == 'OMIP':
532             snn = {'day':'Oday', 'fx':'Ofx'}.get( sn, sn )
533        else:
534             snn = sn
535
536        if sn not in ['general','dims','other output','CFMIP output','Odecxxxx','EditsRequired']:
537          omitOn = False
538          if omitOn and mip in omitThese and sn in omitThese[mip]:
539            pass
540          else:
541            if refMode:
542              doThis = snn in self.refCmv[mip]
543            else:
544              doThis = mip not in self.refCmv or snn not in self.refCmv[mip]
545            if doThis:
546              sht = wb.book.sheet_by_name( sn )
547              try:
548                self.scanSheet( sht, mip, snn,sn, refMode)
549              except:
550                print 'Failed to scan sheet %s in file %s [%s]' % (snn, self.fpath, mip )
551                raise
552      self.sh.close()
553
554  def scanSheet(self,sht,mip,snn,sname,refMode):
555      """Scanning CMIP5 style sheets"""
556      kl = []
557      for k in range(min(40,sht.nrows)):
558        if sht.row(k)[0].value == u'priority':
559          kl.append(k)
560         
561      if len(kl) == 0:
562        kk = 0
563        while len(sht.row(kk)) == 0 or sht.row(kk)[0].ctype not in (2,3):
564          kk += 1
565          assert kk < 40, 'No start found in %s %s %s' % (self.fpath,sname,str(kl))
566      else:
567        kk = kl[0] + 1
568
569      ih = [x.value for x in sht.row( kk-1 )]
570      osc =  omipGsScan()
571      isc,tt = osc.parseh( ih )
572
573      s1 = set()
574      thisTab = []
575      kok = 0
576      for k in range(kk,sht.nrows):
577        ok = True
578        rr  = sht.row(k)
579        rrr = [cleanStr(x) for x in rr]
580        for i in range( len(self.shCols) - len( rr) ):
581          rrr.append( '' )
582        if isc == 1:
583          rrr = osc.filter( rrr )
584        for i in range( 25 - len( rrr) ):
585          rrr.append( '' )
586
587        ll = [x in ['','0.0','42'] for x in rrr]
588        nonblnk = sum( 1 for x in rrr if x not in ['','0.0'] )
589
590        if all( ll[1:6] ) and  all( ll[7:12] ):
591          if not all(ll):
592            log.info( 'INFO.var.0001: %s:%s[%s]: skipping: %s' % (mip,sname,k,str(rrr)) )
593        elif nonblnk < 5:
594            log.info( 'WARN.var.0002: %s:%s[%s]: skipping: %s' % (mip,sname,k,str(rrr)) )
595        else:
596          if rr[17].ctype == 1:
597                  vr = rrr[17]
598          else:
599                  vr = rrr[5]
600          if vr == 'include Amon 2D':
601            log.info( 'WARN.var.0003: %s:%s[%s]: skipping: %s' % (mip,sname,k,str(rrr)) )
602          elif vr == '' and rrr[1][:29] == 'Mole Fraction of Other Radiat':
603            log.info( 'WARN.var.0006: %s:%s[%s]: skipping: %s' % (mip,sname,k,str(rrr)) )
604          else:
605            v1 = rrr[5]
606            rrr[17] = vr
607            if vr == 'CMOR variable name':
608              ok = False
609
610            f1 = v1 in ['','0.0']
611            f2 = vr in ['','0.0']
612            if f1 or f2:
613              log.error( 'ERROR.var.0004: no variable found: [%s.%s] %s' % (mip,sname,str(rrr)) )
614              ok = False
615            v1 = rrr[2]
616            v2 = rrr[8]
617            f1 = v1 in ['','0.0']
618            f2 = v2 in ['','0.0']
619            if f1 and f2:
620              log.error( 'ERROR.var.0005: no units found: [%s.%s] %s' % (mip,sname,str(rrr)) )
621              ok = False
622            elif f1:
623              rrr[2] = v2
624
625            if ok:
626              s1.add( vr )
627              thisTab.append( rrr )
628              kok += 1
629
630      if mip == 'CMIP5':
631          self.cmip5Tables[snn] = tuple( thisTab )
632          self.cmip5TableVars[snn] = list( s1  )
633          toSh = True
634      else:
635          if snn not in self.cmip5Tables:
636             log.info( 'NEW Standard Output sheet name: %s, %s' % (mip,snn) ) 
637             toSh = True
638          elif len(thisTab) == len( self.cmip5Tables[snn] ):
639             nch = 0
640             for k in range(len(thisTab)):
641               if not self.compareMipTabRec( thisTab[k], self.cmip5Tables[snn][k]):
642                 if mip == 'HighResMIP' and snn == 'Omon':
643                    print 'CHANGED RECORD: ',thisTab[k],self.cmip5Tables[snn][k]
644                 nch += 1
645             if nch > 0:
646               log.info( 'MODIFIED Standard Output sheet: %s, %s: records changed: %s (%s)' % (mip,snn,nch,len( self.cmip5Tables[snn] ) ) ) 
647               toSh = True
648             else:
649               toSh = False
650               log.info( 'UNMODIFIED Standard Output sheet skipped: %s, %s' % (mip,snn )  )
651          else:
652             log.info( 'MODIFIED Standard Output sheet: %s, %s: length: %s (%s)' % (mip,snn,len(thisTab),len( self.cmip5Tables[snn] ) ) ) 
653             d1 = sorted( list (s1.difference( set( self.cmip5TableVars[snn] ) ) ))
654             d2 = sorted( list (set( self.cmip5TableVars[snn] ).difference( s1 ) ))
655             log.info( 'MODIFIED Standard Output sheet (.ctd): %s, %s: only here: %s, only CMIP5: %s' % (mip,snn,str(d1), str(d2) ) ) 
656             toSh = True
657
658      log.info( 'INFO.tosh.00001: %s, %s, %s, %s' % ( mip, sname, kok, toSh ) )
659      if toSh:
660        k = kk
661        for rrr in thisTab:
662          vr = rrr[17]
663          vo = rrr[5]
664          ##snn = {'OImon':'SImon'}.get(snn,snn)
665          ##snn = {'aero':'aermonthly'}.get(snn,snn)
666          if vr == 'cllcalipso':
667             log.info( 'INFO.cllcalipso.0001: %s, %s, %s, %s' % (mip,snn, mip in self.refCmv, snn in self.refCmv[mip]) )
668          omit = False
669          if mip in self.refCmv and snn in self.refCmv[mip]:
670             if mip in omitThese and snn in omitThese[mip]:
671                vid = str( uuid.uuid1() )
672                log.info( 'ERROR.newvid.00001: new vid minted for %s, %s, %s: %s' % (mip,sname,vr,vid) )
673                pass
674             else:
675                if vr in {'*','include Oyr 3D tracers'}:
676                   log.info( 'WARN.copy.00004: copy directive found ... copy not implemented: %s,%s:: %s' % (mip,sname,str(rrr)) )
677                   omit = True
678                   vid, sect = '',None
679                else:
680                  vid,sect = self.makeRefVarRec( rrr, k, snn, mip )
681                  log.info( 'INFO.sect.0005: sect=%s [%s,%s] %s' % (sect, snn, mip,vid) )
682                if sect == 'Oyr_3dtr':
683                   rrrc = rrr[:]
684                   rrrc[0] = 2
685                   rrrc[16] = 'longitude latitude time depth0m'
686                   rrrc[19] = 'mon'
687                   rrrc[20] = 'area: areacello'
688                   rrrc[4] = 'Concentrations of all 3D tracers in the uppermost ocean layer. See first table in Oyr for a complete list of these tracers.  "Tracer"  concentations should be reported even if they are diagnosed rather than prognostically calculated.'
689                   vid,sect = self.makeRefVarRec( rrrc, 1000 + k, 'Omon', mip )
690                   log.info( 'INFO.copy.0005: copy to Omon of 3d-tracer : %s' % vr )
691                self.uidLookup[ snn ][vr] = vid
692                stnm = rrr[self.istnm]
693                dims = rrr[self.idims]
694                self.uidLookupSn[ snn ][(stnm,dims)] = (vid,vr,mip)
695          elif snn in ['OImon',]:
696                vid = str( uuid.uuid1() )
697                log.info( 'INFO.newvid.00002: new vid minted for %s, %s, %s' % (mip,sname,vr) )
698          else:
699                assert snn in self.uidLookup or snn in ['aero','LImon','Odec'], 'Bad table name; %s; %s:: %s' % (mip,snn, str( self.uidLookup.keys() ) )
700                if vr in {'*','include Oyr 3D tracers'}:
701                   log.info( 'WARN.copy.00001: copy directive found ... copy not implemented: %s,%s:: %s' % (mip,sname,str(rrr)) )
702                   omit = True
703                elif snn == 'aero':
704                  if vr in self.uidLookup['aermonthly']:
705                    vid = self.uidLookup['aermonthly'][vr]
706                  elif vr in self.refUid['aermonthly']:
707                    vid = self.refUid['aermonthly'][vr]
708                    log.info( 'INFO.newvid.00088: vid from refUid for aermonthly %s, %s: %s' % (vr,mip,vid) )
709                  else:
710                    vid = str( uuid.uuid1() )
711                    log.info( 'WARN.newvid.00088: new vid for aermonthly %s, %s: %s' % (vr,mip,vid) )
712                elif vr in self.uidLookup[snn]:
713                  vid = self.uidLookup[snn][vr]
714                else:
715                  stnm = rrr[self.istnm]
716                  dims = rrr[self.idims]
717                  if (stnm,dims) in self.uidLookupSn[ snn ]:
718                    vr0 = vr
719                    vid,vr,mip0 = self.uidLookupSn[ snn ][(stnm,dims)]
720                    if vr != vr0:
721                      log.info( 'INFO.newvr.00001: new var name for %s, %s, %s: %s,%s,%s' % (mip,sname,vr0, vr,vid,mip0) )
722                  else:
723                    if snn in self.snch.omit and stnm in self.snch.omit[snn]:
724                      log.info( 'INFO.omit.00001: %s, %s, %s, %s' % (mip,sname,vr,stnm) )
725                      omit = True
726                    elif snn in self.snch.trigger and stnm in self.snch.trigger[snn]:
727                      log.info( 'INFO.trigger.00001: %s, %s, %s, %s' % (mip,sname,vr,stnm) )
728                      omit = True
729                    elif snn in self.snch.map and stnm in self.snch.map[snn]:
730                      st2 = self.snch.map[snn][stnm][0]
731                      if (st2,dims) in self.uidLookupSn[ snn ]:
732                        vid,vr,mip0 = self.uidLookupSn[ snn ][(st2,dims)]
733                        log.info( 'INFO.vmap.00001: %s, %s, %s, %s,%s: %s' % (mip,sname,vr,stnm,st2,vid) )
734                      else:
735                        vid = str( uuid.uuid1() )
736                        log.info( 'INFO.newvid.00005: new vid minted for %s, %s, %s, %s: %s' % (mip,sname,vr,stnm,vid) )
737                    elif snn == 'LImon' and vr + 'Li' in self.urefcmv.byTab[snn]:
738                      vid = self.urefcmv.byTab[snn][vr + 'Li']
739                      log.info( 'INFO.vid.01004: vid from urefcmv.byTab -- with Li suffix -- for %s, %s, %s, %s: %s' % (mip,sname,vr,stnm,vid) )
740                    elif snn in self.urefcmv.byTab and vr in self.urefcmv.byTab[snn]:
741                      vid = self.urefcmv.byTab[snn][vr]
742                      log.info( 'INFO.vid.00004: vid from urefcmv.byTab for %s, %s, %s, %s: %s' % (mip,sname,vr,stnm,vid) )
743                    else:
744                      vid = str( uuid.uuid1() )
745                      log.info( 'INFO.newvid.00004: new vid minted for %s, %s, %s, %s: %s' % (mip,sname,vr,stnm,vid) )
746          uid = str( uuid.uuid1() )
747
748          p = int( float( rrr[0] ) )
749          if rrr[24] != '':
750            prs = int( float( rrr[24] ) )
751            if prs == 0:
752              omit=True
753            elif prs == 2:
754              p=1
755## kk = ['var', 'table', 'mip', 'vid', 'priority']
756         
757          if not omit:
758            assert vo not in {'*','include Oyr 3D tracers'} and vr not in {'*','include Oyr 3D tracers'}, 'Bad vo,vr: %s,%s' % (vo,vr)
759            rec = [vr, '%s-%s' % (mip,snn), mip, str(vid), p  ]
760           
761            log.info( 'INFO.requestvar.00004: %s' % str(rec) )
762            self.sh[ uid ] = rec
763            k += 1
764
765  def addAerChem(self):
766    mip = 'AerChemMIP'
767    lc = util_gen.loadAerChem()
768    lsp = util_gen.loadSpatial()
769    self.sh = shelve.open( 'inSh/sh__so_%s' % mip, 'n' )
770    self.sh['__cols__'] = self.shCols
771    self.shInfo = {'prov': '%s request template, request scoping sheet' % mip, 'label': mip, 'title': 'CMIP6 Data Request'}
772    self.sh['__info__'] = self.shInfo
773    dmin = ['minpblz','tasmin']
774    dmax = ['maxpblz','tasmax']
775
776    self.nnss = 0
777    for tab in lc.cc2.keys():
778      nn = 0
779      for var in lc.cc2[tab].a:
780## ['air_temperature', 'Surface Air Temperature', 'tas', 'K', 'near-surface (usually, 2 meter) air temperature', 'AerChemMIP-Day2d', 'daily', 'XY-na', 'mean']
781        assert len( lc.cc2[tab].a[var] ) == 1, 'Multiple definitions for %s, %s' % (tab,var)
782         
783        sn, ln, v0, units, desc, sect, freq, splab, tlab = lc.cc2[tab].a[var][0]
784        assert splab in lsp.uidByLab, 'Spatial shape label %s not found' % splab
785        spid = lsp.uidByLab[splab]
786        print 'INFO.spatial.00010: setting spid: ',splab, spid
787        spdims = lsp.ssu[spid][2]
788        assert tlab in ['mean','point'], 'Time dimension label %s not recognised' % tlab
789        tdim, cmet = {'mean':('time','time: mean'), 'point':('time1','')}[tlab]
790        if v0 in dmin:
791          cmet = 'time: minimum'
792        elif v0 in dmax:
793          cmet = 'time: maximum'
794        cmea = 'area: areacella'
795        if var in ['tos','tossq']:
796          cmea = 'area: areacello'
797       
798        dims = spdims + ' ' + tdim
799        if var in ['tasmax','tasmin']:
800          dims += ' height2m'
801        p = 1
802        vid = '----'
803        gpid = '----'
804        if var not in self.refUid[tab]:
805            self.actions['WARNING: New uid generated for variable in %s %s' % (mip,tab)] += 1
806            uid =  str( uuid.uuid1() )
807            log.warn( 'WARN.uid.0001: new uid for %s (%s, %s): %s' % (var,mip,tab,uid) )
808        else:
809            uid = self.refUid[tab][var]
810        thisrealm = 'unset'
811        rec0 = [uid, desc, '', '', '', '', 'float', '', sn, '', ln, '', cmet, '', cmea, var, 'aerosol', units, '', '', '', var, tab, dims, vid, gpid, sect, p]
812      ##self.shCols = ['priority', 'long name', 'units', 'comment', 'questions & notes', 'output variable name', 'CF Standard name', 'unconfirmed or proposed standard name', 'unformatted units', 'cell_methods', 'valid min', 'valid max', 'mean absolute min', 'mean absolute max', 'positive', 'type', 'CMOR dimensions', 'CMOR variable name', 'realm', 'frequency', 'cell_measures', 'flag_values', 'flag_meanings','table','section','row']
813        rec0 = [p,ln, units, desc, '', var, sn, '', units, cmet, '','','','','','float',dims,var,'aerosol',freq,cmea,'','',tab,sect,-1]
814        rec = [var, tab, mip, str(uid), p  ]
815        self.sh[ uid ] = rec
816
817        nss = None
818        if sect in [u'AerChemMIP-Zonal2d',u'AerChemMIP-Zonal',u'AerChemMIP-Mon2d', u'Monthly-mean 2d', u'Monthly-mean 1d', u'Monthly-mean zonal mean 2d', u'Monthly-mean 2d (T2)', u'Monthly-mean zonal mean 2d (T2)']:
819          print 'INFO.aerchem.00010: ', rec
820          nss = 'aermonthly-oth'
821        elif sect in [ u'AerChemMIP-Mon3d',u'Monthly-mean 3d', u'Monthly-mean 3d (T2)']:
822          print 'INFO.aerchem.00020: ', rec
823          nss = 'aermonthly-3d'
824        if sect == 'Monthly-mean 2dxx':
825          print 'INFO.aerchem.00011: ', rec
826          nss = 'aermonthly-2d'
827        if sect == 'Daily 2d':
828          print 'INFO.aerchem.00021: ', rec
829          nss = 'aerdaily-2d'
830        if nss != None:
831          self.nnss += 1
832          self.sh[ str( uuid.uuid1() ) ] = rec
833          sect = nss
834        if sect in ['AerChemMIP-hr','AerChemMIP-Day2d','fx']:
835          sect = ''
836        else:
837          print 'INFO.aerchem.00050:  unrecognised section', sect, rec
838
839        lset = {'ssect':sect, 'uid':uid, 'rowIndex':-1, 'mipTable':tab}
840        ro = self._getRefRec(rec0,lset)
841        self.shref[uid] = ro
842    self.sh.close()
843
844  def makeRefVarRec( self, rrr, k, snn, mip ):
845    ## ['uuid', 'comment', 'deflate_level', 'shuffle', 'ok_max_mean_abs', 'flag_meanings', \
846     #                     'type', 'ok_min_mean_abs', 'standard_name', 'deflate', 'long_name', 'valid_min',\
847                           #'cell_methods', 'flag_values', 'cell_measures', 'out_name', 'modeling_realm', 'units',\
848                           #'#cell_methods', 'valid_max', 'positive', 'var', 'mipTable','dimensions','vid','gpid','ssect','priority']
849      ##self.shCols = ['priority', 'long name', 'units', 'comment', 'questions & notes', 'output variable name', 'CF Standard name', 'unconfirmed or proposed standard name', 'unformatted units', 'cell_methods', 'valid min', 'valid max', 'mean absolute min', 'mean absolute max', 'positive', 'type', 'CMOR dimensions', 'CMOR variable name', 'realm', 'frequency', 'cell_measures', 'flag_values', 'flag_meanings','table','section','row']
850    tab = self.refCmv[mip][snn]
851    isn = 6
852    var = rrr[17]
853    if var not in self.refUid[tab]:
854        self.actions['WARNING: New uid generated for variable in %s %s' % (mip,tab)] += 1
855        uid =  str( uuid.uuid1() )
856        log.warn( 'WARN.uid.0001: new uid for %s (%s, %s): %s' % (var,mip,tab,uid) )
857    else:
858      uid = self.refUid[tab][var]
859
860    sn = rrr[isn]
861    ln = rrr[1]
862    dims = rrr[16]
863    flg, sect = util_gen.sect(  mip, var, snn, k, dims, ln, isBgc=(self.sss == 'bgc') )
864    if not flg:
865       sect = ''
866
867    lset = {'ssect':sect, 'uid':uid, 'rowIndex':k, 'mipTable':tab}
868    ro = self._getRefRec(rrr,lset)
869    self.shref[uid] = ro
870    return uid,sect
871
872  def _getRefRec(self,rrr,lset):
873    defer = {'gpid','vid'}
874    redundant = {'shuffle','deflate_level', 'cell_methods_xx','deflate'}
875   
876    ro = []
877    for x in self.tabInfo.oh:
878      if x in defer:
879        v = '__deferred__'
880      elif x in lset:
881        v = lset[x]
882      elif x in redundant:
883        v = ''
884      elif x in self.tabInfo.ih:
885        v = rrr[ self.tabInfo.iix[x] ]
886      elif x in self.tabInfo.oix:
887        v = rrr[ self.tabInfo.oix[x] ]
888      else:
889        raise baseException( 'Table heading not found: %s' % x )
890
891      ro.append( v )
892    return ro
893     
894  def refCmvSetup(self, mode='new'):
895    self.refCmv = collections.defaultdict( dict )
896    self.tabMap = {}
897    self.refUid = collections.defaultdict( dict )
898    for x in targTabs:
899      if x[:5] == 'CMIP5':
900        mip,tab = x.split( '_' )
901      else:
902        mip,tab = x.split( '.' )
903      if mip == 'OMIP':
904        tab = {'day':'Oday', 'fx':'Ofx'}.get( tab, tab )
905      self.refCmv[mip][tab] = tab
906      self.tabMap[x] = (mip,tab)
907
908    if mode == 'new':
909      sh = shelve.open( 'inSh/refCmvId', 'r' )
910      for k in sh.keys():
911        tab, lab, ver = sh[k]
912        self.refUid[tab][lab] = k
913    else:
914      for i in self.dq.coll['CMORvar'].items:
915        self.refUid[i.mipTable][i.label] = i.uid
916
917  def compareMipTabRec( self, r1, r2 ):
918    if len(r1) != len(r2):
919      return False
920    return all( [r1[k] == r2[k] for k in [0,1,2,3,4,5,6,9,16,24] ] )
921
922class requestScope(object):
923  rq = dreq_cfg.rqcfg()
924
925  def __init__(self,sdir='inSh'):
926
927    keys = sorted( self.rq.ee.keys() )
928
929    ##self.vgss = util_varGroups.varGroupSs()
930    ##self.vgss.loadGroups()
931    ##self.vgss.setUid()
932##
933    ee = {}
934    self.shCols = ['Short name of group', 'Variable short name', 'Table', 'Frequency', 'Description extension (optional)', 'Shape', 'Levels', 'Time mean, point or climatology', 'Mask (optional)', 'Priority', 'MIP','uid','Prev. Var Name']
935##
936## array for information and debugging
937##
938    self.pr4 = dreq_utils.pr4()
939 
940    eh = {}
941    er = {}
942    for mip in keys:
943        self.mip = mip
944        self.actions = collections.defaultdict( int )
945        self.shInfo = {'prov': '%s request template, request scoping sheet' % mip, 'label': mip, 'title': '%s request'}
946        fn = self.rq.ee[mip]
947        path = '%s%s/%s' % (self.rq.dir0,mip,fn)
948        self.path = path
949        stt = os.stat( path )
950        self.ctime = stt[stat.ST_CTIME]
951        self.fsize = stt[stat.ST_SIZE]
952        wb = workbook( path )
953        if 'Request scoping' in wb.sns:
954          print 'INFO.requestscope.000001: scanning %s' % fn
955          sh = wb.book.sheet_by_name( 'Request scoping' )
956          r4 = map( lambda x: x.value, sh.row(3) )
957          self.pr4.parse( self.mip, r4 )
958          eh[mip] = (list( self.pr4.r4info ), r4)
959          rl = self.scanSheet( sh, self.pr4.r4info )
960          print 'MIP:: %s, %s' % (mip, len(rl))
961          er[mip] = rl
962        else:
963          print 'Skipping ... no request scoping sheet: ',mip
964    sh = shelve.open( '%s/sh__requestScoping' % sdir, 'n' )
965    sh['__headings__'] = eh
966    sh['__records__'] = er
967    sh.close()
968
969  def scanSheet( self,sh, r4info ):
970    rl = []
971    gpmaps = {}
972    gpmaps['ISMIP6'] = {'icesheetfx':['LIfxgre','LIfxant'], 'icesheetyear':['LIyrgre','LIyrant'], 'icesheetmon':['LImongre','LImonant']}
973    gpmaps['VolMIP'] = {'DYVR_monthly':['DYVR_monthly_a','DYVR_monthly_b','DYVR_monthly_c','DYVR_monthly_d']}
974    for i in range(4,sh.nrows):
975      rr = [x.value for x in sh.row(i)]
976      if not (rr[0] in ['',u''] or rr[0][0] == '#'):
977        if self.mip == 'DCPP':
978          j0 = r4info.ownix[0]
979        else:
980          j0 = r4info.ixcntl
981        if all( [x in {u'','',0,0.0} for x in rr[j0:]] ):
982          if rr[1] != 'none':
983            print 'skipping blank line: ',self.mip,rr
984        elif rr[0] == 'Short name of variable group':
985            print 'skipping title line: ',self.mip,rr
986        elif rr[1] == 'none':
987            print 'skipping non-blank line marked as *none*:',self.mip,rr
988        else:
989          if self.mip in gpmaps and rr[0] in gpmaps[self.mip]:
990            rr0 = rr[0]
991            for x in gpmaps[self.mip][rr0]:
992              rr[0] = x
993              rl.append( rr[:] )
994          else:
995            rl.append( rr )
996    return rl
997
998if __name__ == '__main__':
999  #ts = templateStat()
1000  rs = requestScope()
1001  logFarm.shutdown() 
Note: See TracBrowser for help on using the repository browser.