rda-python-miscs 3.0.2__py3-none-any.whl → 3.0.4__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.
@@ -33,15 +33,21 @@ class GdexCp(PgFile):
33
33
  'fp': None, # from Globus endpoint
34
34
  'tp': None, # to Globus endpoint
35
35
  'f': [], # from file names
36
+ 'i': None, # input file holding a list of from file names, one per line
36
37
  't': None, # to file name
37
38
  'r': 0, # 1 if recursive all
38
39
  'R': 0, # > 0 to set recursive limit
40
+ 'o': 0, # 1 to force a downloaded file owned by COMMONUSER; needs -fp
41
+ 'O': 0, # 1 to override an existing target file of the same size
42
+ 'm': 1, # number of multiple processes to copy files in parallel
43
+ 'd': 0, # 1 to add a dscheck record for delayed PBS batch process
39
44
  'F': 0o664, # to file mode, default to 664
40
45
  'D': 0o775, # to directory mode, default to 775
41
46
  }
42
47
  self.CINFO = {
43
48
  'tcnt': 0,
44
49
  'htcnt': 0,
50
+ 'pcnt': 0, # count of dispatched child processes for option -m
45
51
  'cpflag': 0, # 1 file only, 2 directory only, 3 both
46
52
  'cpstr': ['', 'Files', 'Directories', 'Files/Directories'],
47
53
  'fpath': None,
@@ -75,8 +81,8 @@ class GdexCp(PgFile):
75
81
  if ms:
76
82
  option = ms.group(1)
77
83
  if option not in self.RDACP: self.pglog(arg + ": Unknown Option", self.LGEREX)
78
- if option == 'r':
79
- self.RDACP['r'] = 1
84
+ if option in ('r', 'o', 'd', 'O'):
85
+ self.RDACP[option] = 1
80
86
  option = None
81
87
  continue
82
88
  if not option: self.pglog(arg + ": Value provided without option", self.LGEREX)
@@ -84,7 +90,7 @@ class GdexCp(PgFile):
84
90
  self.RDACP['f'].append(arg)
85
91
  defopt = None
86
92
  else:
87
- if option == 'R':
93
+ if option in ('R', 'm'):
88
94
  self.RDACP[option] = int(arg)
89
95
  elif option in 'FD':
90
96
  self.RDACP[option] = self.base2int(arg, 8)
@@ -95,8 +101,29 @@ class GdexCp(PgFile):
95
101
  elif option == 'fh':
96
102
  self.CINFO['fhost'] = arg + '-'
97
103
  option = defopt
104
+ if self.RDACP['i']: self.add_input_files(self.RDACP['i'])
98
105
  if dohelp or not self.RDACP['f']: self.show_usage("gdexcp")
99
-
106
+
107
+ # read source paths from an input file and append them to the -f list
108
+ def add_input_files(self, infile):
109
+ """Append source paths read from an input file to the -f source list.
110
+
111
+ Each non-empty line in the input file is treated as one source path;
112
+ leading/trailing whitespace is stripped and lines starting with '#' are
113
+ ignored as comments.
114
+
115
+ Args:
116
+ infile (str): Path to the input file holding one source path per line.
117
+ """
118
+ finfo = self.check_local_file(infile, 0, self.LGWNEX)
119
+ if not finfo: self.pglog("{}: Input file of -i not found".format(infile), self.LGEREX)
120
+ fd = open(infile, 'r')
121
+ for line in fd:
122
+ line = line.strip()
123
+ if not line or line[0] == '#': continue
124
+ self.RDACP['f'].append(line)
125
+ fd.close()
126
+
100
127
  # function to start actions
101
128
  def start_actions(self):
102
129
  """Validate copy targets, configure host/bucket/endpoint context, and dispatch copies.
@@ -132,7 +159,23 @@ class GdexCp(PgFile):
132
159
  self.PGLOG['BACKUPEP'] = self.RDACP['fp']
133
160
  elif self.RDACP['tp']:
134
161
  self.PGLOG['BACKUPEP'] = self.RDACP['tp']
162
+ if self.RDACP['o']:
163
+ if not self.RDACP['fp']:
164
+ self.pglog("-o: works only when source Globus endpoint -fp is provided", self.LGEREX)
165
+ if self.RDACP['th'] or self.RDACP['tp'] or self.RDACP['tb']:
166
+ self.pglog("-o: works only for downloading to local files (no -th/-tp/-tb)", self.LGEREX)
167
+ if self.RDACP['m'] < 1: self.RDACP['m'] = 1
168
+ if self.RDACP['m'] > 16:
169
+ self.pglog("-m {}: process count too large, capped at 16".format(self.RDACP['m']), self.LOGWRN)
170
+ self.RDACP['m'] = 16
171
+ if self.RDACP['d']:
172
+ self.add_delayed_check()
173
+ self.cmdlog()
174
+ return
175
+ if self.RDACP['m'] > 1:
176
+ self.start_none_daemon('gdexcp', '', self.PGLOG['CURUID'], self.RDACP['m'], 120)
135
177
  self.copy_top_list(self.RDACP['f'])
178
+ if self.RDACP['m'] > 1: self.check_child(None, 0, self.LOGWRN, 1)
136
179
  hinfo = ''
137
180
  if self.RDACP['fh']: hinfo += " From " + self.RDACP['fh']
138
181
  if self.RDACP['th']: hinfo += " To " + self.RDACP['th']
@@ -175,7 +218,7 @@ class GdexCp(PgFile):
175
218
  if not re.match(r'^/', file): file = self.join_paths(self.CINFO['curdir'], file)
176
219
  self.CINFO['fpath'] = (file if dosub else op.dirname(file)) + "/"
177
220
  if info['isfile']:
178
- self.CINFO['tcnt'] += self.copy_file(file, info['isfile'])
221
+ self.CINFO['tcnt'] += self.dispatch_copy(file, info)
179
222
  elif dosub or self.RDACP['R']:
180
223
  flist = self.gdex_glob(file, self.RDACP['fh'], 0, self.LGWNEX)
181
224
  if flist: self.copy_list(flist, 1, file)
@@ -197,7 +240,7 @@ class GdexCp(PgFile):
197
240
  fcnt = 0
198
241
  for file in tlist:
199
242
  if tlist[file]['isfile']:
200
- fcnt += self.copy_file(file, tlist[file]['isfile'])
243
+ fcnt += self.dispatch_copy(file, tlist[file])
201
244
  self.CINFO['cpflag'] |= (1 if tlist[file]['isfile'] else 2)
202
245
  elif level < self.RDACP['R']:
203
246
  flist = self.gdex_glob(file, self.RDACP['fh'], 0, self.LGWNEX)
@@ -206,20 +249,50 @@ class GdexCp(PgFile):
206
249
  self.pglog("{}{}: {} {} copied from directory".format(self.CINFO['fhost'], cdir, fcnt, self.CINFO['cpstr'][self.CINFO['cpflag']]), self.LOGWRN)
207
250
  self.CINFO['tcnt'] += fcnt
208
251
 
252
+ # copy one file, forking a child process when running with option -m
253
+ def dispatch_copy(self, fromfile, finfo):
254
+ """Copy one file, dispatching the copy to a child process when -m > 1.
255
+
256
+ With a single process (-m 1) the file is copied in line. With multiple
257
+ processes the copy is forked to a child via the PgSIG process pool (up to
258
+ RDACP['m'] children run concurrently); the parent records one dispatched
259
+ file and continues traversing, while each child performs the copy, logs
260
+ its own result, and exits.
261
+
262
+ Args:
263
+ fromfile (str): Absolute source file path.
264
+ finfo (dict): Source file-info dict (with 'isfile' and 'data_size').
265
+
266
+ Returns:
267
+ int: 1 if the file was copied (single process) or dispatched to a
268
+ child process (-m > 1), 0 otherwise.
269
+ """
270
+ if self.RDACP['m'] < 2: return self.copy_file(fromfile, finfo)
271
+ stat = self.start_child("gdexcp_{}".format(self.CINFO['pcnt']), self.LOGWRN, 1)
272
+ if stat <= 0: self.pglog("{}: cannot start child process to copy".format(fromfile), self.LGEREX)
273
+ if self.PGSIG['PPID'] > 1: # in child process
274
+ self.copy_file(fromfile, finfo)
275
+ sys.exit(0)
276
+ self.CINFO['pcnt'] += 1 # in parent process; child already dropped its DB link
277
+ return 1
278
+
209
279
  # copy one file
210
- def copy_file(self, fromfile, isfile):
280
+ def copy_file(self, fromfile, finfo):
211
281
  """Resolve the destination path for one source file and perform the copy.
212
282
 
213
283
  When a target directory is set (tpath), strips the source base path prefix
214
284
  and joins the remainder to tpath. Otherwise copies directly to the -t value.
285
+ Skips the copy when the target already exists with the same size as the
286
+ source, unless -O is given to override an existing same-size target.
215
287
 
216
288
  Args:
217
289
  fromfile (str): Absolute source file path.
218
- isfile (int): Non-zero when the source is a regular file (vs. a symlink type).
290
+ finfo (dict): Source file-info dict (with 'isfile' and 'data_size').
219
291
 
220
292
  Returns:
221
293
  int: 1 if the file was copied successfully, 0 otherwise.
222
294
  """
295
+ isfile = finfo['isfile']
223
296
  if self.CINFO['tpath']:
224
297
  fname = re.sub(r'^{}'.format(self.CINFO['fpath']), '', fromfile)
225
298
  if isfile:
@@ -228,7 +301,77 @@ class GdexCp(PgFile):
228
301
  tofile = self.CINFO['tpath'] + '/'
229
302
  else:
230
303
  tofile = self.RDACP['t']
231
- return (1 if self.copy_gdex_file(tofile, fromfile, self.RDACP['th'], self.RDACP['fh'], self.LGWNEX) else 0)
304
+ if isfile and not self.RDACP['O']:
305
+ tinfo = self.check_gdex_file(tofile, self.RDACP['th'], 0, self.LGWNEX)
306
+ if tinfo and tinfo['data_size'] == finfo['data_size']:
307
+ self.pglog("{}{}: Target exists with same size, skip copying".format(self.CINFO['thost'], tofile), self.LOGWRN)
308
+ return 0
309
+ if self.RDACP['o']: return self.force_owner_copy(tofile, fromfile)
310
+ logact = self.LGWNEX | (self.OVRIDE if self.RDACP['O'] else 0)
311
+ return (1 if self.copy_gdex_file(tofile, fromfile, self.RDACP['th'], self.RDACP['fh'], logact) else 0)
312
+
313
+ # copy one file from a Globus endpoint and force COMMONUSER ownership
314
+ def force_owner_copy(self, tofile, fromfile):
315
+ """Download a Globus file via a tmp file so the final copy is owned by COMMONUSER.
316
+
317
+ A Globus endpoint dumps the local file owned by the endpoint's mapped user
318
+ rather than COMMONUSER ('gdexdata'). This downloads to a tmp file under
319
+ PGLOG['TMPPATH'], makes it group readable/writable as its owner via the
320
+ pgstart_<user> setuid wrapper, then copies it locally so the final file is
321
+ owned by COMMONUSER, and removes the tmp file.
322
+
323
+ Args:
324
+ tofile (str): Final local destination path.
325
+ fromfile (str): Source file path on the Globus endpoint.
326
+
327
+ Returns:
328
+ int: 1 if the file was copied successfully, 0 otherwise.
329
+ """
330
+ tmpfile = self.join_paths(self.PGLOG['TMPPATH'], "{}.{}".format(op.basename(fromfile), os.getpid()))
331
+ if not self.copy_gdex_file(tmpfile, fromfile, self.RDACP['th'], self.RDACP['fh'], self.LGWNEX): return 0
332
+ finfo = self.check_local_file(tmpfile, 2, self.LGWNEX)
333
+ owner = finfo['logname'] if finfo else None
334
+ if owner and owner != self.PGLOG['COMMONUSER']:
335
+ self.pgsystem(self.get_local_command("chmod g+rw " + tmpfile, owner), self.LGWNEX)
336
+ ret = self.copy_gdex_file(tofile, tmpfile, self.RDACP['th'], None, self.LGWNEX)
337
+ self.delete_local_file(tmpfile, self.LGWNEX)
338
+ return (1 if ret else 0)
339
+
340
+ # add a dscheck record so this gdexcp command runs later as a PBS batch job
341
+ def add_delayed_check(self):
342
+ """Queue this gdexcp invocation as a delayed PBS batch job via a dscheck record.
343
+
344
+ Records the current command (with the -d flag stripped so the batch run
345
+ performs the actual copy) into the RDADB dscheck table for the dscheck
346
+ daemon to later submit to PBS through bashqsub/tcshqsub. The qsub resource
347
+ option always sets a 24 hour walltime; when -m > 1 it also reserves a single
348
+ node with (number of processes) cpus and 1gb of memory per cpu.
349
+ """
350
+ argv = [arg for arg in sys.argv[1:] if arg != '-d']
351
+ argstr = self.argv_to_string(argv, 1)
352
+ argextra = None
353
+ if len(argstr) > 100:
354
+ argextra = argstr[100:]
355
+ argstr = argstr[0:100]
356
+ record = {
357
+ 'command': 'gdexcp',
358
+ 'argv': argstr,
359
+ 'specialist': self.PGLOG['CURUID'],
360
+ 'workdir': self.CINFO['curdir'],
361
+ 'oindex': 0,
362
+ 'otype': '',
363
+ 'action': None,
364
+ 'dsid': None,
365
+ 'mcount': 1,
366
+ }
367
+ (record['date'], record['time']) = self.get_date_time()
368
+ if argextra: record['argextra'] = argextra
369
+ qoptions = "-l walltime=24:00:00"
370
+ if self.RDACP['m'] > 1:
371
+ qoptions += ",select=1:ncpus={0}:mem={0}gb".format(self.RDACP['m'])
372
+ record['qoptions'] = qoptions
373
+ cidx = self.pgadd("dscheck", record, self.LGWNEX|self.AUTOID)
374
+ self.pglog("Chk{}: gdexcp {} added for delayed batch process".format(cidx, argstr), self.LOGWRN)
232
375
 
233
376
  # main function to execute this script
234
377
  def main():
@@ -1,77 +1,145 @@
1
1
 
2
- Copy files and directories to a target location. Source and target may each
3
- reside on the local host, a remote host, an Object Store bucket, or a Globus
2
+ Name: gdexcp - copy files and directories as user 'gdexdata'
3
+
4
+ Copy files and directories to a target location. The source and the target may
5
+ each reside on the local host, a remote host, an Object Store bucket, or a Globus
4
6
  endpoint. Target files are owned by 'gdexdata' and created with configurable
5
7
  permission modes.
6
8
 
7
9
  Usage: gdexcp [-f] FromDirectories/Files [-t ToDirectory/FileName] \
10
+ [-i InputFile] [-m ProcessCount] [-d] [-r] [-R RecursiveLevel] \
8
11
  [-fh FromHostName] [-th ToHostName] \
9
12
  [-fb FromBucket] [-tb ToBucket] \
10
13
  [-fp FromGlobusEndpoint] [-tp ToGlobusEndpoint] \
11
- [-F FileMode] [-D DirectoryMode] [-r] [-R RecursiveLevel] [-h]
14
+ [-F FileMode] [-D DirectoryMode] [-o] [-O] [-h]
15
+
16
+ gdexcp can be run from any directory. Usage is displayed when no source paths
17
+ are given.
18
+
19
+
20
+ SOURCE AND TARGET
21
+
22
+ -f FromDirectories/Files
23
+ Source directories and/or files to copy. This is the default option,
24
+ so paths may be given without the -f flag. Shell wildcards are
25
+ supported; use './' or '*' to copy everything in the current directory.
26
+ A trailing '/' on a source directory path copies the contents of that
27
+ directory (as a file list) rather than the directory entry itself.
28
+ Source paths must be readable by user 'gdexdata'; gdexcp attempts to
29
+ fix the mode when they are not.
30
+
31
+ -i InputFile
32
+ A file holding a list of source paths to copy, one path per line.
33
+ Blank lines and lines starting with '#' are ignored. The paths read
34
+ are appended to the -f source list.
35
+
36
+ -t ToDirectory/FileName
37
+ Target directory or file name. Defaults to '.' (the current
38
+ directory). A trailing '/' forces the target to be treated as a
39
+ directory. Multiple source files cannot be copied to a single target
40
+ file name.
41
+
42
+
43
+ SOURCE/TARGET LOCATIONS (default to the local host)
44
+
45
+ -fh FromHostName host name where the source files reside.
46
+ -th ToHostName host name where the target files are written.
47
+ -fb FromBucket Object Store bucket name for the source files.
48
+ -tb ToBucket Object Store bucket name for the target files.
49
+ -fp FromGlobusEndpoint Globus endpoint for the source files.
50
+ -tp ToGlobusEndpoint Globus endpoint for the target files.
51
+
52
+
53
+ COPY BEHAVIOR
54
+
55
+ -r Copy directories and files recursively, with no depth limit.
56
+
57
+ -R RecursiveLevel
58
+ Copy recursively up to the given depth. -R 1 copies only the
59
+ immediate contents of each source directory.
60
+
61
+ -m ProcessCount
62
+ Number of processes used to copy files in parallel. Defaults to 1.
63
+ When greater than 1, the source files are distributed across that many
64
+ concurrent child processes. Capped at 16; a larger value is reduced to
65
+ 16 with a warning.
66
+
67
+ -d Add a dscheck record so this gdexcp command runs later as a delayed PBS
68
+ batch job (submitted by the dscheck daemon via bashqsub / tcshqsub).
69
+ The qsub resource always sets a 24 hour walltime; when -m is greater
70
+ than 1 it also reserves a single node with (ProcessCount) cpus and 1gb
71
+ of memory per cpu.
72
+
73
+ -o Force a downloaded file to be owned by 'gdexdata'. A Globus endpoint
74
+ writes the local file owned by the endpoint's mapped user; with -o the
75
+ file is downloaded to a tmp file, then copied locally so the final file
76
+ is owned by 'gdexdata'. Only valid together with -fp and for
77
+ downloading to local files (not with -th/-tp/-tb).
78
+
79
+ -O Override an existing target. By default a source file is skipped when
80
+ the target already exists with the same size; give -O to copy it anyway
81
+ and overwrite the existing target.
82
+
83
+
84
+ TARGET PERMISSIONS (octal notation)
12
85
 
13
- - Option -f, source directories and/or files to copy. This is the
14
- default option, so paths may be given without the -f flag.
15
- Shell wildcards are supported. Use './' or '*' to copy everything
16
- in the current directory. Source paths must be readable by user
17
- 'gdexdata'; gdexcp will attempt to fix the mode if they are not;
86
+ -F FileMode permission mode for target files. Defaults to 664.
87
+ -D DirectoryMode permission mode for target directories. Defaults to 775.
18
88
 
19
- - Option -t, target directory or file name. Defaults to '.' (current
20
- directory). Multiple source files cannot be copied to a single
21
- target file name. A trailing '/' on the target path treats it as
22
- a directory;
23
89
 
24
- - Option -fh, host name where the source files reside.
25
- Defaults to the local host;
90
+ MISCELLANEOUS
26
91
 
27
- - Option -th, host name where the target files should be written.
28
- Defaults to the local host;
92
+ -h Display this help document.
29
93
 
30
- - Option -fb, Object Store bucket name for the source files;
31
94
 
32
- - Option -tb, Object Store bucket name for the target files;
95
+ NOTES
33
96
 
34
- - Option -fp, Globus endpoint for the source files;
97
+ - A trailing '/' on a source directory path copies the contents of that
98
+ directory rather than the directory entry itself.
99
+ - By default an unchanged target (same size) is skipped; use -O to overwrite.
100
+ - If a Globus endpoint (-fp/-tp) is locally accessible, a direct local copy
101
+ (omitting -fp/-tp and giving the local path) is faster, avoiding the Globus
102
+ transfer overhead.
103
+ - A delayed batch job (-d) is given a 24 hour walltime. Do not submit a single
104
+ batch job to copy too many files at once; if the copy cannot finish within 24
105
+ hours the job is killed. Split a very large copy into multiple -d jobs (and/or
106
+ raise -m) so each one completes well within the walltime.
35
107
 
36
- - Option -tp, Globus endpoint for the target files;
37
108
 
38
- - Option -F, permission mode for target files in octal notation.
39
- Defaults to 664;
109
+ EXAMPLES
40
110
 
41
- - Option -D, permission mode for target directories in octal notation.
42
- Defaults to 775;
111
+ 1. Copy all files and subdirectories under the current directory to a remote
112
+ host:
43
113
 
44
- - Option -r, copy directories and files recursively (no depth limit);
114
+ gdexcp -r -f * -t /PathTo/d277006/ -th castle
45
115
 
46
- - Option -R RecursiveLevel, copy recursively up to the specified depth.
47
- -R 1 copies only the immediate contents of each source directory;
116
+ 2. Copy the contents of a local directory to a remote location (trailing '/'
117
+ on the source omits the directory entry itself):
48
118
 
49
- - Option -h, display this help document;
119
+ gdexcp -r -f /PathTo/DirectoryName/ -t /PathTo/d277006/ -th castle
50
120
 
51
- This utility can be run from any directory. Usage is displayed if no source
52
- files are provided. A trailing '/' on a source directory path copies the
53
- contents of that directory rather than the directory entry itself.
121
+ Without the trailing '/', DirectoryName itself is also copied:
54
122
 
55
- Examples:
123
+ gdexcp -r -f /PathTo/DirectoryName -t /PathTo/d277006/ -th castle
56
124
 
57
- 1. Copy all files and subdirectories under the current directory to a
58
- remote host:
125
+ 3. Copy a single file to an Object Store bucket:
59
126
 
60
- gdexcp -r -f * -t /PathTo/d277006/ -th castle
127
+ gdexcp -f /PathTo/myfile.nc -tb my-bucket -t myfile.nc
61
128
 
62
- 2. Copy the contents of a specific local directory to a remote location
63
- (trailing '/' on source omits the directory entry itself):
129
+ 4. Copy files from a remote host to the local current directory:
64
130
 
65
- gdexcp -r -f /PathTo/DirectoryName/ -t /PathTo/d277006/ -th castle
131
+ gdexcp -fh castle -f /PathTo/d277006/myfile.nc
66
132
 
67
- Without the trailing '/', DirectoryName itself is also copied:
133
+ 5. Download a file from a source Globus endpoint and force the local file to
134
+ be owned by 'gdexdata' (-o requires -fp):
68
135
 
69
- gdexcp -r -f /PathTo/DirectoryName -t /PathTo/d277006/ -th castle
136
+ gdexcp -fp gdex-quasar -f /d277006/myfile.nc -t /PathTo/myfile.nc -o
70
137
 
71
- 3. Copy a single file to an Object Store bucket:
138
+ 6. Copy the source paths listed in an input file using 4 parallel processes:
72
139
 
73
- gdexcp -f /PathTo/myfile.nc -tb my-bucket -t myfile.nc
140
+ gdexcp -i filelist.txt -t /PathTo/d277006/ -m 4
74
141
 
75
- 4. Copy files from a remote host to the local current directory:
142
+ 7. Queue a delayed PBS batch job to copy a directory in parallel; the dscheck
143
+ daemon submits it later, reserving one node with 4 cpus and 4gb of memory:
76
144
 
77
- gdexcp -fh castle -f /PathTo/d277006/myfile.nc
145
+ gdexcp -r -f /PathTo/DirectoryName/ -t /PathTo/d277006/ -m 4 -d
@@ -140,10 +140,10 @@ class PgRST(PgFile, PgUtil):
140
140
  """
141
141
  self.OPTS = opts
142
142
  self.ALIAS = alias
143
+ self.DOCS['DOCNAM'] = docname
143
144
 
144
145
  self.parse_docs(docname)
145
146
  if not self.sections: self.pglog(docname + ": empty document", self.LGWNEX)
146
- self.DOCS['DOCNAM'] = docname
147
147
  if docname in self.LINKS: self.LINKS.remove(docname)
148
148
  self.DOCS['DOCLNK'] = r"({})".format('|'.join(self.LINKS))
149
149
  self.DOCS['DOCTIT'] = docname.upper()
@@ -152,6 +152,7 @@ class PgRST(PgFile, PgUtil):
152
152
  self.write_index(self.sections[0])
153
153
  for section in self.sections:
154
154
  self.write_section(section)
155
+ self.write_appendix()
155
156
 
156
157
  #
157
158
  # parse the original document and return a array of sections,
@@ -172,7 +173,9 @@ class PgRST(PgFile, PgUtil):
172
173
  with open(docfile, 'r') as fh:
173
174
  line = fh.readline()
174
175
  while line:
175
- if re.match(r'\s*#', line):
176
+ # Skip full-line authoring comments, but keep '#!' shebang lines so
177
+ # shell scripts shown in example content blocks stay intact.
178
+ if re.match(r'\s*#(?!!)', line):
176
179
  line = fh.readline()
177
180
  continue # skip comment lines
178
181
  ms = re.match(r'^(.*\S)\s+#', line)
@@ -191,13 +194,21 @@ class PgRST(PgFile, PgUtil):
191
194
  option = self.record_option(section, option, example, ms.group(1), ms.group(2))
192
195
  example = None
193
196
  elif option:
194
- ms = re.match(r'^ For( | another )example, (.*)$', line)
195
- if ms: # found example
196
- example = self.record_example(option, example, ms.group(2))
197
- elif example:
198
- example['desc'] += line + "\n"
197
+ if option.get('inexm'):
198
+ # inside an 'Examples:' block: collect raw lines verbatim
199
+ # (split into individual examples later in split_examples)
200
+ option['exmraw'] += line + "\n"
199
201
  else:
200
- option['desc'] += line + "\n"
202
+ ms = re.match(r'^ (?:For(?: | another )example,|Example[ ]*[,\-])\s*(.*)$', line)
203
+ if ms: # found a single labeled example
204
+ example = self.record_example(option, example, ms.group(1))
205
+ elif re.match(r'^ Examples:\s*$', line):
206
+ option['inexm'] = True # start of a multi-example block
207
+ option['exmraw'] = ""
208
+ elif example:
209
+ example['desc'] += line + "\n"
210
+ else:
211
+ option['desc'] += line + "\n"
201
212
  else:
202
213
  section['desc'] += line + "\n"
203
214
 
@@ -261,11 +272,62 @@ class PgRST(PgFile, PgUtil):
261
272
  """
262
273
  if option:
263
274
  if example: self.record_example(option, example)
275
+ self.split_examples(option)
264
276
  self.options[option['opt']] = option # record option globally
265
277
  section['opts'].append(option['opt']) # record option short name in section
266
278
 
267
279
  if nopt: return self.init_option(section['secid'], nopt, ndesc)
268
280
 
281
+ def split_examples(self, option):
282
+ """Split an option's pending ``Examples:`` block into individual examples.
283
+
284
+ The raw text collected after an ``Examples:`` header is segmented into
285
+ blank-line-separated blocks. A block whose last line ends with ``:`` and
286
+ whose first line is descriptive prose (not a command, option flag, or a
287
+ ``<<Content ...>>`` header) starts a new example; following blocks
288
+ (command synopsis, script content, trailing notes) belong to that
289
+ example until the next title block.
290
+
291
+ Args:
292
+ option (dict): The option whose ``exmraw`` buffer is parsed; the
293
+ buffer is removed and one example is recorded per
294
+ title block found.
295
+ """
296
+ buf = option.pop('exmraw', None)
297
+ option.pop('inexm', None)
298
+ if not buf: return
299
+
300
+ blocks = []
301
+ cur = []
302
+ for ln in buf.split('\n'):
303
+ if ln.strip() == '':
304
+ if cur: blocks.append(cur); cur = []
305
+ else:
306
+ cur.append(ln)
307
+ if cur: blocks.append(cur)
308
+
309
+ def is_title(block):
310
+ if not block[-1].rstrip().endswith(':'): return False
311
+ first = block[0].strip()
312
+ if first.startswith('<<'): return False
313
+ if re.match(r'[-*(]', first): return False
314
+ if re.match(r'{}\b'.format(self.DOCS['DOCNAM']), first): return False
315
+ return True
316
+
317
+ exmtext = None
318
+ for block in blocks:
319
+ btext = '\n'.join(block)
320
+ if is_title(block):
321
+ if exmtext is not None:
322
+ self.record_example(option, self.init_example(option['opt'], exmtext))
323
+ exmtext = btext + "\n"
324
+ elif exmtext is not None:
325
+ exmtext += "\n" + btext + "\n"
326
+ else:
327
+ option['desc'] += btext + "\n" # stray text before the first example
328
+ if exmtext is not None:
329
+ self.record_example(option, self.init_example(option['opt'], exmtext))
330
+
269
331
  def record_example(self, option, example, ndesc=None):
270
332
  """Append the completed *example* to ``self.examples`` and optionally start a new one.
271
333
 
@@ -478,18 +540,21 @@ class PgRST(PgFile, PgUtil):
478
540
  """Build and return the RST table-of-contents string of a given section.
479
541
 
480
542
  Produces a nested bullet list of section links (indented by section
481
- level) followed by a flat Appendix A list of all example links.
543
+ level). For the index (``csection is None``) the example appendix is a
544
+ standalone page (``appendixA``) added to the toctree; for a section the
545
+ examples in its subtree are listed inline as a local Appendix A.
482
546
 
483
547
  Returns:
484
548
  str: RST-formatted TOC content ready for ``__TOC__`` substitution.
485
549
  """
486
-
550
+
487
551
  content = ""
488
552
  clevel = csection['level'] if csection else 0
489
553
  csecid = csection['secid'] if csection else ""
490
554
  depth = self.TLEVEL - clevel
491
555
  level = clevel+1
492
556
  preid = csecid+'.'
557
+ is_index = csection is None
493
558
 
494
559
  # nested bullet list for all sections
495
560
  for section in self.sections:
@@ -497,9 +562,14 @@ class PgRST(PgFile, PgUtil):
497
562
  if csecid and not secid.startswith(preid): continue
498
563
  if section['level'] == level: content += " section{}\n".format(secid)
499
564
 
565
+ # The full list of examples lives on its own appendix page in the index.
566
+ if is_index and self.examples: content += " appendixA\n"
567
+
500
568
  if not content: return ""
501
569
 
502
570
  content = f".. toctree::\n :maxdepth: {depth}\n :caption: Table of Contents\n\n{content}\n"
571
+ if is_index: return content
572
+
503
573
  # appendix A: list of examples for the parent section and its subsections
504
574
  appendix = ""
505
575
  idx = 1 # used as example index
@@ -507,7 +577,7 @@ class PgRST(PgFile, PgUtil):
507
577
  opt = exm['opt']
508
578
  option = self.options[opt]
509
579
  secid = option['secid']
510
- if not csecid or secid == csecid or secid.startswith(preid):
580
+ if secid == csecid or secid.startswith(preid):
511
581
  appendix += "- :ref:`A.{}. {} Option -{} (-{}) <{}_e{}>`\n".format(
512
582
  idx, option['type'], opt, option['name'], secid, idx)
513
583
  idx += 1
@@ -516,6 +586,28 @@ class PgRST(PgFile, PgUtil):
516
586
 
517
587
  return content
518
588
 
589
+ #
590
+ # write the appendix page listing all examples in the document
591
+ #
592
+ def write_appendix(self):
593
+ """Write ``appendixA.rst`` listing every example with a link.
594
+
595
+ Each entry links to the example anchor on its section page. Does nothing
596
+ when the document has no examples.
597
+ """
598
+ if not self.examples: return
599
+ content = ""
600
+ idx = 1
601
+ for exm in self.examples:
602
+ option = self.options[exm['opt']]
603
+ secid = option['secid']
604
+ title = exm['title'].strip().rstrip(':')
605
+ content += "- :ref:`A.{}. {} Option -{} (-{}): {} <{}_e{}>`\n".format(
606
+ idx, option['type'], exm['opt'], option['name'], title, secid, idx)
607
+ idx += 1
608
+
609
+ self.template_to_rst("appendix", {'CONTENT': content}, "A")
610
+
519
611
  #
520
612
  # create a section rst content
521
613
  #
@@ -672,7 +764,8 @@ class PgRST(PgFile, PgUtil):
672
764
 
673
765
  for optary in opts:
674
766
  opt = self.get_short_option(optary[1])
675
-
767
+ if opt is None: continue # not an option of this document; leave as-is
768
+
676
769
  pre = optary[0]
677
770
  after = optary[2]
678
771
  secid = self.options[opt]['secid']
@@ -826,7 +919,7 @@ class PgRST(PgFile, PgUtil):
826
919
  line0 = lines[0]
827
920
  normal = 1
828
921
  if dtype == 2:
829
- ms = re.match(r'^<<(Content .*)>>$', line0)
922
+ ms = re.match(r'^\s*<<(Content .*)>>$', line0)
830
923
  if ms: # input files for examples
831
924
  content += ms.group(1) + ":\n\n.. code-block:: none\n\n"
832
925
  normal = 0
@@ -928,13 +1021,23 @@ class PgRST(PgFile, PgUtil):
928
1021
  rows.append(tuple(prev_vals))
929
1022
  content = self.build_rst_list_table(rows)
930
1023
  else:
931
- # multi-column table split on 2+ spaces
932
- rows = []
933
- for i in range(cnt):
934
- line = lines[i].strip()
935
- vals = re.split(r'\s{2,}', self.replace_option_link(line, secid, 1))
936
- rows.append(vals)
937
- content = self.build_rst_simple_table(rows) + "\n"
1024
+ raw = [lines[i].strip() for i in range(cnt)]
1025
+ cmdpat = r'(?:[*\d][\d* ]*\s+)?{}(\s|$)'.format(re.escape(self.DOCS['DOCNAM']))
1026
+ if raw and all(re.match(cmdpat, r) for r in raw):
1027
+ # Command line(s) following a label (e.g. a Quick Start entry):
1028
+ # render as a literal block instead of a (degenerate) table.
1029
+ content = ".. code-block:: none\n\n"
1030
+ for r in raw:
1031
+ content += " " + r + "\n"
1032
+ content += "\n"
1033
+ else:
1034
+ # multi-column table split on 2+ spaces
1035
+ rows = []
1036
+ for i in range(cnt):
1037
+ line = lines[i].strip()
1038
+ vals = re.split(r'\s{2,}', self.replace_option_link(line, secid, 1))
1039
+ rows.append(vals)
1040
+ content = self.build_rst_simple_table(rows) + "\n"
938
1041
 
939
1042
  return content
940
1043
 
@@ -1046,10 +1149,10 @@ class PgRST(PgFile, PgUtil):
1046
1149
  p (str): Option name to look up (short, long, or alias).
1047
1150
 
1048
1151
  Returns:
1049
- str: Canonical two-letter option short name.
1050
-
1051
- Raises:
1052
- PgLOG error (LGWNEX) if *p* cannot be resolved.
1152
+ str | None: Canonical two-letter option short name, or ``None`` when
1153
+ *p* does not name an option of this document (e.g. an option of a
1154
+ different program referenced in prose). Callers skip such tokens
1155
+ so unrelated option-like text is left untouched.
1053
1156
  """
1054
1157
  plen = len(p)
1055
1158
  if plen == 2 and p in self.options: return p
@@ -1061,7 +1164,7 @@ class PgRST(PgFile, PgUtil):
1061
1164
  for alias in self.ALIAS[opt]:
1062
1165
  if re.match(r'^{}$'.format(alias), p, re.I): return opt
1063
1166
 
1064
- self.pglog("{} - unknown option for {}".format(p, self.DOCS['DOCNAM']), self.LGWNEX)
1167
+ return None
1065
1168
 
1066
1169
  #
1067
1170
  # replace with rst link for a given section title
@@ -0,0 +1,19 @@
1
+ ################################################################################
2
+ #
3
+ # Title : appendix_rst.temp
4
+ # Author : Zaihua Ji, zji@ucar.edu
5
+ # Date : 03/17/2026
6
+ # Purpose : template file for help document appendixA.rst (reStructuredText)
7
+ # Github : https://github.com/NCAR/rda-python-mics.git
8
+ #
9
+ ################################################################################
10
+
11
+ .. _appendixA:
12
+
13
+ ============================
14
+ Appendix A: List of Examples
15
+ ============================
16
+
17
+ __CONTENT__
18
+
19
+ | :ref:`Back to Table of Contents <index>`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rda_python_miscs
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: RDA Python package to hold RDA miscellaneous utility programs
5
5
  Author-email: Zaihua Ji <zji@ucar.edu>
6
6
  Project-URL: Homepage, https://github.com/NCAR/rda-python-miscs
@@ -3,8 +3,8 @@ rda_python_miscs/bash_qsub.py,sha256=NsYg1A7zeRy3mR_rAaJEQoWSDaWFxE7cpHNc4mA_JwA
3
3
  rda_python_miscs/bashqsub.py,sha256=SadPwz1ZXHx2zGNYb2VityItQjMEYj-Favq3xDrPBK0,10127
4
4
  rda_python_miscs/bashqsub.usg,sha256=QDaFr1FiQ-5FC9UzCRQ0geYjsPXBnVMhe3lM8qH1xss,2176
5
5
  rda_python_miscs/gdex_ls.py,sha256=fg9jfYajOT8ps6eEhFUUqmQ791BdGwBkrT_pyjmGGvQ,8860
6
- rda_python_miscs/gdexcp.py,sha256=b8ZJspBKDP_u8nH15eWkwI0CSABLSX0MzPw2CVyFJYY,10933
7
- rda_python_miscs/gdexcp.usg,sha256=O-qXc6vMvojEmBWQIqns1pntI2AqxPB4d7LJw4AqV6M,3197
6
+ rda_python_miscs/gdexcp.py,sha256=jCkDzNrZhi7vhZqeSXHR7d1XIKtsionSJE5ntWoZE0g,18110
7
+ rda_python_miscs/gdexcp.usg,sha256=6r8XVi9inPuWuFth6CAzcd3EpXX4Y_ZrT6YubmQ82kM,6172
8
8
  rda_python_miscs/gdexkill.py,sha256=sWa5MCBXvDpN-5iqVBLNrfO8og1vZMZhu7HlCR9c7bI,11607
9
9
  rda_python_miscs/gdexkill.usg,sha256=ptx_1GuFNI9ISi4mjMVTc4aDj9J4JpMG0gvzKbhDJqA,2459
10
10
  rda_python_miscs/gdexls.py,sha256=Z-iB3nC0zUm2fOUIkhpZd_N82j-5WPRbjDcmGKTRGhY,14396
@@ -21,7 +21,7 @@ rda_python_miscs/gdexsub.usg,sha256=SChXnDzo6larOdpR9eZ2CMu8ExJYQUdfFHUzxcGoqJ0,
21
21
  rda_python_miscs/gdexzip.py,sha256=wzMVTqeahXAUHsKAtqYG6wrM4J2EahnncrpliCqFyNQ,3252
22
22
  rda_python_miscs/gdexzip.usg,sha256=cG1Uwa8WZ3KQNSaSkr29edUEaaLb_4cHTU3GRoZSQ84,1918
23
23
  rda_python_miscs/pg_docs.py,sha256=_AoqrWroUu7FQMVWaTSTZBQMwi1BH1-RjKb1nDHgxOI,25369
24
- rda_python_miscs/pg_rst.py,sha256=XldjKneKuciYvZxrHC3h3_OHwyhh51LKYOuKr0s_2Gg,48480
24
+ rda_python_miscs/pg_rst.py,sha256=Wd4qfeetQYvRio9P2YE2x4kUCDI6ZEcX8CQiI0Xqmyk,53063
25
25
  rda_python_miscs/pg_rst.usg,sha256=oYvVEqzHeVmDntN7hfpM8vc6eoYRyQTruQQg2mPfOo4,2484
26
26
  rda_python_miscs/pg_wget.py,sha256=YK8EJQtTwGXxHav_SveXOwE_oA9KZ9l12lGSf77JQnE,7571
27
27
  rda_python_miscs/pgwget.py,sha256=TFHyquKT0tyz-r-RUAV_wpdwktpVRwyZGrHnZF2B3Vk,8115
@@ -38,11 +38,12 @@ rda_python_miscs/rdals.usg,sha256=ChF-nn3Qb2pds3wMXIWubB_tjwZslxHfSha0dTDqOiY,29
38
38
  rda_python_miscs/tcsh_qsub.py,sha256=P4Obzbp5Dvy-N0eRR5F51R9yj3ttWJo6g1NqyVDO6-Y,6670
39
39
  rda_python_miscs/tcshqsub.py,sha256=QxBq9MdVIUs9t2d6vHhkxM1nrcLwRNqcq1lJiWhXKUM,10124
40
40
  rda_python_miscs/tcshqsub.usg,sha256=JYfhrK7cqme-Sij_JfquONOs3HMu-d5dDGI9K_RdudU,2180
41
+ rda_python_miscs/rst_templates/appendix.rst.temp,sha256=HCujxbj-R4_8FgOrLSlMZcBQHUu8f6kfBMWVRygl7jc,560
41
42
  rda_python_miscs/rst_templates/index.rst.temp,sha256=YSa1JM6X9x2SC6UiqJu_9xRrVYGLKwNI43DBJEGDX08,523
42
43
  rda_python_miscs/rst_templates/section.rst.temp,sha256=-CUtutvctG2tIdkqrkFxVEzmNN3atRJXBDQaTnMJ6Gw,572
43
- rda_python_miscs-3.0.2.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
44
- rda_python_miscs-3.0.2.dist-info/METADATA,sha256=2rjjjAkl_aCtq1tCb880gu1r2oHlZCEy5iLKRg2IpRc,5803
45
- rda_python_miscs-3.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
46
- rda_python_miscs-3.0.2.dist-info/entry_points.txt,sha256=pBgb-_g4yZhm6YynwDHtNTAzxVrb8SoDMd7Eiys8gv4,806
47
- rda_python_miscs-3.0.2.dist-info/top_level.txt,sha256=W5rz7DrWb7hXABUbGgWcwe6D644X338LR8_zdgmtLhg,17
48
- rda_python_miscs-3.0.2.dist-info/RECORD,,
44
+ rda_python_miscs-3.0.4.dist-info/licenses/LICENSE,sha256=1dck4EAQwv8QweDWCXDx-4Or0S8YwiCstaso_H57Pno,1097
45
+ rda_python_miscs-3.0.4.dist-info/METADATA,sha256=nxRaTHxAU0aTXLaNZj9RChQZ70oCj0qHdWtZLVAnOOc,5803
46
+ rda_python_miscs-3.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
47
+ rda_python_miscs-3.0.4.dist-info/entry_points.txt,sha256=pBgb-_g4yZhm6YynwDHtNTAzxVrb8SoDMd7Eiys8gv4,806
48
+ rda_python_miscs-3.0.4.dist-info/top_level.txt,sha256=W5rz7DrWb7hXABUbGgWcwe6D644X338LR8_zdgmtLhg,17
49
+ rda_python_miscs-3.0.4.dist-info/RECORD,,