rda-python-common 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1447 @@
1
+ #
2
+ ###############################################################################
3
+ #
4
+ # Title : pg_opt.py
5
+ #
6
+ # Author : Zaihua Ji, zji@ucar.edu
7
+ # Date : 08/26/2020
8
+ # 2025-01-10 transferred to package rda_python_common from
9
+ # https://github.com/NCAR/rda-shared-libraries.git
10
+ # 2025-12-01 convert to class PgOPT
11
+ # Purpose : python library module for holding global varaibles
12
+ # functions for processing options and other global functions
13
+ #
14
+ # Github : https://github.com/NCAR/rda-pyhon-common.git
15
+ #
16
+ ###############################################################################
17
+ #
18
+ import os
19
+ import sys
20
+ import re
21
+ import time
22
+ from os import path as op
23
+ from .pg_file import PgFile
24
+
25
+ class PgOPT(PgFile):
26
+
27
+ def __init__(self):
28
+ super().__init__() # initialize parent class
29
+ self.OUTPUT = None
30
+ self.CMDOPTS = {}
31
+ self.INOPTS = {}
32
+ # global variables are used by all applications and this package.
33
+ # they need be initialized in application specified packages
34
+ self.ALIAS = {}
35
+ self.TBLHASH = {}
36
+ ###############################################################################
37
+ # valid options the first hash value: 0 means mode option, 1 means single-value
38
+ # option, 2 means multiple-value option, and >=4 means action option the second
39
+ # hash values are long option names, either hash keys (considered as short
40
+ # option names) or the associated long names can be used. All options, except for
41
+ # multi-line value ones, can be specified on command line, while single-value and
42
+ # multi-value options, except option -IM for input files, can also given in input
43
+ # files long value option names are used in output files all letters of option
44
+ # names are case insensitive.
45
+ #
46
+ # The third hash value define bit flags,
47
+ # For Action Options:
48
+ # -1 - VSN card actions
49
+ # >0 - setions
50
+ #
51
+ # For Mode Options:
52
+ # 1 - mode for archiving actions
53
+ # 2 - mode for set actions
54
+ #
55
+ # For Single-Value Info Options:
56
+ # 1(0x001) - auto set value
57
+ # 2(0x002) - manually set value
58
+ # 16(0x010) - convert to integer from commandline and input files, set to 0 if empty
59
+ # 32(0x020) - time field
60
+ # 128(0x080) - '' allowed for single letter value
61
+ # 256(0x100) - date field
62
+ #
63
+ # For Multi-Value Info Options:
64
+ # 1(0x001) - one for multiple
65
+ # 2(0x002) - auto-set,
66
+ # 4(0x004) - expanded from one
67
+ # 8(0x008) - validated
68
+ # 16(0x010) - convert to integer from commandline and input files, set to 0 if empty
69
+ # 32(0x020) - time field
70
+ # 64(0x040) - text field allowing multiple lines
71
+ # 128(0x080) - '' allowed for single letter value
72
+ # 256(0x100) - date field
73
+ #
74
+ # The fourth hash values defined retrictions for single letter values
75
+ ###############################################################################
76
+ self.OPTS = {}
77
+ # global initial optional values
78
+ self.PGOPT = {
79
+ 'ACTS' : 0, # carry current action bits
80
+ 'UACTS' : 0, # carry dsarch skip check UD action bits
81
+ 'CACT' : '', # current short action name
82
+ 'IFCNT' : 0, # 1 to read a single Input File at a time
83
+ 'ANAME' : '', # cache the application name if set
84
+ 'TABLE' : '', # table name the action is on
85
+ 'UID' : 0, # user.uid
86
+ 'MSET' : 'SA', # Action for multiple sets
87
+ 'WIDTH' : 128, # max column width
88
+ 'TXTBIT' : 64, # text field bit (0x1000) allow multiple lines
89
+ 'PEMAX' : 12, # max count of reuqest partition errors for auto reprocesses
90
+ 'PTMAX' : 24, # max number of partitions for a single request
91
+ 'REMAX' : 2, # max count of reuqest errors for auto reprocesses
92
+ 'RSMAX' : 100, # max count of gatherxml with options -R -S
93
+ 'RCNTL' : None, # placehold for a request control record
94
+ 'dcm' : "dcm",
95
+ 'sdp' : "sdp",
96
+ 'rcm' : "rcm",
97
+ 'scm' : "scm",
98
+ 'wpg' : "",
99
+ 'gatherxml' : "gatherxml",
100
+ 'cosconvert' : "cosconvert",
101
+ 'emllog' : self.LGWNEM,
102
+ 'emlerr' : self.LOGERR|self.EMEROL,
103
+ 'emerol' : self.LOGWRN|self.EMEROL,
104
+ 'emlsum' : self.LOGWRN|self.EMLSUM,
105
+ 'emlsep' : self.LGWNEM|self.SEPLIN,
106
+ 'wrnlog' : self.LOGWRN,
107
+ 'errlog' : self.LOGERR,
108
+ 'extlog' : self.LGEREX,
109
+ 'PTYPE' : "CPRV",
110
+ 'WDTYP' : "ADNU",
111
+ 'HFTYP' : "DS",
112
+ 'SDTYP' : "PORWUV",
113
+ 'GXTYP' : "DP"
114
+ }
115
+ # global default parameters
116
+ self.params = {
117
+ 'ES' : "<=>",
118
+ 'AO' : "<!>",
119
+ 'DV' : "<:>"
120
+ }
121
+ self.WTYPE = {
122
+ 'A' : "ARCO",
123
+ 'D' : "DATA",
124
+ 'N' : "NCAR",
125
+ 'U' : "UNKNOWN",
126
+ }
127
+ self.HTYPE = {
128
+ 'D' : "DOCUMENT",
129
+ 'S' : "SOFTWARE",
130
+ 'U' : "UNKNOWN"
131
+ }
132
+ self.HPATH = {
133
+ 'D' : "docs",
134
+ 'S' : "software",
135
+ 'U' : "help"
136
+ }
137
+ self.MTYPE = {
138
+ 'P' : "PRIMARY",
139
+ 'A' : "ARCHIVING",
140
+ 'V' : "VERSION",
141
+ 'W' : "WORKING",
142
+ 'R' : "ORIGINAL",
143
+ 'B' : "BACKUP",
144
+ 'O' : "OFFSITE",
145
+ 'C' : "CHRONOPOLIS",
146
+ 'U' : "UNKNOWN"
147
+ }
148
+ self.STYPE = {
149
+ 'O' : "OFFLINE",
150
+ 'P' : "PRIMARY",
151
+ 'R' : "ORIGINAL",
152
+ 'V' : "VERSION",
153
+ 'W' : "WORKING",
154
+ 'U' : "UNKNOWN"
155
+ }
156
+ self.BTYPE = {
157
+ 'B' : "BACKUPONLY",
158
+ 'D' : "BACKDRDATA",
159
+ }
160
+
161
+ # process and parsing input information
162
+ # aname - application name such as 'dsarch', 'dsupdt', and 'dsrqst'
163
+ def parsing_input(self, aname):
164
+ self.PGLOG['LOGFILE'] = aname + ".log"
165
+ self.PGOPT['ANAME'] = aname
166
+ self.dssdb_dbname()
167
+ argv = sys.argv[1:]
168
+ if not argv: self.show_usage(aname)
169
+ self.cmdlog("{} {}".format(aname, ' '.join(argv)))
170
+ # process command line options to fill option values
171
+ option = infile = None
172
+ needhelp = 0
173
+ helpopts = {}
174
+ for param in argv:
175
+ if re.match(r'^(-{0,2}help|-H)$', param, re.I):
176
+ if option: helpopts[option] = self.OPTS[option]
177
+ needhelp = 1
178
+ continue
179
+ ms = re.match(r'^-([a-zA-Z]\w*)$', param)
180
+ if ms: # option parameter
181
+ param = ms.group(1)
182
+ if option and not needhelp and option not in self.params:
183
+ val = self.get_default_info(option)
184
+ if val is not None:
185
+ self.set_option_value(option, val)
186
+ else:
187
+ self.parameter_error("-" + option, "missval")
188
+ option = self.get_option_key(param)
189
+ if needhelp:
190
+ helpopts[option] = self.OPTS[option]
191
+ break
192
+ # set mode/action options
193
+ if self.OPTS[option][0]&3 == 0: self.set_option_value(option)
194
+ elif option:
195
+ ms =re.match(r"^\'(.*)\'$", param)
196
+ if ms: param = ms.group(1)
197
+ self.set_option_value(option, param)
198
+ elif self.find_dataset_id(param):
199
+ self.set_option_value('DS', param)
200
+ else:
201
+ option = self.get_option_key(param, 3, 1)
202
+ if option:
203
+ self.set_option_value(option)
204
+ if needhelp:
205
+ helpopts[option] = self.OPTS[option]
206
+ break
207
+ elif op.exists(param): # assume input file
208
+ infile = param
209
+ else:
210
+ self.parameter_error(param)
211
+ if needhelp: self.show_usage(aname, helpopts)
212
+ if option and option not in self.params:
213
+ val = self.get_default_info(option)
214
+ if val is not None:
215
+ self.set_option_value(option, val)
216
+ else:
217
+ self.parameter_error("-" + option, "missval")
218
+ # check if only an input filename is given on command line following aname
219
+ if infile:
220
+ if 'IF' in self.params:
221
+ self.parameter_error(infile)
222
+ else:
223
+ self.params['IF'] = [infile]
224
+ # process given one or multiple input files to fill option values
225
+ if 'IF' in self.params:
226
+ self.PGOPT['IFCNT'] = 1 if self.PGOPT['CACT'] == 'AQ' else 0
227
+ if self.OPTS['DS'][0] == 1:
228
+ param = self.validate_infile_names(self.params['DS']) if 'DS' in self.params else 0
229
+ else:
230
+ param = 1
231
+ self.get_input_info(self.params['IF'])
232
+ if not param and 'DS' in self.params: self.validate_infile_names(self.params['DS'])
233
+ if not self.PGOPT['ACTS']: self.parameter_error(aname, "missact") # no action enter
234
+ if 'DB' in self.params:
235
+ dcnt = len(self.params['DB'])
236
+ for i in range(dcnt):
237
+ if i == 0:
238
+ self.PGLOG['DBGLEVEL'] = self.params['DB'][0]
239
+ elif i == 1:
240
+ self.PGLOG['DBGPATH'] = self.params['DB'][1]
241
+ elif i == 2:
242
+ self.PGLOG['DBGFILE'] = self.params['DB'][2]
243
+ self.pgdbg(self.PGLOG['DBGLEVEL'])
244
+ if 'GZ' in self.params: self.PGLOG['GMTZ'] = self.diffgmthour()
245
+ if 'BG' in self.params: self.PGLOG['BCKGRND'] = 1
246
+
247
+ # check and get default value for info option, return None if not available
248
+ def get_default_info(self, opt):
249
+ olist = self.OPTS[opt]
250
+ if olist[0]&3 and len(olist) > 3:
251
+ odval = olist[3]
252
+ if not odval or isinstance(odval, int):
253
+ return odval
254
+ else:
255
+ return odval[0] # return the first char of a default string
256
+ return None
257
+
258
+ # set output file name handler now
259
+ def open_output(self, outfile = None):
260
+ if outfile: # result output file
261
+ try:
262
+ self.OUTPUT = open(outfile, 'w')
263
+ except Exception as e:
264
+ self.pglog("{}: Error open file to write - {}".format(outfile, str(e)), self.PGOPT['extlog'])
265
+ else: # result to STDOUT
266
+ self.OUTPUT = sys.stdout
267
+
268
+ # return 1 if valid infile names; sys.exit(1) otherwise
269
+ def validate_infile_names(self, dsid):
270
+ i = 0
271
+ for infile in self.params['IF']:
272
+ if not self.validate_one_infile(infile, dsid): return self.FAILURE
273
+ i += 1
274
+ if self.PGOPT['IFCNT'] and i >= self.PGOPT['IFCNT']: break
275
+ return i
276
+
277
+ # validate an input filename against dsid
278
+ def validate_one_infile(self, infile, dsid):
279
+ ndsid = self.find_dataset_id(infile)
280
+ if ndsid == None:
281
+ return self.pglog("{}: No dsid identified in Input file name {}!".format(dsid, infile), self.PGOPT['extlog'])
282
+ fdsid = self.format_dataset_id(ndsid)
283
+ if fdsid != dsid:
284
+ return self.pglog("{}: Different dsid {} found in Input file name {}!".format(dsid, fdsid, infile), self.PGOPT['extlog'])
285
+ return self.SUCCESS
286
+
287
+ # gather input information from input files
288
+ def get_input_info(self, infiles, table = None):
289
+ i = 0
290
+ for file in infiles:
291
+ i += self.process_infile(file, table)
292
+ if not self.PGOPT['IFCNT'] and self.PGOPT['CACT'] == 'AQ': self.PGOPT['IFCNT'] = 1
293
+ if self.PGOPT['IFCNT']: break
294
+ return i
295
+
296
+ # validate and get info from a single input file
297
+ def read_one_infile(self, infile):
298
+ dsid = self.params['DS']
299
+ del self.params['DS']
300
+ if self.OPTS['DS'][2]&2: self.OPTS['DS'][2] &= ~2
301
+ if 'DS' in self.CMDOPTS: del self.CMDOPTS['DS']
302
+ self.clean_input_values()
303
+ self.process_infile(infile)
304
+ if 'DS' in self.params: dsid = self.params['DS']
305
+ if dsid: self.validate_one_infile(infile, dsid)
306
+ return dsid
307
+
308
+ # gather input option values from one input file
309
+ # return 0 if nothing retireved if table is not null
310
+ def process_infile(self, infile, table = None):
311
+ if not op.exists(infile): self.pglog(infile + ": Input file not exists", self.PGOPT['extlog'])
312
+ if table:
313
+ self.pglog("Gather '{}' information from input file '{}'..." .format(table, infile), self.PGOPT['wrnlog'])
314
+ else:
315
+ self.pglog("Gather information from input file '{}'...".format(infile), self.PGOPT['wrnlog'])
316
+ try:
317
+ fd = open(infile, 'r')
318
+ except Exception as e:
319
+ self.pglog("{}: Error Open input file - {}!".format(infile, str(e)), self.PGOPT['extlog'])
320
+ else:
321
+ lines = fd.readlines()
322
+ fd.close()
323
+ opt = None
324
+ columns = []
325
+ chktbl = 1 if table else -1
326
+ mpes = r'^(\w+)\s*{}\s*(.*)$'.format(self.params['ES'])
327
+ mpao = r'^(\w+)\s*{}'.format(self.params['AO'])
328
+ # column count, column index, value count, value index, line index, option-set count, end divider flag
329
+ colcnt = colidx = valcnt = validx = linidx = setcnt = enddiv = 0
330
+ for line in lines:
331
+ linidx += 1
332
+ if linidx%50000 == 0:
333
+ self.pglog("{}: {} lines read".format(infile, linidx), self.PGOPT['wrnlog'])
334
+ if 'NT' not in self.params: line = self.pgtrim(line, 2)
335
+ if not line:
336
+ if opt: self.set_option_value(opt, '', 1, linidx, line, infile)
337
+ continue # skip empty lines
338
+ if chktbl > 0:
339
+ if re.match(r'^\[{}\]$'.format(table), line, re.I): # found entry for table
340
+ chktbl = 0
341
+ self.clean_input_values() # clean previously saved input values
342
+ continue
343
+ else:
344
+ ms = re.match(r'^\[(\w+)\]$', line)
345
+ if ms:
346
+ if chktbl == 0: break # stop at next sub-title
347
+ if not self.PGOPT['MSET']:
348
+ self.input_error(linidx, line, infile, ms.group(1) + ": Cannt process sub-title")
349
+ elif self.PGOPT['CACT'] != self.PGOPT['MSET']:
350
+ self.input_error(linidx, line, infile, "Use Action -{} to Set multiple sub-titles".format(self.PGOPT['MSET']))
351
+ break # stop getting info if no table given or a different table
352
+ if colcnt == 0: # check single value and action lines first
353
+ ms = re.match(mpes, line)
354
+ if ms: # one value assignment
355
+ key = ms.group(1).strip()
356
+ val = ms.group(2)
357
+ if val and 'NT' not in self.params: val = val.strip()
358
+ opt = self.get_option_key(key, 1, 0, linidx, line, infile, table)
359
+ self.set_option_value(opt, val, 0, linidx, line, infile)
360
+ if not self.OPTS[opt][2]&self.PGOPT['TXTBIT']: opt = None
361
+ setcnt += 1
362
+ continue
363
+ ms = re.match(mpao, line)
364
+ if ms: # set mode or action option
365
+ key = self.get_option_key(ms.group(1).strip(), 4, 0, linidx, line, infile, table)
366
+ self.set_option_value(key, '', 0, linidx, line, infile)
367
+ setcnt += 1
368
+ continue
369
+ # check mutiple value assignment for one or more multi-value options
370
+ values = line.split(self.params['DV'])
371
+ valcnt = len(values)
372
+ if colcnt == 0:
373
+ while colcnt < valcnt:
374
+ key = values[colcnt].strip()
375
+ if not key: break
376
+ opt = self.get_option_key(key, 2, 1, linidx, line, infile, table)
377
+ if not opt: break
378
+ columns.append(opt)
379
+ if opt in self.params: del self.params[opt]
380
+ colcnt += 1
381
+ if colcnt < valcnt:
382
+ if colcnt == (valcnt-1):
383
+ enddiv = 1
384
+ else:
385
+ self.input_error(linidx, line, infile, "Multi-value Option Name missed for column {}".format(colcnt+1))
386
+ opt = None
387
+ continue
388
+ elif valcnt == 1:
389
+ if re.match(mpes, line):
390
+ self.input_error(linidx, line, infile, "Cannot set single value option after Multi-value Options")
391
+ elif re.match(mpao, line):
392
+ self.input_error(linidx, line, infile, "Cannot set acttion/mode option after Multi-value Options")
393
+ if opt: # add to multipe line value
394
+ val = values.pop(0)
395
+ valcnt -= 1
396
+ if val and 'NT' not in self.params: val = val.strip()
397
+ self.set_option_value(opt, val, 1, linidx, line, infile)
398
+ setcnt += 1
399
+ if valcnt == 0: continue # continue to check multiple line value
400
+ colidx += 1
401
+ opt = None
402
+ reduced = 0
403
+ valcnt += colidx
404
+ if valcnt > colcnt:
405
+ if enddiv:
406
+ val = values.pop()
407
+ if not val.strip():
408
+ valcnt -= 1
409
+ reduced = 1
410
+ if valcnt > colcnt:
411
+ self.input_error(linidx, line, infile, "Too many values({}) provided for {} columns".format(valcnt+colidx, colcnt))
412
+ if values:
413
+ for val in values:
414
+ opt = columns[colidx]
415
+ colidx += 1
416
+ if val and 'NT' not in self.params: val = val.strip()
417
+ self.set_option_value(opt, val, 0, linidx, line, infile)
418
+ setcnt += 1
419
+ colidx += (reduced-enddiv)
420
+ if colidx == colcnt:
421
+ colidx = 0 # done with gathering values of a multi-value line
422
+ opt = None
423
+ elif opt and not self.OPTS[opt][2]&self.PGOPT['TXTBIT']:
424
+ colidx += 1
425
+ opt = None
426
+ if setcnt > 0:
427
+ if colidx:
428
+ if colidx < colcnt:
429
+ self.input_error(linidx, '', infile, "{} of {} values missed".format(colcnt-colidx, colcnt))
430
+ elif enddiv:
431
+ self.input_error(linidx, '', infile, "Miss end divider '{}'".format(self.params['DV']))
432
+ return 1 # read something
433
+ else:
434
+ if table: self.pglog("No option information found for '{}'".format(table), self.WARNLG)
435
+ return 0 # read nothing
436
+
437
+ # clean self.params for input option values when set mutiple tables
438
+ def clean_input_values(self):
439
+ # clean previously saved input values if any
440
+ for opt in self.INOPTS:
441
+ del self.params[opt]
442
+ self.INOPTS = {}
443
+
444
+ # build a hash record for add or update of a table record
445
+ def build_record(self, flds, pgrec, tname, idx = 0):
446
+ record = {}
447
+ if not flds: return record
448
+ hash = self.TBLHASH[tname]
449
+ for key in flds:
450
+ if key not in hash: continue
451
+ opt = hash[key][0]
452
+ field = hash[key][3] if len(hash[key]) == 4 else hash[key][1]
453
+ ms = re.search(r'\.(.+)$', field)
454
+ if ms: field = ms.group(1)
455
+ if opt in self.params:
456
+ if self.OPTS[opt][0] == 1:
457
+ val = self.params[opt]
458
+ else:
459
+ if self.OPTS[opt][2]&2 and pgrec and field in pgrec and pgrec[field]: continue
460
+ val = self.params[opt][idx]
461
+ sval = pgrec[field] if pgrec and field in pgrec else None
462
+ if sval is None:
463
+ if val == '': val = None
464
+ elif isinstance(sval, int):
465
+ if isinstance(val, str): val = (int(val) if val else None) # change '' to None for int
466
+ if self.pgcmp(sval, val, 1): record[field] = val # record new or changed value
467
+ return record
468
+
469
+ # set global variable self.PGOPT['UID'] with value of user.uid, fatal if unsuccessful
470
+ def set_uid(self, aname):
471
+ self.set_email_logact()
472
+ if 'LN' not in self.params:
473
+ self.params['LN'] = self.PGLOG['CURUID']
474
+ elif self.params['LN'] != self.PGLOG['CURUID']:
475
+ self.params['MD'] = 1 # make sure this set if running as another user
476
+ if 'NE' not in self.params: self.PGLOG['EMLADDR'] = self.params['LN']
477
+ if 'DM' in self.params and re.match(r'^(start|begin)$', self.params['DM'], re.I):
478
+ msg = "'{}' must start Daemon '{} -{}' as '{}'".format(self.PGLOG['CURUID'], aname, self.PGOPT['CACT'], self.params['LN'])
479
+ else:
480
+ msg = "'{}' runs '{} -{}' as '{}'!".format(self.PGLOG['CURUID'], aname, self.PGOPT['CACT'], self.params['LN'])
481
+ self.pglog(msg, self.PGOPT['wrnlog'])
482
+ self.set_specialist_environments(self.params['LN'])
483
+ if 'LN' not in self.params: self.pglog("Could not get user login name", self.PGOPT['extlog'])
484
+ self.validate_dataset()
485
+ if self.OPTS[self.PGOPT['CACT']][2] > 0: self.validate_dsowner(aname)
486
+ pgrec = self.pgget("dssdb.user", "uid", "logname = '{}' AND until_date IS NULL".format(self.params['LN']), self.PGOPT['extlog'])
487
+ if not pgrec: self.pglog("Could not get user.uid for " + self.params['LN'], self.PGOPT['extlog'])
488
+ self.PGOPT['UID'] = pgrec['uid']
489
+ self.open_output(self.params['OF'] if 'OF' in self.params else None)
490
+
491
+ # set global variable self.PGOPT['UID'] as 0 for a sudo user
492
+ def set_sudo_uid(self, aname, uid):
493
+ self.set_email_logact()
494
+ if self.PGLOG['CURUID'] != uid:
495
+ if 'DM' in self.params and re.match(r'^(start|begin)$', self.params['DM'], re.I):
496
+ msg = "'{}': must start Daemon '{} -{} as '{}'".format(self.PGLOG['CURUID'], aname, self.params['CACT'], uid)
497
+ else:
498
+ msg = "'{}': must run '{} -{}' as '{}'".format(self.PGLOG['CURUID'], aname, self.params['CACT'], uid)
499
+ self.pglog(msg, self.PGOPT['extlog'])
500
+ self.PGOPT['UID'] = 0
501
+ self.params['LN'] = self.PGLOG['CURUID']
502
+
503
+ # set global variable self.PGOPT['UID'] as 0 for root user
504
+ def set_root_uid(self, aname):
505
+ self.set_email_logact()
506
+ if self.PGLOG['CURUID'] != "root":
507
+ if 'DM' in self.params and re.match(r'^(start|begin)$', self.params['DM'], re.I):
508
+ msg = "'{}': you must start Daemon '{} -{} as 'root'".format(self.PGLOG['CURUID'], aname, self.params['CACT'])
509
+ else:
510
+ msg = "'{}': you must run '{} -{}' as 'root'".format(self.PGLOG['CURUID'], aname, self.params['CACT'])
511
+ self.pglog(msg, self.PGOPT['extlog'])
512
+ self.PGOPT['UID'] = 0
513
+ self.params['LN'] = self.PGLOG['CURUID']
514
+
515
+ # set email logging bits
516
+ def set_email_logact(self):
517
+ if 'NE' in self.params:
518
+ self.PGLOG['LOGMASK'] &= ~self.EMLALL # remove all email bits
519
+ elif 'SE' in self.params:
520
+ self.PGLOG['LOGMASK'] &= ~self.EMLLOG # no normal email
521
+
522
+ # validate dataset owner
523
+ # return: 0 or fatal if not valid, 1 if valid, -1 if can not be validated
524
+ def validate_dsowner(self, aname, dsid = None, logname = None, pgds = 0, logact = 0):
525
+ if not logname: logname = (self.params['LN'] if 'LN' in self.params else self.PGLOG['CURUID'])
526
+ if logname == self.PGLOG['GDEXUSER']: return 1
527
+ dsids = {}
528
+ if dsid:
529
+ dsids[dsid] = 1
530
+ elif 'DS' in self.params:
531
+ if self.OPTS['DS'][0] == 2:
532
+ for dsid in self.params['DS']:
533
+ dsids[dsid] = 1
534
+ else:
535
+ dsids[self.params['DS']] = 1
536
+ else:
537
+ return -1
538
+ if not pgds and 'MD' in self.params: pgds = 1
539
+ if not logact: logact = self.PGOPT['extlog']
540
+ for dsid in dsids:
541
+ if not self.pgget("dsowner", "", "dsid = '{}' AND specialist = '{}'".format(dsid, logname), self.PGOPT['extlog']):
542
+ if not self.pgget("dssgrp", "", "logname = '{}'".format(logname), self.PGOPT['extlog']):
543
+ return self.pglog("'{}' is not DSS Specialist!".format(logname), logact)
544
+ elif not pgds:
545
+ return self.pglog("'{}' not listed as Specialist of '{}'\nRun '{}' with Option -MD!".format(logname, dsid, aname), logact)
546
+ return 1
547
+
548
+ # validate dataset
549
+ def validate_dataset(self):
550
+ cnt = 1
551
+ if 'DS' in self.params:
552
+ if self.OPTS['DS'][0] == 2:
553
+ for dsid in self.params['DS']:
554
+ cnt = self.pgget("dataset", "", "dsid = '{}'".format(dsid), self.PGOPT['extlog'])
555
+ if cnt == 0: break
556
+ else:
557
+ dsid = self.params['DS']
558
+ cnt = self.pgget("dataset", "", "dsid = '{}'".format(dsid), self.PGOPT['extlog'])
559
+ if not cnt: self.pglog(dsid + " not exists in RDADB!", self.PGOPT['extlog'])
560
+
561
+ # validate given group indices or group names
562
+ def validate_groups(self, parent = 0):
563
+ if parent:
564
+ gi = 'PI'
565
+ gn = 'PN'
566
+ else:
567
+ gi = 'GI'
568
+ gn = 'GN'
569
+ if (self.OPTS[gi][2]&8): return # already validated
570
+ dcnd = "dsid = '{}'".format(self.params['DS'])
571
+ if gi in self.params:
572
+ grpcnt = len(self.params[gi])
573
+ i = 0
574
+ while i < grpcnt:
575
+ gidx = self.params[gi][i]
576
+ if not isinstance(gidx, int) and re.match(r'^(!|<|>|<>)$', gidx): break
577
+ i += 1
578
+ if i >= grpcnt: # normal group index given
579
+ for i in range(grpcnt):
580
+ gidx = self.params[gi][i]
581
+ gidx = int(gidx) if gidx else 0
582
+ self.params[gi][i] = gidx
583
+ if gidx == 0 or (i > 0 and gidx == self.params[gi][i-1]): continue
584
+ if not self.pgget("dsgroup", '', "{} AND gindex = {}".format(dcnd, gidx), self.PGOPT['extlog']):
585
+ if i > 0 and parent and self.params['GI']:
586
+ j = 0
587
+ while j < i:
588
+ if gidx == self.params['GI'][j]: break
589
+ j += 1
590
+ if j < i: continue
591
+ self.pglog("Group Index {} not in RDADB for {}".format(gidx, self.params['DS']), self.PGOPT['extlog'])
592
+ else: # found none-equal condition sign
593
+ pgrec = self.pgmget("dsgroup", "DISTINCT gindex", dcnd + self.get_field_condition("gindex", self.params[gi]), self.PGOPT['extlog'])
594
+ grpcnt = (len(pgrec['gindex']) if pgrec else 0)
595
+ if grpcnt == 0:
596
+ self.pglog("No Group matches given Group Index condition for " + self.params['DS'], self.PGOPT['extlog'])
597
+ self.params[gi] = pgrec['gindex']
598
+ elif gn in self.params:
599
+ self.params[gi] = self.group_id_to_index(self.params[gn])
600
+ self.OPTS[gi][2] |= 8 # set validated flag
601
+
602
+ # get group index array from given group IDs
603
+ def group_id_to_index(self, grpids):
604
+ count = len(grpids) if grpids else 0
605
+ if count == 0: return None
606
+ indices = []
607
+ dcnd = "dsid = '{}'".format(self.params['DS'])
608
+ i = 0
609
+ while i < count:
610
+ gid = grpids[i]
611
+ if gid and (re.match(r'^(!|<|>|<>)$', gid) or gid.find('%') > -1): break
612
+ i += 1
613
+ if i >= count: # normal group id given
614
+ for i in range(count):
615
+ gid = grpids[i]
616
+ if not gid:
617
+ indices.append(0)
618
+ elif i and gid == grpids[i-1]:
619
+ indices.append(indices[i-1])
620
+ else:
621
+ pgrec = self.pgget("dsgroup", "gindex", "{} AND grpid = '{}'".format(dcnd, gid), self.PGOPT['extlog'])
622
+ if not pgrec: self.pglog("Group ID {} not in RDADB for {}".format(gid, self.params['DS']), self.PGOPT['extlog'])
623
+ indices.append(pgrec['gindex'])
624
+ return indices
625
+ else: # found wildcard and/or none-equal condition sign
626
+ pgrec = self.pgmget("dsgroup", "DISTINCT gindex", dcnd + self.get_field_condition("grpid", grpids, 1), self.PGOPT['extlog'])
627
+ count = (len(pgrec['gindex']) if pgrec else 0)
628
+ if count == 0: self.pglog("No Group matches given Group ID condition for " + self.params['DS'], self.PGOPT['extlog'])
629
+ return pgrec['gindex']
630
+
631
+ # get group ID array from given group indices
632
+ def group_index_to_id(self, indices):
633
+ count = len(indices) if indices else 0
634
+ if count == 0: return None
635
+ grpids = []
636
+ dcnd = "dsid = '{}'".format(self.params['DS'])
637
+ i = 0
638
+ while i < count:
639
+ gidx = indices[i]
640
+ if not isinstance(gidx, int) and re.match(r'^(!|<|>|<>)$', gidx): break
641
+ i += 1
642
+ if i >= count: # normal group index given
643
+ for i in range(count):
644
+ gidx = indices[i]
645
+ if not gidx:
646
+ grpids.append('') # default value
647
+ elif i and gidx == indices[i-1]:
648
+ grpids.append(grpids[i-1])
649
+ else:
650
+ pgrec = self.pgget("dsgroup", "grpid", "{} AND gindex = {}".format(dcnd, gidx), self.PGOPT['extlog'])
651
+ if not pgrec: self.pglog("Group Index {} not in RDADB for {}".format(gidx, self.params['DS']), self.PGOPT['extlog'])
652
+ grpids.append(pgrec['grpid'])
653
+ return grpids
654
+ else: # found none-equal condition sign
655
+ pgrec = self.pgmget("dsgroup", "DISTINCT grpid", dcnd + self.get_field_condition("gindex", indices), self.PGOPT['extlog'])
656
+ count = (len(pgrec['grpid']) if pgrec else 0)
657
+ if count == 0: self.pglog("No Group matches given Group Index condition for " + self.params['DS'], self.PGOPT['extlog'])
658
+ return pgrec['grpid']
659
+
660
+ # validate order fields and
661
+ # get an array of order fields that are not in given fields
662
+ def append_order_fields(self, oflds, flds, tname, excludes = None):
663
+ orders = ''
664
+ hash = self.TBLHASH[tname]
665
+ for ofld in oflds:
666
+ ufld = ofld.upper()
667
+ if ufld not in hash or excludes and excludes.find(ufld) > -1: continue
668
+ if flds and flds.find(ufld) > -1: continue
669
+ orders += ofld
670
+ return orders
671
+
672
+ # validate mutiple values for given fields
673
+ def validate_multiple_values(self, tname, count, flds = None):
674
+ opts = []
675
+ hash = self.TBLHASH[tname]
676
+ if flds:
677
+ for fld in flds:
678
+ if fld in hash: opts.append(hash[fld][0])
679
+ else:
680
+ for fld in hash:
681
+ opts.append(hash[fld][0])
682
+ self.validate_multiple_options(count, opts, (1 if tname == 'htarfile' else 0))
683
+
684
+ # validate multiple values for given options
685
+ def validate_multiple_options(self, count, opts, remove = 0):
686
+ for opt in opts:
687
+ if opt not in self.params or self.OPTS[opt][0] != 2: continue # no value given or not multiple value option
688
+ cnt = len(self.params[opt])
689
+ if cnt == 1 and count > 1 and self.OPTS[opt][2]&1:
690
+ val0 = self.params[opt][0]
691
+ self.params[opt] = [val0]*count
692
+ self.OPTS[opt][2] |= 4 # expanded
693
+ cnt = count
694
+ if cnt != count:
695
+ if count == 1 and cnt > 1 and self.OPTS[opt][2]&self.PGOPT['TXTBIT']:
696
+ self.params[opt][0] = ' '.join(self.params[opt])
697
+ elif remove and cnt == 1 and count > 1:
698
+ del self.params[opt]
699
+ elif cnt < count:
700
+ self.pglog("Multi-value Option {}({}): {} Given and {} needed".format(opt, self.OPTS[opt][1], cnt, count), self.PGOPT['extlog'])
701
+
702
+ # get field keys for a RDADB table, include all if !include
703
+ def get_field_keys(self, tname, include = None, exclude = None):
704
+ fields = ''
705
+ hash = self.TBLHASH[tname]
706
+ for fld in hash:
707
+ if include and include.find(fld) < 0: continue
708
+ if exclude and exclude.find(fld) > -1: continue
709
+ opt = hash[fld][0]
710
+ if opt in self.params: fields += fld
711
+ return fields if fields else None
712
+
713
+ # get a string for fields of a RDADB table
714
+ def get_string_fields(self, flds, tname, include = None, exclude = None):
715
+ fields = []
716
+ hash = self.TBLHASH[tname]
717
+ for fld in flds:
718
+ ufld = fld.upper() # in case
719
+ if include and include.find(ufld) < 0: continue
720
+ if exclude and exclude.find(ufld) > -1: continue
721
+ if ufld not in hash:
722
+ self.pglog("Invalid field '{}' to get from '{}'".format(fld, tname), self.PGOPT['extlog'])
723
+ elif hash[ufld][0] not in self.OPTS:
724
+ self.pglog("Option '{}' is not defined for field '{} - {}'".format(hash[ufld][0], ufld, hash[ufld][1]), self.PGOPT['extlog'])
725
+ if len(hash[ufld]) == 4:
726
+ fname = "{} {}".format(hash[ufld][3], hash[ufld][1])
727
+ else:
728
+ fname = hash[ufld][1]
729
+ fields.append(fname)
730
+ return ', '.join(fields)
731
+
732
+ # get max count for given options
733
+ def get_max_count(self, opts):
734
+ count = 0
735
+ for opt in opts:
736
+ if opt not in self.params: continue
737
+ cnt = len(self.params[opt])
738
+ if cnt > count: count = cnt
739
+ return count
740
+
741
+ # get a string of fields of a RDADB table for sorting
742
+ def get_order_string(self, flds, tname, exclude = None):
743
+ orders = []
744
+ hash = self.TBLHASH[tname]
745
+ for fld in flds:
746
+ if fld.islower():
747
+ desc = " DESC"
748
+ fld = fld.upper()
749
+ else:
750
+ desc = ""
751
+ if exclude and exclude.find(fld) > -1: continue
752
+ orders.append(hash[fld][1] + desc)
753
+ return (" ORDER BY " + ', '.join(orders)) if orders else ''
754
+
755
+ # get a string for column titles of a given table
756
+ def get_string_titles(self, flds, hash, lens):
757
+ titles = []
758
+ colcnt = len(flds)
759
+ for i in range(colcnt):
760
+ fld = flds[i]
761
+ if fld not in hash: continue
762
+ opt = hash[fld][0]
763
+ if opt not in self.OPTS: self.pglog("ERROR: Undefined option " + opt, self.PGOPT['extlog'])
764
+ title = self.OPTS[opt][1]
765
+ if lens:
766
+ if len(title) > lens[i]: title = opt
767
+ title = "{:{}}".format(title, lens[i])
768
+ titles.append(title)
769
+ return self.params['DV'].join(titles) + self.params['DV']
770
+
771
+ # display error message and exit
772
+ def parameter_error(self, p, opt = None, lidx = 0, line = 0, infile = None):
773
+ if not opt:
774
+ errmsg = "value passed in without leading info option"
775
+ elif opt == "continue":
776
+ errmsg = "error processing input file on continue Line"
777
+ elif opt == 'specified':
778
+ errmsg = "option -{}/-{} is specified already".format(p, self.OPTS[p][1])
779
+ elif opt == "mixed":
780
+ errmsg = "single-value option mixed with multi-value option"
781
+ elif opt == "missact":
782
+ errmsg = "No Action Option is specified"
783
+ elif opt == "missval":
784
+ errmsg = "No value provided following Info Option"
785
+ elif opt == 'duplicate':
786
+ errmsg = "multiple actions not allowed"
787
+ elif opt == "delayed":
788
+ errmsg = "delayed Mode option not supported"
789
+ elif self.OPTS[opt][0] == 0:
790
+ errmsg = "value follows Mode Option -{}/-{}".format(opt, self.OPTS[opt][1])
791
+ elif self.OPTS[opt][0] == 1:
792
+ errmsg = "multiple values follow single-value Option -{}/-{}".format(opt, self.OPTS[opt][1])
793
+ elif self.OPTS[opt][0] >= 4:
794
+ errmsg = "value follows Action Option -{}/-{}".format(opt, self.OPTS[opt][1])
795
+ else:
796
+ errmsg = None
797
+ if errmsg:
798
+ if lidx:
799
+ self.input_error(lidx, line, infile, "{} - {}".format(p, errmsg))
800
+ else:
801
+ self.pglog("ERROR: {} - {}".format(p, errmsg), self.PGOPT['extlog'])
802
+
803
+ # wrap function to self.pglog() for error in input files
804
+ def input_error(self, lidx, line, infile, errmsg):
805
+ self.pglog("ERROR at {}({}): {}\n {}".format(infile, lidx, line, errmsg), self.PGOPT['extlog'])
806
+
807
+ # wrap function to self.pglog() for error for action
808
+ def action_error(self, errmsg, cact = None):
809
+ msg = "ERROR"
810
+ if self.PGOPT['ANAME']: msg += " " + self.PGOPT['ANAME']
811
+ if not cact: cact = self.PGOPT['CACT']
812
+ if cact: msg += " for Action {} ({})".format(cact, self.OPTS[cact][1])
813
+ if 'DS' in self.params:
814
+ if self.OPTS['DS'][0] == 1:
815
+ msg += " of " + self.params['DS']
816
+ elif self.OPTS['DS'][0] == 2 and len(self.params['DS']) == 1:
817
+ msg += " of " + self.params['DS'][0]
818
+ msg += ": " + errmsg
819
+ if self.PGLOG['DSCHECK']: self.record_dscheck_error(msg, self.PGOPT['extlog'])
820
+ self.pglog(msg, self.PGOPT['extlog'])
821
+
822
+ # get the valid option for given parameter by checking if the given option
823
+ # name matches either an valid option key (short name) or its long name
824
+ # flag: 1 - value key only, 2 - multi-value key only, 3 - action key only,
825
+ # 4 - mode&action key only
826
+ def get_option_key(self, p, flag = 0, skip = 0, lidx = 0, line = None, infile = None, table = None):
827
+ if p is None: p = ''
828
+ opt = self.get_short_option(p)
829
+ errmsg = None
830
+ if opt:
831
+ if flag == 1:
832
+ if self.OPTS[opt][0]&3 == 0: errmsg = "NOT a Value Option"
833
+ elif flag == 2:
834
+ if self.OPTS[opt][0]&2 == 0: errmsg = "NOT a Multi-Value Option"
835
+ elif flag == 3:
836
+ if self.OPTS[opt][0] < 4:
837
+ if lidx:
838
+ errmsg = "NOT an Action Option"
839
+ else:
840
+ errmsg = "Miss leading '-' for none action option"
841
+ elif flag == 4:
842
+ if self.OPTS[opt][0]&3:
843
+ errmsg = "NOT a Mode/Action Option"
844
+ if errmsg: errmsg = "{}({}) - {}".format(opt, self.OPTS[opt][1], errmsg)
845
+ elif not skip:
846
+ if p:
847
+ errmsg = "-{} - Unknown Option".format(p)
848
+ else:
849
+ errmsg = "'' - Empty Option Name"
850
+ if errmsg:
851
+ if lidx:
852
+ self.input_error(lidx, line, infile, errmsg)
853
+ else:
854
+ self.pglog("ERROR: " + errmsg, self.PGOPT['extlog'])
855
+ elif opt and (table or self.PGOPT['IFCNT'] and self.OPTS[opt][0] == 2):
856
+ self.INOPTS[opt] = 1
857
+ return opt
858
+
859
+ # set values to given options, ignore options set in input files if the options
860
+ # already set on command line
861
+ def set_option_value(self, opt, val = None, cnl = 0, lidx = 0, line = None, infile = None):
862
+ if opt in self.CMDOPTS and lidx: # in input file, but given on command line already
863
+ if opt not in self.params: self.params[opt] = self.CMDOPTS[opt]
864
+ return
865
+ if val is None: val = ''
866
+ if self.OPTS[opt][0]&3:
867
+ if self.OPTS[opt][2]&16:
868
+ if not val:
869
+ val = 0
870
+ elif re.match(r'^\d+$', val):
871
+ val = int(val)
872
+ elif val and (opt == 'DS' or opt == 'OD'):
873
+ val = self.format_dataset_id(val)
874
+ errmsg = None
875
+ if not cnl and self.OPTS[opt][0]&3:
876
+ if opt in self.params:
877
+ if self.OPTS[opt][0] == 2:
878
+ if self.OPTS[opt][2]&2: del self.params[opt] # clean auto set values
879
+ elif self.params[opt] != val and not self.OPTS[opt][2]&1:
880
+ errmsg = "'{}', multiple values not allowed for Single-Value Option".format(val)
881
+ if not errmsg and (not self.PGOPT['CACT'] or self.OPTS[self.PGOPT['CACT']][2]):
882
+ dstr = self.OPTS[opt][3] if len(self.OPTS[opt]) > 3 else None
883
+ if dstr:
884
+ vlen = len(val)
885
+ ms = re.match(r'^!(\w*)', dstr)
886
+ if ms:
887
+ dstr = ms.group(1)
888
+ if vlen == 1 and dstr.find(val) > -1: errmsg = "{}: character must not be one of '{}'".format(val, str)
889
+ elif vlen > 1 or (vlen == 0 and not self.OPTS[opt][2]&128) or (vlen == 1 and dstr.find(val) < 0):
890
+ errmsg = "{} single-letter value must be one of '{}'".format(val, dstr)
891
+ if not errmsg:
892
+ if self.OPTS[opt][0] == 2: # multiple value option
893
+ if opt not in self.params:
894
+ self.params[opt] = [val] # set the first value
895
+ if opt == 'QF' and self.PGOPT['ACTS'] == self.OPTS['DL'][0]: self.OPTS['FS'][3] = 'ANT'
896
+ else:
897
+ if cnl:
898
+ rowidx = len(self.params[opt]) - 1
899
+ if self.params[opt][rowidx]:
900
+ if not re.match(r'^(DE|DI|DM|DW)$', opt):
901
+ errmsg = "Multi-line value not allowed"
902
+ else:
903
+ self.params[opt][rowidx] += "\n" + val # multiple line value
904
+ else:
905
+ self.params[opt][rowidx] = val
906
+ else:
907
+ self.params[opt].append(val) # add next value
908
+ elif self.OPTS[opt][0] == 1: # single value option
909
+ if cnl and opt in self.params:
910
+ if val: errmsg = "Multi-line value not allowed"
911
+ elif self.OPTS[opt][2]&2 and self.pgcmp(self.params[opt], val):
912
+ errmsg = "{}: Single-Value Info Option has value '{}' already".format(val, self.params[opt])
913
+ else:
914
+ self.params[opt] = val
915
+ self.OPTS[opt][2] |= 2
916
+ elif val:
917
+ if self.OPTS[opt][0] == 0 and re.match(r'^(Y|N)$', val, re.I):
918
+ self.params[opt] = 1 if (val == 'Y' or val == 'y') else 0
919
+ else:
920
+ self.parameter_error(val, opt, lidx, line, infile) # no value for flag or action options
921
+ elif opt not in self.params:
922
+ self.params[opt] = 1 # set flag or action option
923
+ if self.OPTS[opt][0] > 2:
924
+ if self.PGOPT['ACTS']: self.parameter_error(opt, "duplicate", lidx ,line, infile) # no duplicated action options
925
+ self.PGOPT['ACTS'] = self.OPTS[opt][0] # add action bit
926
+ self.PGOPT['CACT'] = opt # add action name
927
+ if opt == "SB": self.PGOPT['MSET'] = opt
928
+ if errmsg:
929
+ if lidx:
930
+ self.input_error(lidx, line, infile, "{}({}) - {}".format(opt, self.OPTS[opt][1], errmsg))
931
+ else:
932
+ self.pglog("ERROR: {}({}) - {}".format(opt, self.OPTS[opt][1], errmsg), self.PGOPT['extlog'])
933
+ if not lidx: self.CMDOPTS[opt] = self.params[opt] # record options set on command lines
934
+
935
+ # get width for a single row if in column format
936
+ def get_row_width(self, pgrec):
937
+ slen = len(self.params['DV'])
938
+ width = 0
939
+ for key in pgrec:
940
+ wd = 0
941
+ for val in pgrec[key]:
942
+ if not val: continue
943
+ if not isinstance(val, str): val = str(val)
944
+ if key == 'note':
945
+ vlen = val.find('\n') + 1
946
+ else:
947
+ vlen = 0
948
+ if vlen < 1: vlen = len(val)
949
+ if vlen > wd: wd = vlen # get max width of each column
950
+ # accumulate all column width plus length of delimiter to get row width
951
+ if width: width += slen
952
+ width += wd
953
+ return width
954
+
955
+ # get a short option name by searching dict self.OPTS and self.ALIAS
956
+ def get_short_option(self, p):
957
+ plen = len(p)
958
+ if plen == 2:
959
+ p = p.upper()
960
+ if p in self.OPTS: return p
961
+ for opt in self.OPTS: # get main option first
962
+ if not self.pgcmp(self.OPTS[opt][1], p, 1): return opt
963
+ for opt in self.ALIAS: # then check alias option
964
+ for key in self.ALIAS[opt]:
965
+ if not self.pgcmp(key, p, 1): return opt
966
+ return None
967
+
968
+ # print result in column format, with multiple values each row
969
+ def print_column_format(self, pgrec, flds, hash, lens, retbuf = 0):
970
+ rowcnt = -1
971
+ colcnt = len(flds)
972
+ buf = ''
973
+ fields = []
974
+ flens = []
975
+ for i in range(colcnt):
976
+ fld = flds[i]
977
+ if fld in hash:
978
+ fld = hash[fld][1]
979
+ ms = re.search(r'\.(.+)$', fld)
980
+ if ms: fld = ms.group(1)
981
+ if fld in pgrec:
982
+ fields.append(fld)
983
+ flens.append((lens[i] if lens else 0))
984
+ if rowcnt < 0: rowcnt = len(pgrec[fld])
985
+ else:
986
+ self.pglog(fld + ": Unkown field name", self.PGOPT['extlog'])
987
+ colcnt = len(fields)
988
+ for i in range(rowcnt):
989
+ offset = 0
990
+ values = []
991
+ for j in range(colcnt):
992
+ fld = fields[j]
993
+ idx = -1
994
+ val = pgrec[fld][i]
995
+ slen = flens[j]
996
+ if val is None:
997
+ val = ''
998
+ elif isinstance(val, str):
999
+ idx = val.find("\n")
1000
+ if idx > 0:
1001
+ val = "\n" + val
1002
+ idx = 0
1003
+ else:
1004
+ val = str(val)
1005
+ if slen:
1006
+ if idx < 0:
1007
+ val = "{:{}}".format(val, slen)
1008
+ else:
1009
+ val += "\n{:{}}".format(' ', offset)
1010
+ offset += slen
1011
+ values.append(val)
1012
+ line = self.params['DV'].join(values) + self.params['DV'] + "\n"
1013
+ if retbuf:
1014
+ buf += line
1015
+ else:
1016
+ self.OUTPUT.write(line)
1017
+ return buf if retbuf else rowcnt
1018
+
1019
+ # print result in row format, with single value on each row
1020
+ def print_row_format(self, pgrec, flds, hash):
1021
+ for fld in flds:
1022
+ if fld not in hash: continue
1023
+ line = "{}{}".format(self.OPTS[hash[fld][0]][1], self.params['ES'])
1024
+ field = hash[fld][1]
1025
+ ms = re.search(r'\.(.+)$', field)
1026
+ if ms: field = ms.group(1)
1027
+ if field in pgrec:
1028
+ value = pgrec[field]
1029
+ if value is not None: line += str(value)
1030
+ self.OUTPUT.write(line + "\n")
1031
+
1032
+ # compress/uncompress given files and change the formats accordingly
1033
+ def compress_files(self, files, formats, count):
1034
+ if 'UZ' in self.params:
1035
+ strcmp = 'Uncompress'
1036
+ actcmp = 0
1037
+ else:
1038
+ strcmp = 'Compress'
1039
+ actcmp = 1
1040
+ fmtcnt = len(formats)
1041
+ if not fmtcnt: return files # just in case
1042
+ s = 's' if count > 1 else ''
1043
+ self.pglog("{}ing {} File{} for {} ...".format(strcmp, count, s, self.params['DS']), self.PGOPT['wrnlog'])
1044
+ cmpcnt = 0
1045
+ for i in range(count):
1046
+ fmt = formats[i] if(i < fmtcnt and formats[i]) else formats[0]
1047
+ (ofile, fmt) = self.compress_local_file(files[i], fmt, actcmp, self.PGOPT['extlog'])
1048
+ if ofile != files[i]:
1049
+ files[i] = ofile
1050
+ cmpcnt += 1
1051
+ self.pglog("{}/{} Files {}ed for {}".format(cmpcnt, count, strcmp, self.params['DS']) , self.PGOPT['emllog'])
1052
+ if 'ZD' in self.params: del self.params['ZD']
1053
+ if 'UZ' in self.params: del self.params['UZ']
1054
+ return files
1055
+
1056
+ # get hash condition
1057
+ # tname - table name to identify a table hash
1058
+ # noand - 1 for not add leading 'AND'
1059
+ def get_hash_condition(self, tname, include = None, exclude = None, noand = 0):
1060
+ condition = ''
1061
+ hash = self.TBLHASH[tname]
1062
+ for key in hash:
1063
+ if include and include.find(key) < 0: continue
1064
+ if exclude and exclude.find(key) > -1: continue
1065
+ opt = hash[key][0]
1066
+ if opt not in self.params: continue # no option value
1067
+ flg = hash[key][2]
1068
+ if flg < 0: # condition is ignore for this option
1069
+ self.pglog("Condition given per Option -{} (-{}) is ignored".format(opt, self.OPTS[opt][1]), self.PGOPT['errlog'])
1070
+ continue
1071
+ fld = hash[key][1]
1072
+ condition += self.get_field_condition(fld, self.params[opt], flg, noand)
1073
+ noand = 0
1074
+ return condition
1075
+
1076
+ # set default self.params value for given opt empty the value if 'all' is given
1077
+ def set_default_value(self, opt, dval = None):
1078
+ flag = self.OPTS[opt][0]
1079
+ if flag&3 == 0: return # skip if not single&multiple value options
1080
+ oval = 0
1081
+ if opt in self.params:
1082
+ if flag == 1:
1083
+ oval = self.params[opt]
1084
+ else:
1085
+ count = len(self.params[opt])
1086
+ if count == 1:
1087
+ oval = self.params[opt][0]
1088
+ elif count > 1:
1089
+ return # multiple values given already
1090
+ if oval:
1091
+ if re.match(r'^all$', oval, re.I):
1092
+ del self.params[opt] # remove option value for all
1093
+ return # value given already
1094
+ if dval:
1095
+ # set default value
1096
+ if flag == 1:
1097
+ self.params[opt] = dval
1098
+ else:
1099
+ self.params[opt] = [dval]
1100
+
1101
+ # add/strip COS block for give file name and cosflg if given/not-given cosfile
1102
+ # return the file size after the convertion
1103
+ def cos_convert(self, locfile, cosflg, cosfile = None):
1104
+ if cosfile:
1105
+ cmd = "cosconvert -{} {} {}".format(cosflg, cosfile, locfile)
1106
+ else:
1107
+ cmd = "cosconvert -{} {}".format(cosflg.lower(), locfile)
1108
+ cosfile = locfile
1109
+ self.pgsystem(cmd)
1110
+ info = self.check_local_file(cosfile)
1111
+ if not info:
1112
+ return self.pglog("Error - " + cmd, self.PGOPT['errlog']) # should not happen
1113
+ else:
1114
+ return info['data_size']
1115
+
1116
+ # evaluate count of values for given options
1117
+ def get_option_count(self, opts):
1118
+ count = 0
1119
+ for opt in opts:
1120
+ if opt in self.params:
1121
+ cnt = len(self.params[opt])
1122
+ if cnt > count: count = cnt
1123
+ if count > 0: self.validate_multiple_options(count, opts)
1124
+ return count
1125
+
1126
+ # gather subgroup indices recursively for given condition
1127
+ # dsid: Dataset Id
1128
+ # pidx: parent group index
1129
+ # gtype: group type if not empty, P - public groups only)
1130
+ # Return: array reference of group indices
1131
+ def get_all_subgroups(self, dcnd, pidx, gtype = None):
1132
+ gidxs = [pidx]
1133
+ gflds = "gindex"
1134
+ if gtype: gflds += ", grptype"
1135
+ grecs = self.pgmget("dsgroup", gflds, "{} and pindex = {}".format(dcnd, pidx), self.LGWNEX)
1136
+ if not grecs: return gidxs
1137
+ gcnt = len(grecs['gindex'])
1138
+ for i in range(gcnt):
1139
+ gidx = grecs['gindex'][i]
1140
+ if abs(gidx) <= abs(pidx) or gtype and grecs['grptype'][i] != gtype: continue
1141
+ subs = self.get_all_subgroups(dcnd, gidx, gtype)
1142
+ gidxs.extend(subs)
1143
+ return gidxs
1144
+
1145
+ # gather public subgroup indices recursively for given condition. A group index is
1146
+ # gathered only if there are data files right under it. The pidx is included too
1147
+ # if file count of it larger then zero.
1148
+ # dsid: Dataset Id
1149
+ # pidx: parent group index
1150
+ # cfld: count field (dwebcnt, nwebcnt, savedcnt)
1151
+ # pfcnt: file count for parent group index pidx 0 to skip)
1152
+ # Return: array reference of group indices
1153
+ def get_data_subgroups(self, dcnd, pidx, cfld, pfcnt = 0):
1154
+ if not pfcnt: # get file count for the parent group
1155
+ pfcnt = self.group_file_count(dcnd, pidx, cfld)
1156
+ if not pfcnt: return None
1157
+ gflds = "gindex, " + cfld
1158
+ gcnd = "{} AND pindex = {} AND {} > 0".format(dcnd, pidx, cfld)
1159
+ grecs = self.pgmget("dsgroup", gflds, gcnd, self.LGWNEX)
1160
+ if not grecs: return ([pidx] if pfcnt > 0 else None)
1161
+ gcnt = len(grecs['gindex'])
1162
+ gidxs = []
1163
+ for i in range(gcnt):
1164
+ gidx = grecs['gindex'][i]
1165
+ fcnt = grecs[cfld][i]
1166
+ if fcnt == 0 or abs(gidx) <= abs(pidx): continue
1167
+ subs = self.get_data_subgroups(dcnd, gidx, cfld, fcnt)
1168
+ if subs: gidxs.extend(subs)
1169
+ pfcnt -= fcnt
1170
+ if pfcnt > 0: gidxs.insert(0, pidx)
1171
+ return (gidxs if gidxs else None)
1172
+
1173
+ # get group file count for given count field name
1174
+ def group_file_count(self, cnd, gidx, cfld):
1175
+ if gidx:
1176
+ table = "dsgroup"
1177
+ cnd += " AND gindex = {}".format(gidx)
1178
+ else:
1179
+ table = "dataset"
1180
+ pgrec = self.pgget(table, cfld, cnd)
1181
+ return (pgrec[cfld] if pgrec else 0)
1182
+
1183
+ # set file format for actions -AM/-AW from given local files
1184
+ def set_file_format(self, count):
1185
+ if 'LF' in self.params:
1186
+ files = self.params['LF']
1187
+ else:
1188
+ return
1189
+ fmtcnt = 0
1190
+ fmts = [None] * count
1191
+ for i in range(count):
1192
+ fmt = self.get_file_format(files[i])
1193
+ if fmt:
1194
+ fmtcnt += 1
1195
+ fmts[i] = fmt
1196
+ if fmtcnt:
1197
+ self.params['AF'] = fmts
1198
+ self.OPTS['AF'][2] |= 2
1199
+
1200
+ # get frequency information
1201
+ @staticmethod
1202
+ def get_control_frequency(frequency):
1203
+ val = nf = 0
1204
+ unit = None
1205
+ ms = re.match(r'^(\d+)([YMWDHNS])$', frequency, re.I)
1206
+ if ms:
1207
+ val = int(ms.group(1))
1208
+ unit = ms.group(2).upper()
1209
+ else:
1210
+ ms = re.match(r'^(\d+)M/(\d+)', frequency, re.I)
1211
+ if ms:
1212
+ val = int(ms.group(1))
1213
+ nf = int(ms.group(2))
1214
+ unit = 'M'
1215
+ if nf < 2 or nf > 10 or (30%nf): val = 0
1216
+ if not val:
1217
+ if nf:
1218
+ unit = "fraction of month frequency '{}' MUST be (2,3,5,6,10)".format(frequency)
1219
+ elif unit:
1220
+ val = "frequency '{}' MUST be larger than 0".format(frequency)
1221
+ elif re.search(r'/(\d+)$', frequency):
1222
+ val = "fractional frequency '{}' for month ONLY".format(frequency)
1223
+ else:
1224
+ val = "invalid frequency '{}', unit must be (Y,M,W,D,H)".format(frequency)
1225
+ return (None, unit)
1226
+ freq = [0]*7 # initialize the frequence list
1227
+ uidx = {'Y' : 0, 'D' : 2, 'H' : 3, 'N' : 4, 'S' : 5}
1228
+ if unit == 'M':
1229
+ freq[1] = val
1230
+ if nf: freq[6] = nf # number of fractions in a month
1231
+ elif unit == 'W':
1232
+ freq[2] = 7 * val
1233
+ elif unit in uidx:
1234
+ freq[uidx[unit]] = val
1235
+ return (freq, unit)
1236
+
1237
+ # check if valid data time for given pindex
1238
+ def valid_data_time(self, pgrec, cstr = None, logact = 0):
1239
+ if pgrec['pindex'] and pgrec['datatime']:
1240
+ (freq, unit) = self.get_control_frequency(pgrec['frequency'])
1241
+ if not freq:
1242
+ if cstr: self.pglog("{}: {}".format(cstr, unit), logact)
1243
+ return self.FAILURE
1244
+ dtime = self.adddatetime(pgrec['datatime'], freq[0], freq[1], freq[2], freq[3], freq[4], freq[5], freq[6])
1245
+ if self.pgget("dcupdt", "", "cindex = {} AND datatime < '{}'".format(pgrec['pindex'], dtime), self.PGOPT['extlog']):
1246
+ if cstr: self.pglog("{}: MUST be processed After Control Index {}".format(cstr, pgrec['pindex']), logact)
1247
+ return self.FAILURE
1248
+ return self.SUCCESS
1249
+
1250
+ # publish filelists for given datasets
1251
+ def publish_dataset_filelist(self, dsids):
1252
+ for dsid in dsids:
1253
+ self.pgsystem("publish_filelist " + dsid, self.PGOPT['wrnlog'], 7)
1254
+
1255
+ # get the current active version index for given dsid
1256
+ def get_version_index(self, dsid, logact = 0):
1257
+ pgrec = self.pgget("dsvrsn", "vindex", "dsid = '{}' AND status = 'A'".format(dsid), logact)
1258
+ return (pgrec['vindex'] if pgrec else 0)
1259
+
1260
+ # append given format (data or archive) sfmt to format string sformat
1261
+ @staticmethod
1262
+ def append_format_string(sformat, sfmt, chkend = 0):
1263
+ mp = r'(^|\.){}$' if chkend else r'(^|\.){}(\.|$)'
1264
+ if sfmt:
1265
+ if not sformat:
1266
+ sformat = sfmt
1267
+ else:
1268
+ for fmt in re.split(r'\.', sfmt):
1269
+ if not re.search(mp.format(fmt), sformat, re.I): sformat += '.' + fmt
1270
+ return sformat
1271
+
1272
+ # get request type string or shared info
1273
+ @staticmethod
1274
+ def request_type(rtype, idx = 0):
1275
+ RTYPE = {
1276
+ 'C' : ["Customized Data", 0],
1277
+ 'D' : ["CDP Link", 0],
1278
+ 'M' : ["Delayed Mode Data", 1],
1279
+ 'N' : ["NCARDAP(THREDDS) Data Server", 0],
1280
+ 'Q' : ["Database Query", 0],
1281
+ 'R' : ["Realtime Data", 0],
1282
+ 'S' : ["Subset Data", 0],
1283
+ 'T' : ["Subset/Format-Conversion Data", 0],
1284
+ 'F' : ["Format Conversion Data", 1], # web
1285
+ 'A' : ["Archive Format Conversion", 1], # web
1286
+ 'P' : ["Plot Chart", 0],
1287
+ 'U' : ["Data", 0]
1288
+ }
1289
+ if rtype not in RTYPE: rtype = 'U'
1290
+ return RTYPE[rtype][idx]
1291
+
1292
+ # email notice of for user
1293
+ def send_request_email_notice(self, pgrqst, errmsg, fcount, rstat, readyfile = None, pgpart = None):
1294
+ pgcntl = self.PGOPT['RCNTL']
1295
+ rhome = self.params['WH'] if 'WH' in self.params and self.params['WH'] else self.PGLOG['RQSTHOME']
1296
+ if errmsg:
1297
+ if pgpart:
1298
+ if self.cache_partition_email_error(pgpart['rindex'], errmsg): return rstat
1299
+ enote = "email_part_error"
1300
+ else:
1301
+ enote = "email_error"
1302
+ elif fcount == 0:
1303
+ if pgcntl and pgcntl['empty_out'] == 'Y':
1304
+ enote = "email_empty"
1305
+ else:
1306
+ errmsg = "NO output data generated"
1307
+ if pgpart:
1308
+ if self.cache_partition_email_error(pgpart['rindex'], errmsg): return rstat
1309
+ enote = "email_part_error"
1310
+ else:
1311
+ enote = "email_error"
1312
+ elif 'EN' in self.params and self.params['EN'][0]:
1313
+ enote = self.params['EN'][0]
1314
+ elif pgrqst['enotice']:
1315
+ enote = pgrqst['enotice']
1316
+ elif pgcntl and pgcntl['enotice']:
1317
+ enote = pgcntl['enotice']
1318
+ elif pgrqst['globus_transfer'] == 'Y' and pgrqst['task_id']:
1319
+ enote = "email_notice_globus"
1320
+ else:
1321
+ enote = "email_" + ("command" if pgrqst['location'] else "notice")
1322
+ if enote[0] not in '/.': enote = "{}/notices/{}".format(rhome, enote)
1323
+ finfo = self.check_local_file(enote, 128)
1324
+ if not finfo:
1325
+ if finfo is None:
1326
+ ferror = "file not exists"
1327
+ else:
1328
+ ferror = "Error check file"
1329
+ else:
1330
+ ef = open(enote, 'r') # open email notice file
1331
+ ferror = None
1332
+ if ferror:
1333
+ if errmsg:
1334
+ self.pglog("{}: {}\nCannot email error to {}@ucar.edu: {}".format(enote, ferror, self.PGLOG['CURUID'], errmsg),
1335
+ (self.PGOPT['errlog'] if rstat else self.PGOPT['extlog']))
1336
+ return "E"
1337
+ else:
1338
+ errmsg = self.pglog("{}: {}\nCannot email notice to {}".format(enote, ferror, pgrqst['email']), self.PGOPT['errlog']|self.RETMSG)
1339
+ enote = rhome + "/notices/email_error"
1340
+ ef = open(enote, 'r')
1341
+ rstat = 'E'
1342
+ ebuf = ''
1343
+ ebuf += ef.read()
1344
+ ef.close()
1345
+ einfo = {}
1346
+ einfo['HOSTNAME'] = self.PGLOG['HOSTNAME']
1347
+ einfo['DSID'] = pgrqst['dsid']
1348
+ einfo['DSSURL'] = self.PGLOG['DSSURL']
1349
+ if pgrqst['location']:
1350
+ einfo['WHOME'] = pgrqst['location']
1351
+ else:
1352
+ einfo['WHOME'] = self.PGLOG['RQSTURL']
1353
+ einfo['SENDER'] = pgrqst['specialist'] + "@ucar.edu"
1354
+ einfo['RECEIVER'] = pgrqst['email']
1355
+ einfo['RTYPE'] = self.request_type(pgrqst['rqsttype'])
1356
+ self.add_carbon_copy() # clean carbon copy email in case not empty
1357
+ exclude = (einfo['SENDER'] if errmsg else einfo['RECEIVER'])
1358
+ if not errmsg and pgcntl and pgcntl['ccemail']:
1359
+ self.add_carbon_copy(pgcntl['ccemail'], 1, exclude, pgrqst['specialist'])
1360
+ if self.PGLOG['CURUID'] != pgrqst['specialist'] and self.PGLOG['CURUID'] != self.PGLOG['GDEXUSER']:
1361
+ self.add_carbon_copy(self.PGLOG['CURUID'], 1, exclude)
1362
+ if 'CC' in self.params: self.add_carbon_copy(self.params['CC'], 0, exclude)
1363
+ einfo['CCD'] = self.PGLOG['CCDADDR']
1364
+ einfo['RINDEX'] = str(pgrqst['rindex'])
1365
+ einfo['RQSTID'] = pgrqst['rqstid']
1366
+ pgrec = self.pgget("dataset", "title", "dsid = '{}'".format(pgrqst['dsid']), self.PGOPT['extlog'])
1367
+ einfo['DSTITLE'] = pgrec['title'] if pgrec and pgrec['title'] else ''
1368
+ einfo['SUBJECT'] = ''
1369
+ if errmsg:
1370
+ einfo['ERRMSG'] = self.get_error_command(int(time.time()), self.PGOPT['errlog']) + errmsg
1371
+ einfo['SUBJECT'] = "Error "
1372
+ if pgpart:
1373
+ einfo['PARTITION'] = " partition"
1374
+ einfo['PTIDX'] = "(PTIDX{})".format(pgpart['pindex'])
1375
+ einfo['SUBJECT'] += "Process Partitions of "
1376
+ else:
1377
+ einfo['PARTITION'] = einfo['PTIDX'] = ''
1378
+ einfo['SUBJECT'] += "Build "
1379
+ einfo['SUBJECT'] += "{} Rqst{} from {}".format(einfo['RTYPE'], pgrqst['rindex'], pgrqst['dsid'])
1380
+ else:
1381
+ if fcount == 0:
1382
+ einfo['SUBJECT'] += "NO Output:"
1383
+ else:
1384
+ einfo['SUBJECT'] += "Completed:"
1385
+ einfo['DAYS'] = str(self.PGOPT['VP'])
1386
+ pgrec = self.pgget("dssgrp", "lstname, fstname, phoneno",
1387
+ "logname = '{}'".format(self.PGLOG['CURUID']), self.PGOPT['extlog'])
1388
+ if pgrec:
1389
+ einfo['SPECIALIST'] = "{} {}".format(pgrec['fstname'], pgrec['lstname'])
1390
+ einfo['PHONENO'] = pgrec['phoneno']
1391
+ einfo['SUBJECT'] += f" {pgrqst['dsid']} {einfo['RTYPE']} request {pgrqst['rindex']}"
1392
+ if pgrqst['note']:
1393
+ einfo['RNOTE'] = "\nRequest Detail:\n{}\n".format(pgrqst['note'])
1394
+ elif fcount > 0 and pgrqst['rinfo']:
1395
+ einfo['RNOTE'] = "\nRequest Detail:\n{}\n".format(pgrqst['rinfo'])
1396
+ else:
1397
+ einfo['RNOTE'] = ""
1398
+ if pgrqst['globus_transfer'] == 'Y' and pgrqst['task_id']:
1399
+ einfo['GLOBUS_TASK_URL'] = "https://app.globus.org/activity/" + pgrqst['task_id']
1400
+ for ekey in einfo:
1401
+ if ekey == 'CCD' and not einfo['CCD']:
1402
+ mp = r'Cc:\s*<CCD>\s*'
1403
+ rep = ''
1404
+ else:
1405
+ mp = r'<{}>'.format(ekey)
1406
+ rep = einfo[ekey]
1407
+ if rep is None:
1408
+ self.pglog("{}.{}: None ekey value for reuqest email".format(pgrqst['rindex'], ekey),
1409
+ self.PGOPT['wrnlog']|self.FRCLOG)
1410
+ rep = ''
1411
+ ebuf = re.sub(mp, rep, ebuf)
1412
+ if self.PGLOG['DSCHECK'] and not pgpart:
1413
+ tbl = "dscheck"
1414
+ cnd = "cindex = {}".format(self.PGLOG['DSCHECK']['cindex'])
1415
+ else:
1416
+ tbl = "dsrqst"
1417
+ cnd = "rindex = {}".format(pgrqst['rindex'])
1418
+ if self.send_customized_email(f"{tbl}.{cnd}", ebuf, 0):
1419
+ if errmsg:
1420
+ self.pglog("Error Email sent to {} for {}.{}:\n{}".format(einfo['SENDER'], tbl, cnd, errmsg), self.PGOPT['errlog'])
1421
+ readyfile = None
1422
+ else:
1423
+ self.pglog("{}Email sent to {} for {}.{}\nSubset: {}".format(("Customized " if pgrqst['enotice'] else ""), einfo['RECEIVER'], tbl, cnd, einfo['SUBJECT']),
1424
+ self.PGOPT['wrnlog']|self.FRCLOG)
1425
+ else:
1426
+ if not self.cache_customized_email(tbl, "einfo", cnd, ebuf, 0): return 'E'
1427
+ if errmsg:
1428
+ self.pglog("Error Email {} cached to {}.einfo for {}:\n{}".format(einfo['SENDER'], tbl, cnd, errmsg), self.PGOPT['errlog'])
1429
+ readyfile = None
1430
+ else:
1431
+ self.pglog("{}Email {} cached to {}.einfo for {}\nSubset: {}".format(("Customized " if pgrqst['enotice'] else ""), einfo['RECEIVER'], tbl, cnd, einfo['SUBJECT']),
1432
+ self.PGOPT['wrnlog']|self.FRCLOG)
1433
+ if readyfile:
1434
+ rf = open(readyfile, 'w')
1435
+ rf.write(ebuf)
1436
+ rf.close()
1437
+ self.set_local_mode(readyfile, 1, self.PGLOG['FILEMODE'])
1438
+ return rstat
1439
+
1440
+ # cache partition process error to existing email buffer
1441
+ def cache_partition_email_error(self, ridx, errmsg):
1442
+ pkey = "<PARTERR>"
1443
+ pgrec = self.pgget("dsrqst", 'einfo', "rindex = {}".format(ridx), self.PGOPT['extlog'])
1444
+ if not (pgrec and pgrec['einfo'] and pgrec['einfo'].find(pkey) > -1): return 0
1445
+ errmsg = self.get_error_command(int(time.time()), self.PGOPT['errlog']) + ("{}\n{}".format(errmsg, pkey))
1446
+ pgrec['einfo'] = re.sub(pkey, errmsg, pgrec['einfo'])
1447
+ return self.pgupdt("dsrqst", pgrec, "rindex = {}".format(ridx), self.PGOPT['extlog'])