scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.2__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.
Files changed (104) hide show
  1. pyworkflow/apps/__init__.py +29 -0
  2. pyworkflow/apps/pw_manager.py +37 -0
  3. pyworkflow/apps/pw_plot.py +51 -0
  4. pyworkflow/apps/pw_project.py +130 -0
  5. pyworkflow/apps/pw_protocol_list.py +143 -0
  6. pyworkflow/apps/pw_protocol_run.py +51 -0
  7. pyworkflow/apps/pw_run_tests.py +268 -0
  8. pyworkflow/apps/pw_schedule_run.py +322 -0
  9. pyworkflow/apps/pw_sleep.py +37 -0
  10. pyworkflow/apps/pw_sync_data.py +440 -0
  11. pyworkflow/apps/pw_viewer.py +78 -0
  12. pyworkflow/constants.py +1 -1
  13. pyworkflow/gui/__init__.py +36 -0
  14. pyworkflow/gui/browser.py +768 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +981 -0
  17. pyworkflow/gui/form.py +2727 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +571 -0
  21. pyworkflow/gui/matplotlib_image.py +233 -0
  22. pyworkflow/gui/plotter.py +247 -0
  23. pyworkflow/gui/project/__init__.py +25 -0
  24. pyworkflow/gui/project/base.py +193 -0
  25. pyworkflow/gui/project/constants.py +139 -0
  26. pyworkflow/gui/project/labels.py +205 -0
  27. pyworkflow/gui/project/project.py +491 -0
  28. pyworkflow/gui/project/searchprotocol.py +240 -0
  29. pyworkflow/gui/project/searchrun.py +181 -0
  30. pyworkflow/gui/project/steps.py +171 -0
  31. pyworkflow/gui/project/utils.py +332 -0
  32. pyworkflow/gui/project/variables.py +179 -0
  33. pyworkflow/gui/project/viewdata.py +472 -0
  34. pyworkflow/gui/project/viewprojects.py +519 -0
  35. pyworkflow/gui/project/viewprotocols.py +2141 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +774 -0
  38. pyworkflow/gui/tooltip.py +185 -0
  39. pyworkflow/gui/tree.py +684 -0
  40. pyworkflow/gui/widgets.py +307 -0
  41. pyworkflow/mapper/__init__.py +26 -0
  42. pyworkflow/mapper/mapper.py +226 -0
  43. pyworkflow/mapper/sqlite.py +1583 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/object.py +1 -0
  46. pyworkflow/plugin.py +4 -4
  47. pyworkflow/project/__init__.py +31 -0
  48. pyworkflow/project/config.py +454 -0
  49. pyworkflow/project/manager.py +180 -0
  50. pyworkflow/project/project.py +2095 -0
  51. pyworkflow/project/usage.py +165 -0
  52. pyworkflow/protocol/__init__.py +38 -0
  53. pyworkflow/protocol/bibtex.py +48 -0
  54. pyworkflow/protocol/constants.py +87 -0
  55. pyworkflow/protocol/executor.py +515 -0
  56. pyworkflow/protocol/hosts.py +318 -0
  57. pyworkflow/protocol/launch.py +277 -0
  58. pyworkflow/protocol/package.py +42 -0
  59. pyworkflow/protocol/params.py +781 -0
  60. pyworkflow/protocol/protocol.py +2712 -0
  61. pyworkflow/resources/protlabels.xcf +0 -0
  62. pyworkflow/resources/sprites.png +0 -0
  63. pyworkflow/resources/sprites.xcf +0 -0
  64. pyworkflow/template.py +1 -1
  65. pyworkflow/tests/__init__.py +29 -0
  66. pyworkflow/tests/test_utils.py +25 -0
  67. pyworkflow/tests/tests.py +342 -0
  68. pyworkflow/utils/__init__.py +38 -0
  69. pyworkflow/utils/dataset.py +414 -0
  70. pyworkflow/utils/echo.py +104 -0
  71. pyworkflow/utils/graph.py +169 -0
  72. pyworkflow/utils/log.py +293 -0
  73. pyworkflow/utils/path.py +528 -0
  74. pyworkflow/utils/process.py +154 -0
  75. pyworkflow/utils/profiler.py +92 -0
  76. pyworkflow/utils/progressbar.py +154 -0
  77. pyworkflow/utils/properties.py +618 -0
  78. pyworkflow/utils/reflection.py +129 -0
  79. pyworkflow/utils/utils.py +880 -0
  80. pyworkflow/utils/which.py +229 -0
  81. pyworkflow/webservices/__init__.py +8 -0
  82. pyworkflow/webservices/config.py +8 -0
  83. pyworkflow/webservices/notifier.py +152 -0
  84. pyworkflow/webservices/repository.py +59 -0
  85. pyworkflow/webservices/workflowhub.py +86 -0
  86. pyworkflowtests/tests/__init__.py +0 -0
  87. pyworkflowtests/tests/test_canvas.py +72 -0
  88. pyworkflowtests/tests/test_domain.py +45 -0
  89. pyworkflowtests/tests/test_logs.py +74 -0
  90. pyworkflowtests/tests/test_mappers.py +392 -0
  91. pyworkflowtests/tests/test_object.py +507 -0
  92. pyworkflowtests/tests/test_project.py +42 -0
  93. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  94. pyworkflowtests/tests/test_protocol_export.py +78 -0
  95. pyworkflowtests/tests/test_protocol_output.py +158 -0
  96. pyworkflowtests/tests/test_streaming.py +47 -0
  97. pyworkflowtests/tests/test_utils.py +210 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
  99. scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
  100. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  101. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
  102. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
  103. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
  104. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,528 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
4
+ # *
5
+ # * [1] SciLifeLab, Stockholm University
6
+ # *
7
+ # * This program is free software: you can redistribute it and/or modify
8
+ # * it under the terms of the GNU General Public License as published by
9
+ # * the Free Software Foundation, either version 3 of the License, or
10
+ # * (at your option) any later version.
11
+ # *
12
+ # * This program is distributed in the hope that it will be useful,
13
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # * GNU General Public License for more details.
16
+ # *
17
+ # * You should have received a copy of the GNU General Public License
18
+ # * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ # *
20
+ # * All comments concerning this program package may be sent to the
21
+ # * e-mail address 'scipion@cnb.csic.es'
22
+ # *
23
+ # **************************************************************************
24
+ """
25
+ This module contains the PATH related utilities
26
+ inside the utils module
27
+ """
28
+
29
+ import logging
30
+
31
+ from pyworkflow.utils import yellow
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ import os
36
+ import shutil
37
+ import sys
38
+ from glob import glob
39
+ import datetime
40
+
41
+ from pyworkflow import SCIPION_SCRATCH, DOCSITEURLS, ASCII_COLOR_2_TKINTER
42
+ from pyworkflow.exceptions import PyworkflowException
43
+ from pyworkflow.config import Config
44
+
45
+ ROOT = "/"
46
+
47
+
48
+ def findFileRecursive(filename, path):
49
+ """
50
+ Finds a file/folder in a single path recursively
51
+
52
+ :param filename: Name (myfile.txt) of the file to look for.
53
+ :param path: folder to start the search from.
54
+ :return: The absolute path to the 'filename' found or None if not found.
55
+ """
56
+ for root, dirs, files in os.walk(path):
57
+ if filename in files or filename in dirs:
58
+ return os.path.join(root, filename)
59
+ return None
60
+
61
+
62
+ def findFile(filename, *paths, recursive=False):
63
+ """
64
+ Search if a file/folder is present in any of the paths provided.
65
+
66
+ :param filename: Name (myfile.txt) of the file to look for.
67
+ :param paths: N number of folders to look at.
68
+ :param recursive: If True it will iterate into the subfolders.
69
+ :return: None if nothing is found.
70
+ """
71
+
72
+ if filename:
73
+ for p in paths:
74
+ fn = os.path.join(p, filename)
75
+ if os.path.exists(fn):
76
+ return fn
77
+ if recursive:
78
+ f = findFileRecursive(filename, p)
79
+ if f:
80
+ return f
81
+ return None
82
+
83
+
84
+ def findRootFrom(referenceFile, searchFile):
85
+ """ This method will find a path (root) from 'referenceFile'
86
+ from which the 'searchFile' os.path.exists.
87
+ A practical example of 'referenceFile' is a metadata file
88
+ and 'searchFile' is an image to be found from the metadata.
89
+ Return None if the path is not found.
90
+ """
91
+ absPath = os.path.dirname(os.path.abspath(referenceFile))
92
+
93
+ while absPath is not None and absPath != '/':
94
+ if os.path.exists(os.path.join(absPath, searchFile)):
95
+ return absPath
96
+ absPath = os.path.dirname(absPath)
97
+
98
+ return None
99
+
100
+
101
+ def getParentFolder(path):
102
+ """ Returns the absolute parent folder of a file or folder. Work for
103
+ folders that ens with "/" which dirname can't"""
104
+ return os.path.dirname(os.path.abspath(path))
105
+
106
+
107
+ def replaceExt(filename, newExt):
108
+ """ Replace the current path extension(from last .)
109
+ with a new one. The new one should not contain the ."""
110
+ return os.path.splitext(filename)[0] + '.' + newExt
111
+
112
+
113
+ def replaceBaseExt(filename, newExt):
114
+ """ Replace the current basename extension(from last .)
115
+ with a new one. The new one should not contains the .
116
+ """
117
+ return replaceExt(os.path.basename(filename), newExt)
118
+
119
+
120
+ def removeBaseExt(filename):
121
+ """Take the basename of the filename and remove extension"""
122
+ return removeExt(os.path.basename(filename))
123
+
124
+
125
+ def removeExt(filename):
126
+ """ Remove extension from basename """
127
+ return os.path.splitext(filename)[0]
128
+
129
+
130
+ def joinExt(*extensions):
131
+ """ Join several path parts with a ."""
132
+ return '.'.join(extensions)
133
+
134
+
135
+ def getExt(filePath):
136
+ """ Return the extension given a file. """
137
+ return os.path.splitext(filePath)[1]
138
+
139
+
140
+ def cleanPath(*paths):
141
+ """ Remove a list of paths, either folders or files"""
142
+ for p in paths:
143
+ if os.path.exists(p):
144
+ if os.path.isdir(p):
145
+ if os.path.islink(p):
146
+ os.remove(p)
147
+ else:
148
+ shutil.rmtree(p)
149
+ else:
150
+ os.remove(p)
151
+
152
+
153
+ def cleanPattern(pattern):
154
+ """ Remove all files that match the pattern. """
155
+ files = glob(pattern)
156
+ cleanPath(*files)
157
+
158
+
159
+ def copyPattern(pattern, destFolder):
160
+ """ Copy all files matching the pattern to the given destination folder."""
161
+ for file in glob(pattern):
162
+ copyFile(file, destFolder)
163
+
164
+
165
+ def makePath(*paths):
166
+ """ Create a list of paths if they don't os.path.exists.
167
+ Recursively create all folder needed in a path.
168
+ If a path passed is a file, only the directory will be created.
169
+ """
170
+ for p in paths:
171
+ if not os.path.exists(p) and len(p):
172
+ os.makedirs(p)
173
+
174
+
175
+ def makeTmpPath(protocol):
176
+ """
177
+ Create the scratch folder if SCIPION_SCRATCH variable is defined into the
178
+ Scipion config, i.o.c create tmp folder
179
+ """
180
+ tmpPath = protocol._getTmpPath()
181
+ if not os.path.exists(tmpPath) and len(tmpPath):
182
+ scratchPath = Config.SCIPION_SCRATCH
183
+
184
+ if scratchPath is None: # Case when SCIPION_SCRATCH doesn't exist. TMP folder is created
185
+ os.makedirs(tmpPath)
186
+ else:
187
+ try:
188
+ project = protocol.getProject()
189
+ folderId = "_".join([project.getShortName(),project.getProtWorkingDir(protocol)])
190
+ tmpScratchFolder = os.path.join(scratchPath, folderId)
191
+ if os.path.exists(tmpScratchFolder):
192
+ cleanPath(tmpScratchFolder)
193
+ os.makedirs(tmpScratchFolder) # Create scratch folder
194
+ createAbsLink(tmpScratchFolder, tmpPath) # Create a sym link
195
+
196
+ except Exception as e:
197
+ raise PyworkflowException("Couldn't create the temporary folder %s at:\n %s\nPlease, review %s variable." %
198
+ (folderId, scratchPath, SCIPION_SCRATCH), url=DOCSITEURLS.CONFIG_SECTION % "scratch-folder") from e
199
+
200
+
201
+ def makeFilePath(*files):
202
+ """ Make the path to ensure that files can be written. """
203
+ makePath(*[os.path.dirname(f) for f in files])
204
+
205
+
206
+ def missingPaths(*paths):
207
+ """ Check if the list of paths os.path.exists.
208
+ Will return the list of missing files,
209
+ if the list is empty means that all path os.path.exists
210
+ """
211
+ return [p for p in paths if not os.path.exists(p)]
212
+
213
+
214
+ def getHomePath(user=''):
215
+ """Return the home path of a give user."""
216
+ return os.path.expanduser("~" + user)
217
+
218
+
219
+ def expandPattern(pattern, vars=True, user=True):
220
+ """ Expand environment vars and user from a given pattern. """
221
+ if vars:
222
+ pattern = os.path.expandvars(pattern)
223
+ if user:
224
+ pattern = os.path.expanduser(pattern)
225
+ return pattern
226
+
227
+
228
+ def getFiles(folderPath):
229
+ """
230
+ Gets all files of given folder and it subfolders.
231
+ folderPath -- Folder path to get files.
232
+ returns -- Set with all folder files.
233
+ """
234
+ filePaths = set()
235
+ for path, dirs, files in os.walk(folderPath):
236
+ for f in files:
237
+ filePaths.add(os.path.join(path, f))
238
+ return filePaths
239
+
240
+
241
+ def copyTree(source, dest):
242
+ """
243
+ Wrapper around the shutil.copytree, but allowing
244
+ that the dest folder also os.path.exists.
245
+ """
246
+ if not os.path.exists(dest):
247
+ shutil.copytree(source, dest, symlinks=True)
248
+ else:
249
+ for f in os.listdir(source):
250
+ fnPath = os.path.join(source, f)
251
+ if os.path.isfile(fnPath):
252
+ shutil.copy(fnPath, dest)
253
+ elif os.path.isdir(fnPath):
254
+ copyTree(fnPath, os.path.join(dest, f))
255
+
256
+
257
+ def moveTree(src, dest):
258
+ copyTree(src, dest)
259
+ cleanPath(src)
260
+
261
+
262
+ def copyFile(source, dest):
263
+ """ Shortcut to shutil.copy. """
264
+ shutil.copy(source, dest)
265
+
266
+
267
+ def moveFile(source, dest):
268
+ """ Move file from source to dest. """
269
+ copyFile(source, dest)
270
+ cleanPath(source)
271
+
272
+
273
+ def createLink(source, dest):
274
+ """ Creates a relative link to a given file path.
275
+ Try to use common path for source and dest to avoid errors.
276
+ Different relative paths may exist since there are different valid paths
277
+ for a file, it depends on the current working dir path"""
278
+ if os.path.islink(dest):
279
+ os.remove(dest)
280
+
281
+ if os.path.exists(dest):
282
+ raise Exception('Destination %s os.path.exists and is not a link'
283
+ % dest)
284
+ sourcedir = getParentFolder(source)
285
+ destdir = getParentFolder(dest)
286
+ relsource = os.path.join(os.path.relpath(sourcedir, destdir),
287
+ os.path.basename(source))
288
+ os.symlink(relsource, dest)
289
+
290
+
291
+ def createAbsLink(source, dest):
292
+ """ Creates a link to a given file path"""
293
+ if os.path.islink(dest):
294
+ os.remove(dest)
295
+
296
+ if os.path.exists(dest):
297
+ raise Exception('Destination %s os.path.exists and is not a link' % dest)
298
+
299
+ source = os.path.abspath(source)
300
+ os.symlink(source, dest)
301
+
302
+
303
+ def getLastFile(pattern):
304
+ """ Return the last file matching the pattern. """
305
+ files = glob(pattern)
306
+ if len(files):
307
+ files.sort()
308
+ return files[-1]
309
+ return None
310
+
311
+
312
+ def commonPath(*paths):
313
+ """ Return the common longest prefix path.
314
+ It uses the python os.path.commonprefix and
315
+ then the direname over it since the former is
316
+ implemented in char-by-char base.
317
+ """
318
+ return os.path.dirname(os.path.commonprefix(*paths))
319
+
320
+
321
+
322
+ def renderTextFile(fname, add, offset=0, lineNo=0, numberLines=True,
323
+ maxSize=400, headSize=40, tailSize=None, notifyLine=None, errors='strict'):
324
+ """
325
+ Call callback function add() on each fragment of text from file fname,
326
+ delimited by lines and/or color codes.
327
+
328
+ :param add: callback function with signature (txt, tag='normal')
329
+ :param offset: byte offset - we start reading the file from there
330
+ :param lineNo: lines will be numbered from this value on
331
+ :param numberLines: whether to prepend the line numbers
332
+
333
+ """
334
+ textfile = open(fname, encoding='utf-8', errors=errors)
335
+ size = (os.stat(fname).st_size - offset) / 1024 # in kB
336
+
337
+ for line in iterBigFile(textfile, offset, size,
338
+ maxSize, headSize, tailSize):
339
+ if line is not None:
340
+ lineNo += 1
341
+ if notifyLine is not None:
342
+ notifyLine(line)
343
+ renderLine(line, add, lineNo, numberLines)
344
+ else:
345
+ add("""\n
346
+ ==> Too much data to read (%d kB) -- %d kB omitted
347
+ ==> Click on """ % (size, size - headSize - (tailSize or headSize)))
348
+ add(fname, 'link:%s' % fname)
349
+ add(' to open it with the default viewer\n\n')
350
+ if numberLines:
351
+ add(' ==> Line numbers below are not '
352
+ 'in sync with the input data\n\n')
353
+
354
+ offset = textfile.tell() # save last position in file
355
+ textfile.close()
356
+
357
+ return offset, lineNo
358
+
359
+
360
+ def renderLine(line, add, lineNo=1, numberLines=True):
361
+ """
362
+ Find all the fragments of formatted text in line and call
363
+ add(fragment, tag) for each of them.
364
+ """
365
+ # Prepend line number
366
+ if numberLines and lineNo:
367
+ add('%05d:' % lineNo, 'cyan')
368
+ add(' ')
369
+
370
+ # iter 1\riter 2\riter 3 --> iter 3
371
+ if '\r' in line:
372
+ line = line[line.rfind('\r')+1:] # overwriting!
373
+
374
+ # Find all console escape codes and use the appropriate tag instead.
375
+ pos = 0 # current position in the line we are parsing
376
+ attribute = None
377
+ while True:
378
+ # line looks like:
379
+ # 'blah blah \x1b[{attr1};...;{attrn}mTEXT\x1b[0m blah blah'
380
+ # where {attrn} is the color code (31 is red, for example). See
381
+ # http://www.termsys.demon.co.uk/vtansi.htm#colors
382
+ start = line.find('\x1b[', pos)
383
+ if start < 0: # no more escape codes, just add the remaining text
384
+ add(line[pos:], attribute)
385
+ break
386
+
387
+ add(line[pos:start], attribute)
388
+ end = line.find('m', start+2)
389
+ if end < 0: # an escape code interrupted by newline... weird
390
+ break
391
+ code = line[start+2:end]
392
+
393
+ # See what attribute to use from now on, and update pos
394
+ if code == '0':
395
+ attribute = None
396
+ else:
397
+ attribute = ASCII_COLOR_2_TKINTER.get(code[-2:], None)
398
+ pos = end + 1 # go to the character next to "m", the closing char
399
+
400
+
401
+ def iterBigFile(textfile, offset=0, size=None,
402
+ maxSize=400, headSize=40, tailSize=None):
403
+ """
404
+ Yield lines from file textfile. If the size to read is bigger
405
+ than maxSize then yield the first lines until headSize bytes, then
406
+ yield None, then yield the last lines from tailSize bytes to the end.
407
+ """
408
+ if size is None:
409
+ # Size in kB of the part of the file that we will read
410
+ textfile.seek(0, 2)
411
+ sizeKb = (textfile.tell() - offset) / 1024
412
+ else:
413
+ sizeKb = size
414
+
415
+ headSizeB = headSize * 1024
416
+ tailSizeB = (tailSize or headSize) * 1024
417
+
418
+ textfile.seek(offset)
419
+ # If the size is bigger than the max that we want to read (in kB).
420
+ if 0 < maxSize < sizeKb:
421
+ # maxSize <= 0 means we just want to read it all and not enter here.
422
+ for line in textfile.read(headSizeB).split('\n'):
423
+ yield line + '\n'
424
+ yield None # Special result to mark omitting lines
425
+ textfile.seek(sizeKb*1024-tailSizeB) # ready to show the last bytes
426
+
427
+ # Add the remaining lines (from our last offset)
428
+ for line in textfile:
429
+ yield line
430
+
431
+
432
+ def createUniqueFileName(fn):
433
+ """
434
+ This function creates a file name that is similar to the original
435
+ by adding a unique numeric suffix. check NamedTemporaryFile
436
+ from tempfile for alternatives
437
+ """
438
+ if not os.path.os.path.exists(fn):
439
+ return fn
440
+
441
+ path, name = os.path.split(fn)
442
+ name, ext = os.path.splitext(name)
443
+
444
+ make_fn = lambda i: os.path.join(path, '%s_tmp_%d_%s' % (name, i, ext))
445
+
446
+ for i in range(2, sys.maxsize):
447
+ uni_fn = make_fn(i)
448
+ if not os.path.os.path.exists(uni_fn):
449
+ return uni_fn
450
+
451
+ return None
452
+
453
+
454
+ def getFileSize(fn):
455
+ """ Shortcut to inspect the size of a file or a folder. """
456
+
457
+ if not os.path.exists(fn):
458
+ return 0
459
+
460
+ elif os.path.isdir(fn):
461
+ total_size = 0
462
+ for dirpath, dirnames, filenames in os.walk(fn):
463
+ for f in filenames:
464
+ fp = os.path.join(dirpath, f)
465
+ if not os.path.islink(fp):
466
+ total_size += os.path.getsize(fp)
467
+ return total_size
468
+
469
+ else:
470
+ return os.path.getsize(fn)
471
+
472
+
473
+ def hasChangedSince(fn, time):
474
+ """ Returns if the file has changed since the timestamp passed as parameter. It will check
475
+ the last modified time of the file this set uses to persists.
476
+
477
+ :parameter time: timestamp to compare to the last modification time """
478
+
479
+ if time is None:
480
+ return True
481
+
482
+ # Get the last time it was modified
483
+ modTime = getFileLastModificationDate(fn)
484
+
485
+ return time < modTime
486
+
487
+ def isFileFinished(fn , duration=60):
488
+ """ Returns True if the file (fn) last modification has not changed for 60 seconds (default duration)"""
489
+
490
+ modTime = getFileLastModificationDate(fn)
491
+ fileAge = datetime.datetime.now()-modTime
492
+
493
+ return fileAge.seconds > duration
494
+
495
+
496
+ def getFileLastModificationDate(fn):
497
+ """ Returns the last modification date of a file or None
498
+ if it doesn't exist. """
499
+ if os.path.exists(fn):
500
+ ts = os.path.getmtime(fn)
501
+ return datetime.datetime.fromtimestamp(ts)
502
+ else:
503
+ logger.info(fn + " does not exist!!. Can't check last modification date.")
504
+ return None
505
+
506
+
507
+ def backup(fpath):
508
+ """
509
+ Create directory "backup" if necessary and back up the file.
510
+
511
+ :param fpath:
512
+ :return: None
513
+
514
+ """
515
+ BACKUPS="backup"
516
+
517
+ dname = os.path.dirname(fpath)
518
+
519
+ if not os.path.exists(dname):
520
+ os.makedirs(dname)
521
+
522
+ elif os.path.exists(fpath):
523
+ if not os.path.exists(os.path.join(dname, BACKUPS)):
524
+ os.makedirs(os.path.join(dname, BACKUPS))
525
+ backupFn = os.path.join(dname, BACKUPS,
526
+ '%s.%s' % (os.path.basename(fpath), datetime.datetime.now().strftime("%Y%m%d%H%M%S")))
527
+ logger.info(yellow("* Creating backup: %s" % backupFn))
528
+ os.rename(fpath, backupFn)
@@ -0,0 +1,154 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
4
+ # * Laura del Cano (ldelcano@cnb.csic.es)
5
+ # *
6
+ # * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
7
+ # *
8
+ # * This program is free software; you can redistribute it and/or modify
9
+ # * it under the terms of the GNU General Public License as published by
10
+ # * the Free Software Foundation; either version 3 of the License, or
11
+ # * (at your option) any later version.
12
+ # *
13
+ # * This program is distributed in the hope that it will be useful,
14
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # * GNU General Public License for more details.
17
+ # *
18
+ # * You should have received a copy of the GNU General Public License
19
+ # * along with this program; if not, write to the Free Software
20
+ # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21
+ # * 02111-1307 USA
22
+ # *
23
+ # * All comments concerning this program package may be sent to the
24
+ # * e-mail address 'scipion@cnb.csic.es'
25
+ # *
26
+ # **************************************************************************
27
+ """
28
+ This module handles process execution
29
+ """
30
+
31
+ import logging
32
+ logger = logging.getLogger(__name__)
33
+
34
+ import sys
35
+ from subprocess import check_call
36
+ import psutil
37
+
38
+ from .utils import greenStr
39
+ from pyworkflow import Config
40
+ from pyworkflow.constants import PLUGIN_MODULE_VAR, PARALLEL_COMMAND_VAR, RUN_JOB_GPU_PARAM
41
+
42
+
43
+ # The job should be launched from the working directory!
44
+ def runJob(log, programname, params,
45
+ numberOfMpi=1, numberOfThreads=1,
46
+ hostConfig=None, env=None, cwd=None, gpuList=None, executable=None, context=dict()):
47
+
48
+ command = buildRunCommand(programname, params, numberOfMpi, hostConfig,
49
+ env, gpuList=gpuList,context=context)
50
+
51
+ if log is None:
52
+ log = logger
53
+
54
+ log.info("** Running command: **")
55
+ log.info(greenStr(command))
56
+
57
+ return runCommand(command, env=env, cwd=cwd, executable=executable)
58
+
59
+
60
+ def runCommand(command, env=None, cwd=None, executable=None):
61
+ """ Execute command with given environment env and directory cwd """
62
+
63
+ # First let us create core dumps if in debug mode
64
+ if Config.debugOn():
65
+ import resource
66
+ resource.setrlimit(resource.RLIMIT_CORE,
67
+ (resource.RLIM_INFINITY, resource.RLIM_INFINITY))
68
+ # This is like "ulimit -u 99999999", so we can create core dumps
69
+
70
+ # TODO: maybe have to set PBS_NODEFILE in case it is used by "command"
71
+ # (useful for example with gnu parallel)
72
+ check_call(command, shell=True, stdout=sys.stdout, stderr=sys.stderr,
73
+ env=env, cwd=cwd, executable=executable)
74
+ # It would be nice to avoid shell=True and calling buildRunCommand()...
75
+
76
+
77
+ def buildRunCommand(programname, params, numberOfMpi, hostConfig=None,
78
+ env=None, gpuList=None, context=dict()):
79
+ """ Return a string with the command line to run
80
+
81
+ :param context: a dictionary with extra context variable to make run command more flexible"""
82
+
83
+ # Convert our list of params to a string, with each element escaped
84
+ # with "" in case there are spaces.
85
+ if not isinstance(params, str):
86
+ params = ' '.join('"%s"' % p for p in params)
87
+
88
+ if gpuList:
89
+ params = params % {RUN_JOB_GPU_PARAM: ' '.join(str(g) for g in gpuList)}
90
+ if "CUDA_VISIBLE_DEVICES" in programname:
91
+ sep = "," if len(gpuList) > 1 else ""
92
+ programname = programname % {RUN_JOB_GPU_PARAM: sep.join(str(g) for g in gpuList)}
93
+
94
+ prepend = '' if env is None else env.getPrepend()
95
+
96
+ if numberOfMpi <= 1:
97
+ return '%s %s %s' % (prepend, programname, params)
98
+ else:
99
+ if hostConfig is None:
100
+ raise AssertionError('hostConfig needed to launch MPI processes.')
101
+
102
+ if programname.startswith('xmipp') and not programname.startswith('xmipp_mpi'):
103
+ programname = programname.replace('xmipp', 'xmipp_mpi')
104
+
105
+ mpiFlags = '' if env is None else env.get('SCIPION_MPI_FLAGS', '')
106
+
107
+ context.update({
108
+ 'JOB_NODES': numberOfMpi,
109
+ 'COMMAND': "%s `which %s` %s" % (mpiFlags, programname, params),
110
+ })
111
+ logger.debug("Context variables for mpi command are: %s" % context)
112
+
113
+ mpiCommand = hostConfig.mpiCommand.get()
114
+ pluginModule = context.get(PLUGIN_MODULE_VAR, None)
115
+
116
+ if pluginModule is not None:
117
+ custom_command_var = PARALLEL_COMMAND_VAR + "_" + pluginModule.upper()
118
+ if custom_command_var in env:
119
+ mpiCommand = env.get(custom_command_var)
120
+ logger.info("Custom mpi command for this plugin found. Using it. %s: %s"
121
+ % (custom_command_var, mpiCommand))
122
+ else:
123
+ logger.info("%s not found in the environment. Using default mpi command found in %s. %s: %s"
124
+ % (custom_command_var, Config.SCIPION_HOSTS, PARALLEL_COMMAND_VAR, mpiCommand))
125
+
126
+
127
+ mpiCmd = mpiCommand % context
128
+
129
+ return '%s %s' % (prepend, mpiCmd)
130
+
131
+
132
+ def killWithChilds(pid):
133
+ """ Kill the process with given pid and all children processes.
134
+
135
+ :param pid: the process id to terminate
136
+ """
137
+ proc = psutil.Process(pid)
138
+ for c in proc.children(recursive=True):
139
+ if c.pid is not None:
140
+ logger.info("Terminating child pid: %d" % c.pid)
141
+ c.kill()
142
+ logger.info("Terminating process pid: %s" % pid)
143
+ if pid is None:
144
+ logger.warning("Got None PID!!!")
145
+ else:
146
+ proc.kill()
147
+
148
+
149
+ def isProcessAlive(pid):
150
+ try:
151
+ psutil.Process(pid)
152
+ return True
153
+ except Exception:
154
+ return False