##// END OF EJS Templates
Beta version!
Juan C. Espinoza -
r1330:959603ec6724
parent child
Show More
1 NO CONTENT: file renamed from LICENSE to LICENSE.txt
NO CONTENT: file renamed from LICENSE to LICENSE.txt
@@ -1,238 +1,238
1 import click
1 import click
2 import subprocess
2 import subprocess
3 import os
3 import os
4 import sys
4 import sys
5 import glob
5 import glob
6 import schainpy
6 import schainpy
7 from schainpy.controller import Project
7 from schainpy.controller import Project
8 from schainpy.model import Operation, ProcessingUnit
8 from schainpy.model import Operation, ProcessingUnit
9 from schainpy.utils import log
9 from schainpy.utils import log
10 from importlib import import_module
10 from importlib import import_module
11 from pydoc import locate
11 from pydoc import locate
12 from fuzzywuzzy import process
12 from fuzzywuzzy import process
13 from schainpy.cli import templates
13 from schainpy.cli import templates
14 import inspect
14 import inspect
15 try:
15 try:
16 from queue import Queue
16 from queue import Queue
17 except:
17 except:
18 from Queue import Queue
18 from Queue import Queue
19
19
20
20
21 def getProcs():
21 def getProcs():
22 modules = dir(schainpy.model)
22 modules = dir(schainpy.model)
23 procs = check_module(modules, 'processing')
23 procs = check_module(modules, 'processing')
24 try:
24 try:
25 procs.remove('ProcessingUnit')
25 procs.remove('ProcessingUnit')
26 except Exception as e:
26 except Exception as e:
27 pass
27 pass
28 return procs
28 return procs
29
29
30 def getOperations():
30 def getOperations():
31 module = dir(schainpy.model)
31 module = dir(schainpy.model)
32 noProcs = [x for x in module if not x.endswith('Proc')]
32 noProcs = [x for x in module if not x.endswith('Proc')]
33 operations = check_module(noProcs, 'operation')
33 operations = check_module(noProcs, 'operation')
34 try:
34 try:
35 operations.remove('Operation')
35 operations.remove('Operation')
36 operations.remove('Figure')
36 operations.remove('Figure')
37 operations.remove('Plot')
37 operations.remove('Plot')
38 except Exception as e:
38 except Exception as e:
39 pass
39 pass
40 return operations
40 return operations
41
41
42 def getArgs(op):
42 def getArgs(op):
43 module = locate('schainpy.model.{}'.format(op))
43 module = locate('schainpy.model.{}'.format(op))
44 try:
44 try:
45 obj = module(1, 2, 3, Queue())
45 obj = module(1, 2, 3, Queue())
46 except:
46 except:
47 obj = module()
47 obj = module()
48
48
49 if hasattr(obj, '__attrs__'):
49 if hasattr(obj, '__attrs__'):
50 args = obj.__attrs__
50 args = obj.__attrs__
51 else:
51 else:
52 if hasattr(obj, 'myrun'):
52 if hasattr(obj, 'myrun'):
53 args = inspect.getfullargspec(obj.myrun).args
53 args = inspect.getfullargspec(obj.myrun).args
54 else:
54 else:
55 args = inspect.getfullargspec(obj.run).args
55 args = inspect.getfullargspec(obj.run).args
56
56
57 try:
57 try:
58 args.remove('self')
58 args.remove('self')
59 except Exception as e:
59 except Exception as e:
60 pass
60 pass
61 try:
61 try:
62 args.remove('dataOut')
62 args.remove('dataOut')
63 except Exception as e:
63 except Exception as e:
64 pass
64 pass
65 return args
65 return args
66
66
67 def getDoc(obj):
67 def getDoc(obj):
68 module = locate('schainpy.model.{}'.format(obj))
68 module = locate('schainpy.model.{}'.format(obj))
69 try:
69 try:
70 obj = module(1, 2, 3, Queue())
70 obj = module(1, 2, 3, Queue())
71 except:
71 except:
72 obj = module()
72 obj = module()
73 return obj.__doc__
73 return obj.__doc__
74
74
75 def getAll():
75 def getAll():
76 modules = getOperations()
76 modules = getOperations()
77 modules.extend(getProcs())
77 modules.extend(getProcs())
78 return modules
78 return modules
79
79
80
80
81 def print_version(ctx, param, value):
81 def print_version(ctx, param, value):
82 if not value or ctx.resilient_parsing:
82 if not value or ctx.resilient_parsing:
83 return
83 return
84 click.echo(schainpy.__version__)
84 click.echo(schainpy.__version__)
85 ctx.exit()
85 ctx.exit()
86
86
87
87
88 PREFIX = 'experiment'
88 PREFIX = 'experiment'
89
89
90 @click.command()
90 @click.command()
91 @click.option('--version', '-v', is_flag=True, callback=print_version, help='SChain version', type=str)
91 @click.option('--version', '-v', is_flag=True, callback=print_version, help='SChain version', type=str)
92 @click.argument('command', default='run', required=True)
92 @click.argument('command', default='run', required=True)
93 @click.argument('nextcommand', default=None, required=False, type=str)
93 @click.argument('nextcommand', default=None, required=False, type=str)
94 def main(command, nextcommand, version):
94 def main(command, nextcommand, version):
95 """COMMAND LINE INTERFACE FOR SIGNAL CHAIN - JICAMARCA RADIO OBSERVATORY V3.0\n
95 """COMMAND LINE INTERFACE FOR SIGNAL CHAIN - JICAMARCA RADIO OBSERVATORY V3.0\n
96 Available commands:\n
96 Available commands:\n
97 xml: runs a schain XML generated file\n
97 xml: runs a schain XML generated file\n
98 run: runs any python script'\n
98 run: runs any python script'\n
99 generate: generates a template schain script\n
99 generate: generates a template schain script\n
100 list: return a list of available procs and operations\n
100 list: return a list of available procs and operations\n
101 search: return avilable operations, procs or arguments of the given
101 search: return avilable operations, procs or arguments of the given
102 operation/proc\n"""
102 operation/proc\n"""
103 if command == 'xml':
103 if command == 'xml':
104 runFromXML(nextcommand)
104 runFromXML(nextcommand)
105 elif command == 'generate':
105 elif command == 'generate':
106 generate()
106 generate()
107 elif command == 'test':
107 elif command == 'test':
108 test()
108 test()
109 elif command == 'run':
109 elif command == 'run':
110 runschain(nextcommand)
110 runschain(nextcommand)
111 elif command == 'search':
111 elif command == 'search':
112 search(nextcommand)
112 search(nextcommand)
113 elif command == 'list':
113 elif command == 'list':
114 cmdlist(nextcommand)
114 cmdlist(nextcommand)
115 else:
115 else:
116 log.error('Command {} is not defined'.format(command))
116 log.error('Command {} is not defined'.format(command))
117
117
118
118
119 def check_module(possible, instance):
119 def check_module(possible, instance):
120 def check(x):
120 def check(x):
121 try:
121 try:
122 instancia = locate('schainpy.model.{}'.format(x))
122 instancia = locate('schainpy.model.{}'.format(x))
123 ret = instancia.proc_type == instance
123 ret = instancia.proc_type == instance
124 return ret
124 return ret
125 except Exception as e:
125 except Exception as e:
126 return False
126 return False
127 clean = clean_modules(possible)
127 clean = clean_modules(possible)
128 return [x for x in clean if check(x)]
128 return [x for x in clean if check(x)]
129
129
130
130
131 def clean_modules(module):
131 def clean_modules(module):
132 noEndsUnder = [x for x in module if not x.endswith('__')]
132 noEndsUnder = [x for x in module if not x.endswith('__')]
133 noStartUnder = [x for x in noEndsUnder if not x.startswith('__')]
133 noStartUnder = [x for x in noEndsUnder if not x.startswith('__')]
134 noFullUpper = [x for x in noStartUnder if not x.isupper()]
134 noFullUpper = [x for x in noStartUnder if not x.isupper()]
135 return noFullUpper
135 return noFullUpper
136
136
137 def cmdlist(nextcommand):
137 def cmdlist(nextcommand):
138 if nextcommand is None:
138 if nextcommand is None:
139 log.error('Missing argument, available arguments: procs, operations', '')
139 log.error('Missing argument, available arguments: procs, operations', '')
140 elif nextcommand == 'procs':
140 elif nextcommand == 'procs':
141 procs = getProcs()
141 procs = getProcs()
142 log.success(
142 log.success(
143 'Current ProcessingUnits are:\n {}'.format('\n '.join(procs)), '')
143 'Current ProcessingUnits are:\n {}'.format('\n '.join(procs)), '')
144 elif nextcommand == 'operations':
144 elif nextcommand == 'operations':
145 operations = getOperations()
145 operations = getOperations()
146 log.success('Current Operations are:\n {}'.format(
146 log.success('Current Operations are:\n {}'.format(
147 '\n '.join(operations)), '')
147 '\n '.join(operations)), '')
148 else:
148 else:
149 log.error('Wrong argument', '')
149 log.error('Wrong argument', '')
150
150
151 def search(nextcommand):
151 def search(nextcommand):
152 if nextcommand is None:
152 if nextcommand is None:
153 log.error('There is no Operation/ProcessingUnit to search', '')
153 log.error('There is no Operation/ProcessingUnit to search', '')
154 else:
154 else:
155 try:
155 try:
156 args = getArgs(nextcommand)
156 args = getArgs(nextcommand)
157 doc = getDoc(nextcommand)
157 doc = getDoc(nextcommand)
158 log.success('{}\n{}\n\narguments:\n {}'.format(
158 log.success('{}\n{}\n\nparameters:\n {}'.format(
159 nextcommand, doc, ', '.join(args)), ''
159 nextcommand, doc, ', '.join(args)), ''
160 )
160 )
161 except Exception as e:
161 except Exception as e:
162 log.error('Module `{}` does not exists'.format(nextcommand), '')
162 log.error('Module `{}` does not exists'.format(nextcommand), '')
163 allModules = getAll()
163 allModules = getAll()
164 similar = [t[0] for t in process.extract(nextcommand, allModules, limit=12) if t[1]>80]
164 similar = [t[0] for t in process.extract(nextcommand, allModules, limit=12) if t[1]>80]
165 log.success('Possible modules are: {}'.format(', '.join(similar)), '')
165 log.success('Possible modules are: {}'.format(', '.join(similar)), '')
166
166
167 def runschain(nextcommand):
167 def runschain(nextcommand):
168 if nextcommand is None:
168 if nextcommand is None:
169 currentfiles = glob.glob('./{}_*.py'.format(PREFIX))
169 currentfiles = glob.glob('./{}_*.py'.format(PREFIX))
170 numberfiles = len(currentfiles)
170 numberfiles = len(currentfiles)
171 if numberfiles > 1:
171 if numberfiles > 1:
172 log.error('There is more than one file to run')
172 log.error('There is more than one file to run')
173 elif numberfiles == 1:
173 elif numberfiles == 1:
174 subprocess.call(['python ' + currentfiles[0]], shell=True)
174 subprocess.call(['python ' + currentfiles[0]], shell=True)
175 else:
175 else:
176 log.error('There is no file to run')
176 log.error('There is no file to run')
177 else:
177 else:
178 try:
178 try:
179 subprocess.call(['python ' + nextcommand], shell=True)
179 subprocess.call(['python ' + nextcommand], shell=True)
180 except Exception as e:
180 except Exception as e:
181 log.error("I cannot run the file. Does it exists?")
181 log.error("I cannot run the file. Does it exists?")
182
182
183
183
184 def basicInputs():
184 def basicInputs():
185 inputs = {}
185 inputs = {}
186 inputs['name'] = click.prompt(
186 inputs['name'] = click.prompt(
187 'Name of the project', default="project", type=str)
187 'Name of the project', default="project", type=str)
188 inputs['desc'] = click.prompt(
188 inputs['desc'] = click.prompt(
189 'Enter a description', default="A schain project", type=str)
189 'Enter a description', default="A schain project", type=str)
190 inputs['multiprocess'] = click.prompt(
190 inputs['multiprocess'] = click.prompt(
191 '''Select data type:
191 '''Select data type:
192
192
193 - Voltage (*.r): [1]
193 - Voltage (*.r): [1]
194 - Spectra (*.pdata): [2]
194 - Spectra (*.pdata): [2]
195 - Voltage and Spectra (*.r): [3]
195 - Voltage and Spectra (*.r): [3]
196
196
197 -->''', type=int)
197 -->''', type=int)
198 inputs['path'] = click.prompt('Data path', default=os.getcwd(
198 inputs['path'] = click.prompt('Data path', default=os.getcwd(
199 ), type=click.Path(exists=True, resolve_path=True))
199 ), type=click.Path(exists=True, resolve_path=True))
200 inputs['startDate'] = click.prompt(
200 inputs['startDate'] = click.prompt(
201 'Start date', default='1970/01/01', type=str)
201 'Start date', default='1970/01/01', type=str)
202 inputs['endDate'] = click.prompt(
202 inputs['endDate'] = click.prompt(
203 'End date', default='2018/12/31', type=str)
203 'End date', default='2018/12/31', type=str)
204 inputs['startHour'] = click.prompt(
204 inputs['startHour'] = click.prompt(
205 'Start hour', default='00:00:00', type=str)
205 'Start hour', default='00:00:00', type=str)
206 inputs['endHour'] = click.prompt('End hour', default='23:59:59', type=str)
206 inputs['endHour'] = click.prompt('End hour', default='23:59:59', type=str)
207 inputs['figpath'] = inputs['path'] + '/figs'
207 inputs['figpath'] = inputs['path'] + '/figs'
208 return inputs
208 return inputs
209
209
210
210
211 def generate():
211 def generate():
212 inputs = basicInputs()
212 inputs = basicInputs()
213
213
214 if inputs['multiprocess'] == 1:
214 if inputs['multiprocess'] == 1:
215 current = templates.voltage.format(**inputs)
215 current = templates.voltage.format(**inputs)
216 elif inputs['multiprocess'] == 2:
216 elif inputs['multiprocess'] == 2:
217 current = templates.spectra.format(**inputs)
217 current = templates.spectra.format(**inputs)
218 elif inputs['multiprocess'] == 3:
218 elif inputs['multiprocess'] == 3:
219 current = templates.voltagespectra.format(**inputs)
219 current = templates.voltagespectra.format(**inputs)
220 scriptname = '{}_{}.py'.format(PREFIX, inputs['name'])
220 scriptname = '{}_{}.py'.format(PREFIX, inputs['name'])
221 script = open(scriptname, 'w')
221 script = open(scriptname, 'w')
222 try:
222 try:
223 script.write(current)
223 script.write(current)
224 log.success('Script {} generated'.format(scriptname))
224 log.success('Script {} generated'.format(scriptname))
225 except Exception as e:
225 except Exception as e:
226 log.error('I cannot create the file. Do you have writing permissions?')
226 log.error('I cannot create the file. Do you have writing permissions?')
227
227
228
228
229 def test():
229 def test():
230 log.warning('testing')
230 log.warning('testing')
231
231
232
232
233 def runFromXML(filename):
233 def runFromXML(filename):
234 controller = Project()
234 controller = Project()
235 if not controller.readXml(filename):
235 if not controller.readXml(filename):
236 return
236 return
237 controller.start()
237 controller.start()
238 return
238 return
@@ -1,656 +1,656
1 # Copyright (c) 2012-2020 Jicamarca Radio Observatory
1 # Copyright (c) 2012-2020 Jicamarca Radio Observatory
2 # All rights reserved.
2 # All rights reserved.
3 #
3 #
4 # Distributed under the terms of the BSD 3-clause license.
4 # Distributed under the terms of the BSD 3-clause license.
5 """API to create signal chain projects
5 """API to create signal chain projects
6
6
7 The API is provide through class: Project
7 The API is provide through class: Project
8 """
8 """
9
9
10 import re
10 import re
11 import sys
11 import sys
12 import ast
12 import ast
13 import datetime
13 import datetime
14 import traceback
14 import traceback
15 import time
15 import time
16 from multiprocessing import Process, Queue
16 from multiprocessing import Process, Queue
17 from threading import Thread
17 from threading import Thread
18 from xml.etree.ElementTree import ElementTree, Element, SubElement
18 from xml.etree.ElementTree import ElementTree, Element, SubElement
19
19
20 from schainpy.admin import Alarm, SchainWarning
20 from schainpy.admin import Alarm, SchainWarning
21 from schainpy.model import *
21 from schainpy.model import *
22 from schainpy.utils import log
22 from schainpy.utils import log
23
23
24
24
25 class ConfBase():
25 class ConfBase():
26
26
27 def __init__(self):
27 def __init__(self):
28
28
29 self.id = '0'
29 self.id = '0'
30 self.name = None
30 self.name = None
31 self.priority = None
31 self.priority = None
32 self.parameters = {}
32 self.parameters = {}
33 self.object = None
33 self.object = None
34 self.operations = []
34 self.operations = []
35
35
36 def getId(self):
36 def getId(self):
37
37
38 return self.id
38 return self.id
39
39
40 def getNewId(self):
40 def getNewId(self):
41
41
42 return int(self.id) * 10 + len(self.operations) + 1
42 return int(self.id) * 10 + len(self.operations) + 1
43
43
44 def updateId(self, new_id):
44 def updateId(self, new_id):
45
45
46 self.id = str(new_id)
46 self.id = str(new_id)
47
47
48 n = 1
48 n = 1
49 for conf in self.operations:
49 for conf in self.operations:
50 conf_id = str(int(new_id) * 10 + n)
50 conf_id = str(int(new_id) * 10 + n)
51 conf.updateId(conf_id)
51 conf.updateId(conf_id)
52 n += 1
52 n += 1
53
53
54 def getKwargs(self):
54 def getKwargs(self):
55
55
56 params = {}
56 params = {}
57
57
58 for key, value in self.parameters.items():
58 for key, value in self.parameters.items():
59 if value not in (None, '', ' '):
59 if value not in (None, '', ' '):
60 params[key] = value
60 params[key] = value
61
61
62 return params
62 return params
63
63
64 def update(self, **kwargs):
64 def update(self, **kwargs):
65
65
66 for key, value in kwargs.items():
66 for key, value in kwargs.items():
67 self.addParameter(name=key, value=value)
67 self.addParameter(name=key, value=value)
68
68
69 def addParameter(self, name, value, format=None):
69 def addParameter(self, name, value, format=None):
70 '''
70 '''
71 '''
71 '''
72
72
73 if isinstance(value, str) and re.search(r'(\d+/\d+/\d+)', value):
73 if isinstance(value, str) and re.search(r'(\d+/\d+/\d+)', value):
74 self.parameters[name] = datetime.date(*[int(x) for x in value.split('/')])
74 self.parameters[name] = datetime.date(*[int(x) for x in value.split('/')])
75 elif isinstance(value, str) and re.search(r'(\d+:\d+:\d+)', value):
75 elif isinstance(value, str) and re.search(r'(\d+:\d+:\d+)', value):
76 self.parameters[name] = datetime.time(*[int(x) for x in value.split(':')])
76 self.parameters[name] = datetime.time(*[int(x) for x in value.split(':')])
77 else:
77 else:
78 try:
78 try:
79 self.parameters[name] = ast.literal_eval(value)
79 self.parameters[name] = ast.literal_eval(value)
80 except:
80 except:
81 if isinstance(value, str) and ',' in value:
81 if isinstance(value, str) and ',' in value:
82 self.parameters[name] = value.split(',')
82 self.parameters[name] = value.split(',')
83 else:
83 else:
84 self.parameters[name] = value
84 self.parameters[name] = value
85
85
86 def getParameters(self):
86 def getParameters(self):
87
87
88 params = {}
88 params = {}
89 for key, value in self.parameters.items():
89 for key, value in self.parameters.items():
90 s = type(value).__name__
90 s = type(value).__name__
91 if s == 'date':
91 if s == 'date':
92 params[key] = value.strftime('%Y/%m/%d')
92 params[key] = value.strftime('%Y/%m/%d')
93 elif s == 'time':
93 elif s == 'time':
94 params[key] = value.strftime('%H:%M:%S')
94 params[key] = value.strftime('%H:%M:%S')
95 else:
95 else:
96 params[key] = str(value)
96 params[key] = str(value)
97
97
98 return params
98 return params
99
99
100 def makeXml(self, element):
100 def makeXml(self, element):
101
101
102 xml = SubElement(element, self.ELEMENTNAME)
102 xml = SubElement(element, self.ELEMENTNAME)
103 for label in self.xml_labels:
103 for label in self.xml_labels:
104 xml.set(label, str(getattr(self, label)))
104 xml.set(label, str(getattr(self, label)))
105
105
106 for key, value in self.getParameters().items():
106 for key, value in self.getParameters().items():
107 xml_param = SubElement(xml, 'Parameter')
107 xml_param = SubElement(xml, 'Parameter')
108 xml_param.set('name', key)
108 xml_param.set('name', key)
109 xml_param.set('value', value)
109 xml_param.set('value', value)
110
110
111 for conf in self.operations:
111 for conf in self.operations:
112 conf.makeXml(xml)
112 conf.makeXml(xml)
113
113
114 def __str__(self):
114 def __str__(self):
115
115
116 if self.ELEMENTNAME == 'Operation':
116 if self.ELEMENTNAME == 'Operation':
117 s = ' {}[id={}]\n'.format(self.name, self.id)
117 s = ' {}[id={}]\n'.format(self.name, self.id)
118 else:
118 else:
119 s = '{}[id={}, inputId={}]\n'.format(self.name, self.id, self.inputId)
119 s = '{}[id={}, inputId={}]\n'.format(self.name, self.id, self.inputId)
120
120
121 for key, value in self.parameters.items():
121 for key, value in self.parameters.items():
122 if self.ELEMENTNAME == 'Operation':
122 if self.ELEMENTNAME == 'Operation':
123 s += ' {}: {}\n'.format(key, value)
123 s += ' {}: {}\n'.format(key, value)
124 else:
124 else:
125 s += ' {}: {}\n'.format(key, value)
125 s += ' {}: {}\n'.format(key, value)
126
126
127 for conf in self.operations:
127 for conf in self.operations:
128 s += str(conf)
128 s += str(conf)
129
129
130 return s
130 return s
131
131
132 class OperationConf(ConfBase):
132 class OperationConf(ConfBase):
133
133
134 ELEMENTNAME = 'Operation'
134 ELEMENTNAME = 'Operation'
135 xml_labels = ['id', 'name']
135 xml_labels = ['id', 'name']
136
136
137 def setup(self, id, name, priority, project_id, err_queue):
137 def setup(self, id, name, priority, project_id, err_queue):
138
138
139 self.id = str(id)
139 self.id = str(id)
140 self.project_id = project_id
140 self.project_id = project_id
141 self.name = name
141 self.name = name
142 self.type = 'other'
142 self.type = 'other'
143 self.err_queue = err_queue
143 self.err_queue = err_queue
144
144
145 def readXml(self, element, project_id, err_queue):
145 def readXml(self, element, project_id, err_queue):
146
146
147 self.id = element.get('id')
147 self.id = element.get('id')
148 self.name = element.get('name')
148 self.name = element.get('name')
149 self.type = 'other'
149 self.type = 'other'
150 self.project_id = str(project_id)
150 self.project_id = str(project_id)
151 self.err_queue = err_queue
151 self.err_queue = err_queue
152
152
153 for elm in element.iter('Parameter'):
153 for elm in element.iter('Parameter'):
154 self.addParameter(elm.get('name'), elm.get('value'))
154 self.addParameter(elm.get('name'), elm.get('value'))
155
155
156 def createObject(self):
156 def createObject(self):
157
157
158 className = eval(self.name)
158 className = eval(self.name)
159
159
160 if 'Plot' in self.name or 'Writer' in self.name or 'Send' in self.name or 'print' in self.name:
160 if 'Plot' in self.name or 'Writer' in self.name or 'Send' in self.name or 'print' in self.name:
161 kwargs = self.getKwargs()
161 kwargs = self.getKwargs()
162 opObj = className(self.id, self.id, self.project_id, self.err_queue, **kwargs)
162 opObj = className(self.id, self.id, self.project_id, self.err_queue, **kwargs)
163 opObj.start()
163 opObj.start()
164 self.type = 'external'
164 self.type = 'external'
165 else:
165 else:
166 opObj = className()
166 opObj = className()
167
167
168 self.object = opObj
168 self.object = opObj
169 return opObj
169 return opObj
170
170
171 class ProcUnitConf(ConfBase):
171 class ProcUnitConf(ConfBase):
172
172
173 ELEMENTNAME = 'ProcUnit'
173 ELEMENTNAME = 'ProcUnit'
174 xml_labels = ['id', 'inputId', 'name']
174 xml_labels = ['id', 'inputId', 'name']
175
175
176 def setup(self, project_id, id, name, datatype, inputId, err_queue):
176 def setup(self, project_id, id, name, datatype, inputId, err_queue):
177 '''
177 '''
178 '''
178 '''
179
179
180 if datatype == None and name == None:
180 if datatype == None and name == None:
181 raise ValueError('datatype or name should be defined')
181 raise ValueError('datatype or name should be defined')
182
182
183 if name == None:
183 if name == None:
184 if 'Proc' in datatype:
184 if 'Proc' in datatype:
185 name = datatype
185 name = datatype
186 else:
186 else:
187 name = '%sProc' % (datatype)
187 name = '%sProc' % (datatype)
188
188
189 if datatype == None:
189 if datatype == None:
190 datatype = name.replace('Proc', '')
190 datatype = name.replace('Proc', '')
191
191
192 self.id = str(id)
192 self.id = str(id)
193 self.project_id = project_id
193 self.project_id = project_id
194 self.name = name
194 self.name = name
195 self.datatype = datatype
195 self.datatype = datatype
196 self.inputId = inputId
196 self.inputId = inputId
197 self.err_queue = err_queue
197 self.err_queue = err_queue
198 self.operations = []
198 self.operations = []
199 self.parameters = {}
199 self.parameters = {}
200
200
201 def removeOperation(self, id):
201 def removeOperation(self, id):
202
202
203 i = [1 if x.id==id else 0 for x in self.operations]
203 i = [1 if x.id==id else 0 for x in self.operations]
204 self.operations.pop(i.index(1))
204 self.operations.pop(i.index(1))
205
205
206 def getOperation(self, id):
206 def getOperation(self, id):
207
207
208 for conf in self.operations:
208 for conf in self.operations:
209 if conf.id == id:
209 if conf.id == id:
210 return conf
210 return conf
211
211
212 def addOperation(self, name, optype='self'):
212 def addOperation(self, name, optype='self'):
213 '''
213 '''
214 '''
214 '''
215
215
216 id = self.getNewId()
216 id = self.getNewId()
217 conf = OperationConf()
217 conf = OperationConf()
218 conf.setup(id, name=name, priority='0', project_id=self.project_id, err_queue=self.err_queue)
218 conf.setup(id, name=name, priority='0', project_id=self.project_id, err_queue=self.err_queue)
219 self.operations.append(conf)
219 self.operations.append(conf)
220
220
221 return conf
221 return conf
222
222
223 def readXml(self, element, project_id, err_queue):
223 def readXml(self, element, project_id, err_queue):
224
224
225 self.id = element.get('id')
225 self.id = element.get('id')
226 self.name = element.get('name')
226 self.name = element.get('name')
227 self.inputId = None if element.get('inputId') == 'None' else element.get('inputId')
227 self.inputId = None if element.get('inputId') == 'None' else element.get('inputId')
228 self.datatype = element.get('datatype', self.name.replace(self.ELEMENTNAME.replace('Unit', ''), ''))
228 self.datatype = element.get('datatype', self.name.replace(self.ELEMENTNAME.replace('Unit', ''), ''))
229 self.project_id = str(project_id)
229 self.project_id = str(project_id)
230 self.err_queue = err_queue
230 self.err_queue = err_queue
231 self.operations = []
231 self.operations = []
232 self.parameters = {}
232 self.parameters = {}
233
233
234 for elm in element:
234 for elm in element:
235 if elm.tag == 'Parameter':
235 if elm.tag == 'Parameter':
236 self.addParameter(elm.get('name'), elm.get('value'))
236 self.addParameter(elm.get('name'), elm.get('value'))
237 elif elm.tag == 'Operation':
237 elif elm.tag == 'Operation':
238 conf = OperationConf()
238 conf = OperationConf()
239 conf.readXml(elm, project_id, err_queue)
239 conf.readXml(elm, project_id, err_queue)
240 self.operations.append(conf)
240 self.operations.append(conf)
241
241
242 def createObjects(self):
242 def createObjects(self):
243 '''
243 '''
244 Instancia de unidades de procesamiento.
244 Instancia de unidades de procesamiento.
245 '''
245 '''
246
246
247 className = eval(self.name)
247 className = eval(self.name)
248 kwargs = self.getKwargs()
248 kwargs = self.getKwargs()
249 procUnitObj = className()
249 procUnitObj = className()
250 procUnitObj.name = self.name
250 procUnitObj.name = self.name
251 log.success('creating process...', self.name)
251 log.success('creating process...', self.name)
252
252
253 for conf in self.operations:
253 for conf in self.operations:
254
254
255 opObj = conf.createObject()
255 opObj = conf.createObject()
256
256
257 log.success('adding operation: {}, type:{}'.format(
257 log.success('adding operation: {}, type:{}'.format(
258 conf.name,
258 conf.name,
259 conf.type), self.name)
259 conf.type), self.name)
260
260
261 procUnitObj.addOperation(conf, opObj)
261 procUnitObj.addOperation(conf, opObj)
262
262
263 self.object = procUnitObj
263 self.object = procUnitObj
264
264
265 def run(self):
265 def run(self):
266 '''
266 '''
267 '''
267 '''
268
268
269 return self.object.call(**self.getKwargs())
269 return self.object.call(**self.getKwargs())
270
270
271
271
272 class ReadUnitConf(ProcUnitConf):
272 class ReadUnitConf(ProcUnitConf):
273
273
274 ELEMENTNAME = 'ReadUnit'
274 ELEMENTNAME = 'ReadUnit'
275
275
276 def __init__(self):
276 def __init__(self):
277
277
278 self.id = None
278 self.id = None
279 self.datatype = None
279 self.datatype = None
280 self.name = None
280 self.name = None
281 self.inputId = None
281 self.inputId = None
282 self.operations = []
282 self.operations = []
283 self.parameters = {}
283 self.parameters = {}
284
284
285 def setup(self, project_id, id, name, datatype, err_queue, path='', startDate='', endDate='',
285 def setup(self, project_id, id, name, datatype, err_queue, path='', startDate='', endDate='',
286 startTime='', endTime='', server=None, **kwargs):
286 startTime='', endTime='', server=None, **kwargs):
287
287
288 if datatype == None and name == None:
288 if datatype == None and name == None:
289 raise ValueError('datatype or name should be defined')
289 raise ValueError('datatype or name should be defined')
290 if name == None:
290 if name == None:
291 if 'Reader' in datatype:
291 if 'Reader' in datatype:
292 name = datatype
292 name = datatype
293 datatype = name.replace('Reader','')
293 datatype = name.replace('Reader','')
294 else:
294 else:
295 name = '{}Reader'.format(datatype)
295 name = '{}Reader'.format(datatype)
296 if datatype == None:
296 if datatype == None:
297 if 'Reader' in name:
297 if 'Reader' in name:
298 datatype = name.replace('Reader','')
298 datatype = name.replace('Reader','')
299 else:
299 else:
300 datatype = name
300 datatype = name
301 name = '{}Reader'.format(name)
301 name = '{}Reader'.format(name)
302
302
303 self.id = id
303 self.id = id
304 self.project_id = project_id
304 self.project_id = project_id
305 self.name = name
305 self.name = name
306 self.datatype = datatype
306 self.datatype = datatype
307 self.err_queue = err_queue
307 self.err_queue = err_queue
308
308
309 self.addParameter(name='path', value=path)
309 self.addParameter(name='path', value=path)
310 self.addParameter(name='startDate', value=startDate)
310 self.addParameter(name='startDate', value=startDate)
311 self.addParameter(name='endDate', value=endDate)
311 self.addParameter(name='endDate', value=endDate)
312 self.addParameter(name='startTime', value=startTime)
312 self.addParameter(name='startTime', value=startTime)
313 self.addParameter(name='endTime', value=endTime)
313 self.addParameter(name='endTime', value=endTime)
314
314
315 for key, value in kwargs.items():
315 for key, value in kwargs.items():
316 self.addParameter(name=key, value=value)
316 self.addParameter(name=key, value=value)
317
317
318
318
319 class Project(Process):
319 class Project(Process):
320 """API to create signal chain projects"""
320 """API to create signal chain projects"""
321
321
322 ELEMENTNAME = 'Project'
322 ELEMENTNAME = 'Project'
323
323
324 def __init__(self):
324 def __init__(self, name=''):
325
325
326 Process.__init__(self, name='')
326 Process.__init__(self)
327 self.id = '1'
327 self.id = '1'
328 if name:
328 if name:
329 self.name = '{} ({})'.format(Process.__name__, name)
329 self.name = '{} ({})'.format(Process.__name__, name)
330 self.filename = None
330 self.filename = None
331 self.description = None
331 self.description = None
332 self.email = None
332 self.email = None
333 self.alarm = []
333 self.alarm = []
334 self.configurations = {}
334 self.configurations = {}
335 # self.err_queue = Queue()
335 # self.err_queue = Queue()
336 self.err_queue = None
336 self.err_queue = None
337 self.started = False
337 self.started = False
338
338
339 def getNewId(self):
339 def getNewId(self):
340
340
341 idList = list(self.configurations.keys())
341 idList = list(self.configurations.keys())
342 id = int(self.id) * 10
342 id = int(self.id) * 10
343
343
344 while True:
344 while True:
345 id += 1
345 id += 1
346
346
347 if str(id) in idList:
347 if str(id) in idList:
348 continue
348 continue
349
349
350 break
350 break
351
351
352 return str(id)
352 return str(id)
353
353
354 def updateId(self, new_id):
354 def updateId(self, new_id):
355
355
356 self.id = str(new_id)
356 self.id = str(new_id)
357
357
358 keyList = list(self.configurations.keys())
358 keyList = list(self.configurations.keys())
359 keyList.sort()
359 keyList.sort()
360
360
361 n = 1
361 n = 1
362 new_confs = {}
362 new_confs = {}
363
363
364 for procKey in keyList:
364 for procKey in keyList:
365
365
366 conf = self.configurations[procKey]
366 conf = self.configurations[procKey]
367 idProcUnit = str(int(self.id) * 10 + n)
367 idProcUnit = str(int(self.id) * 10 + n)
368 conf.updateId(idProcUnit)
368 conf.updateId(idProcUnit)
369 new_confs[idProcUnit] = conf
369 new_confs[idProcUnit] = conf
370 n += 1
370 n += 1
371
371
372 self.configurations = new_confs
372 self.configurations = new_confs
373
373
374 def setup(self, id=1, name='', description='', email=None, alarm=[]):
374 def setup(self, id=1, name='', description='', email=None, alarm=[]):
375
375
376 self.id = str(id)
376 self.id = str(id)
377 self.description = description
377 self.description = description
378 self.email = email
378 self.email = email
379 self.alarm = alarm
379 self.alarm = alarm
380 if name:
380 if name:
381 self.name = '{} ({})'.format(Process.__name__, name)
381 self.name = '{} ({})'.format(Process.__name__, name)
382
382
383 def update(self, **kwargs):
383 def update(self, **kwargs):
384
384
385 for key, value in kwargs.items():
385 for key, value in kwargs.items():
386 setattr(self, key, value)
386 setattr(self, key, value)
387
387
388 def clone(self):
388 def clone(self):
389
389
390 p = Project()
390 p = Project()
391 p.id = self.id
391 p.id = self.id
392 p.name = self.name
392 p.name = self.name
393 p.description = self.description
393 p.description = self.description
394 p.configurations = self.configurations.copy()
394 p.configurations = self.configurations.copy()
395
395
396 return p
396 return p
397
397
398 def addReadUnit(self, id=None, datatype=None, name=None, **kwargs):
398 def addReadUnit(self, id=None, datatype=None, name=None, **kwargs):
399
399
400 '''
400 '''
401 '''
401 '''
402
402
403 if id is None:
403 if id is None:
404 idReadUnit = self.getNewId()
404 idReadUnit = self.getNewId()
405 else:
405 else:
406 idReadUnit = str(id)
406 idReadUnit = str(id)
407
407
408 conf = ReadUnitConf()
408 conf = ReadUnitConf()
409 conf.setup(self.id, idReadUnit, name, datatype, self.err_queue, **kwargs)
409 conf.setup(self.id, idReadUnit, name, datatype, self.err_queue, **kwargs)
410 self.configurations[conf.id] = conf
410 self.configurations[conf.id] = conf
411
411
412 return conf
412 return conf
413
413
414 def addProcUnit(self, id=None, inputId='0', datatype=None, name=None):
414 def addProcUnit(self, id=None, inputId='0', datatype=None, name=None):
415
415
416 '''
416 '''
417 '''
417 '''
418
418
419 if id is None:
419 if id is None:
420 idProcUnit = self.getNewId()
420 idProcUnit = self.getNewId()
421 else:
421 else:
422 idProcUnit = id
422 idProcUnit = id
423
423
424 conf = ProcUnitConf()
424 conf = ProcUnitConf()
425 conf.setup(self.id, idProcUnit, name, datatype, inputId, self.err_queue)
425 conf.setup(self.id, idProcUnit, name, datatype, inputId, self.err_queue)
426 self.configurations[conf.id] = conf
426 self.configurations[conf.id] = conf
427
427
428 return conf
428 return conf
429
429
430 def removeProcUnit(self, id):
430 def removeProcUnit(self, id):
431
431
432 if id in self.configurations:
432 if id in self.configurations:
433 self.configurations.pop(id)
433 self.configurations.pop(id)
434
434
435 def getReadUnit(self):
435 def getReadUnit(self):
436
436
437 for obj in list(self.configurations.values()):
437 for obj in list(self.configurations.values()):
438 if obj.ELEMENTNAME == 'ReadUnit':
438 if obj.ELEMENTNAME == 'ReadUnit':
439 return obj
439 return obj
440
440
441 return None
441 return None
442
442
443 def getProcUnit(self, id):
443 def getProcUnit(self, id):
444
444
445 return self.configurations[id]
445 return self.configurations[id]
446
446
447 def getUnits(self):
447 def getUnits(self):
448
448
449 keys = list(self.configurations)
449 keys = list(self.configurations)
450 keys.sort()
450 keys.sort()
451
451
452 for key in keys:
452 for key in keys:
453 yield self.configurations[key]
453 yield self.configurations[key]
454
454
455 def updateUnit(self, id, **kwargs):
455 def updateUnit(self, id, **kwargs):
456
456
457 conf = self.configurations[id].update(**kwargs)
457 conf = self.configurations[id].update(**kwargs)
458
458
459 def makeXml(self):
459 def makeXml(self):
460
460
461 xml = Element('Project')
461 xml = Element('Project')
462 xml.set('id', str(self.id))
462 xml.set('id', str(self.id))
463 xml.set('name', self.name)
463 xml.set('name', self.name)
464 xml.set('description', self.description)
464 xml.set('description', self.description)
465
465
466 for conf in self.configurations.values():
466 for conf in self.configurations.values():
467 conf.makeXml(xml)
467 conf.makeXml(xml)
468
468
469 self.xml = xml
469 self.xml = xml
470
470
471 def writeXml(self, filename=None):
471 def writeXml(self, filename=None):
472
472
473 if filename == None:
473 if filename == None:
474 if self.filename:
474 if self.filename:
475 filename = self.filename
475 filename = self.filename
476 else:
476 else:
477 filename = 'schain.xml'
477 filename = 'schain.xml'
478
478
479 if not filename:
479 if not filename:
480 print('filename has not been defined. Use setFilename(filename) for do it.')
480 print('filename has not been defined. Use setFilename(filename) for do it.')
481 return 0
481 return 0
482
482
483 abs_file = os.path.abspath(filename)
483 abs_file = os.path.abspath(filename)
484
484
485 if not os.access(os.path.dirname(abs_file), os.W_OK):
485 if not os.access(os.path.dirname(abs_file), os.W_OK):
486 print('No write permission on %s' % os.path.dirname(abs_file))
486 print('No write permission on %s' % os.path.dirname(abs_file))
487 return 0
487 return 0
488
488
489 if os.path.isfile(abs_file) and not(os.access(abs_file, os.W_OK)):
489 if os.path.isfile(abs_file) and not(os.access(abs_file, os.W_OK)):
490 print('File %s already exists and it could not be overwriten' % abs_file)
490 print('File %s already exists and it could not be overwriten' % abs_file)
491 return 0
491 return 0
492
492
493 self.makeXml()
493 self.makeXml()
494
494
495 ElementTree(self.xml).write(abs_file, method='xml')
495 ElementTree(self.xml).write(abs_file, method='xml')
496
496
497 self.filename = abs_file
497 self.filename = abs_file
498
498
499 return 1
499 return 1
500
500
501 def readXml(self, filename):
501 def readXml(self, filename):
502
502
503 abs_file = os.path.abspath(filename)
503 abs_file = os.path.abspath(filename)
504
504
505 self.configurations = {}
505 self.configurations = {}
506
506
507 try:
507 try:
508 self.xml = ElementTree().parse(abs_file)
508 self.xml = ElementTree().parse(abs_file)
509 except:
509 except:
510 log.error('Error reading %s, verify file format' % filename)
510 log.error('Error reading %s, verify file format' % filename)
511 return 0
511 return 0
512
512
513 self.id = self.xml.get('id')
513 self.id = self.xml.get('id')
514 self.name = self.xml.get('name')
514 self.name = self.xml.get('name')
515 self.description = self.xml.get('description')
515 self.description = self.xml.get('description')
516
516
517 for element in self.xml:
517 for element in self.xml:
518 if element.tag == 'ReadUnit':
518 if element.tag == 'ReadUnit':
519 conf = ReadUnitConf()
519 conf = ReadUnitConf()
520 conf.readXml(element, self.id, self.err_queue)
520 conf.readXml(element, self.id, self.err_queue)
521 self.configurations[conf.id] = conf
521 self.configurations[conf.id] = conf
522 elif element.tag == 'ProcUnit':
522 elif element.tag == 'ProcUnit':
523 conf = ProcUnitConf()
523 conf = ProcUnitConf()
524 input_proc = self.configurations[element.get('inputId')]
524 input_proc = self.configurations[element.get('inputId')]
525 conf.readXml(element, self.id, self.err_queue)
525 conf.readXml(element, self.id, self.err_queue)
526 self.configurations[conf.id] = conf
526 self.configurations[conf.id] = conf
527
527
528 self.filename = abs_file
528 self.filename = abs_file
529
529
530 return 1
530 return 1
531
531
532 def __str__(self):
532 def __str__(self):
533
533
534 text = '\nProject[id=%s, name=%s, description=%s]\n\n' % (
534 text = '\nProject[id=%s, name=%s, description=%s]\n\n' % (
535 self.id,
535 self.id,
536 self.name,
536 self.name,
537 self.description,
537 self.description,
538 )
538 )
539
539
540 for conf in self.configurations.values():
540 for conf in self.configurations.values():
541 text += '{}'.format(conf)
541 text += '{}'.format(conf)
542
542
543 return text
543 return text
544
544
545 def createObjects(self):
545 def createObjects(self):
546
546
547 keys = list(self.configurations.keys())
547 keys = list(self.configurations.keys())
548 keys.sort()
548 keys.sort()
549 for key in keys:
549 for key in keys:
550 conf = self.configurations[key]
550 conf = self.configurations[key]
551 conf.createObjects()
551 conf.createObjects()
552 if conf.inputId is not None:
552 if conf.inputId is not None:
553 conf.object.setInput(self.configurations[conf.inputId].object)
553 conf.object.setInput(self.configurations[conf.inputId].object)
554
554
555 def monitor(self):
555 def monitor(self):
556
556
557 t = Thread(target=self._monitor, args=(self.err_queue, self.ctx))
557 t = Thread(target=self._monitor, args=(self.err_queue, self.ctx))
558 t.start()
558 t.start()
559
559
560 def _monitor(self, queue, ctx):
560 def _monitor(self, queue, ctx):
561
561
562 import socket
562 import socket
563
563
564 procs = 0
564 procs = 0
565 err_msg = ''
565 err_msg = ''
566
566
567 while True:
567 while True:
568 msg = queue.get()
568 msg = queue.get()
569 if '#_start_#' in msg:
569 if '#_start_#' in msg:
570 procs += 1
570 procs += 1
571 elif '#_end_#' in msg:
571 elif '#_end_#' in msg:
572 procs -=1
572 procs -=1
573 else:
573 else:
574 err_msg = msg
574 err_msg = msg
575
575
576 if procs == 0 or 'Traceback' in err_msg:
576 if procs == 0 or 'Traceback' in err_msg:
577 break
577 break
578 time.sleep(0.1)
578 time.sleep(0.1)
579
579
580 if '|' in err_msg:
580 if '|' in err_msg:
581 name, err = err_msg.split('|')
581 name, err = err_msg.split('|')
582 if 'SchainWarning' in err:
582 if 'SchainWarning' in err:
583 log.warning(err.split('SchainWarning:')[-1].split('\n')[0].strip(), name)
583 log.warning(err.split('SchainWarning:')[-1].split('\n')[0].strip(), name)
584 elif 'SchainError' in err:
584 elif 'SchainError' in err:
585 log.error(err.split('SchainError:')[-1].split('\n')[0].strip(), name)
585 log.error(err.split('SchainError:')[-1].split('\n')[0].strip(), name)
586 else:
586 else:
587 log.error(err, name)
587 log.error(err, name)
588 else:
588 else:
589 name, err = self.name, err_msg
589 name, err = self.name, err_msg
590
590
591 time.sleep(1)
591 time.sleep(1)
592
592
593 ctx.term()
593 ctx.term()
594
594
595 message = ''.join(err)
595 message = ''.join(err)
596
596
597 if err_msg:
597 if err_msg:
598 subject = 'SChain v%s: Error running %s\n' % (
598 subject = 'SChain v%s: Error running %s\n' % (
599 schainpy.__version__, self.name)
599 schainpy.__version__, self.name)
600
600
601 subtitle = 'Hostname: %s\n' % socket.gethostbyname(
601 subtitle = 'Hostname: %s\n' % socket.gethostbyname(
602 socket.gethostname())
602 socket.gethostname())
603 subtitle += 'Working directory: %s\n' % os.path.abspath('./')
603 subtitle += 'Working directory: %s\n' % os.path.abspath('./')
604 subtitle += 'Configuration file: %s\n' % self.filename
604 subtitle += 'Configuration file: %s\n' % self.filename
605 subtitle += 'Time: %s\n' % str(datetime.datetime.now())
605 subtitle += 'Time: %s\n' % str(datetime.datetime.now())
606
606
607 readUnitConfObj = self.getReadUnit()
607 readUnitConfObj = self.getReadUnit()
608 if readUnitConfObj:
608 if readUnitConfObj:
609 subtitle += '\nInput parameters:\n'
609 subtitle += '\nInput parameters:\n'
610 subtitle += '[Data path = %s]\n' % readUnitConfObj.parameters['path']
610 subtitle += '[Data path = %s]\n' % readUnitConfObj.parameters['path']
611 subtitle += '[Start date = %s]\n' % readUnitConfObj.parameters['startDate']
611 subtitle += '[Start date = %s]\n' % readUnitConfObj.parameters['startDate']
612 subtitle += '[End date = %s]\n' % readUnitConfObj.parameters['endDate']
612 subtitle += '[End date = %s]\n' % readUnitConfObj.parameters['endDate']
613 subtitle += '[Start time = %s]\n' % readUnitConfObj.parameters['startTime']
613 subtitle += '[Start time = %s]\n' % readUnitConfObj.parameters['startTime']
614 subtitle += '[End time = %s]\n' % readUnitConfObj.parameters['endTime']
614 subtitle += '[End time = %s]\n' % readUnitConfObj.parameters['endTime']
615
615
616 a = Alarm(
616 a = Alarm(
617 modes=self.alarm,
617 modes=self.alarm,
618 email=self.email,
618 email=self.email,
619 message=message,
619 message=message,
620 subject=subject,
620 subject=subject,
621 subtitle=subtitle,
621 subtitle=subtitle,
622 filename=self.filename
622 filename=self.filename
623 )
623 )
624
624
625 a.start()
625 a.start()
626
626
627 def setFilename(self, filename):
627 def setFilename(self, filename):
628
628
629 self.filename = filename
629 self.filename = filename
630
630
631 def runProcs(self):
631 def runProcs(self):
632
632
633 err = False
633 err = False
634 n = len(self.configurations)
634 n = len(self.configurations)
635
635
636 while not err:
636 while not err:
637 for conf in self.getUnits():
637 for conf in self.getUnits():
638 ok = conf.run()
638 ok = conf.run()
639 if ok is 'Error':
639 if ok is 'Error':
640 n -= 1
640 n -= 1
641 continue
641 continue
642 elif not ok:
642 elif not ok:
643 break
643 break
644 if n == 0:
644 if n == 0:
645 err = True
645 err = True
646
646
647 def run(self):
647 def run(self):
648
648
649 log.success('\nStarting Project {} [id={}]'.format(self.name, self.id), tag='')
649 log.success('\nStarting Project {} [id={}]'.format(self.name, self.id), tag='')
650 self.started = True
650 self.started = True
651 self.start_time = time.time()
651 self.start_time = time.time()
652 self.createObjects()
652 self.createObjects()
653 self.runProcs()
653 self.runProcs()
654 log.success('{} Done (Time: {:4.2f}s)'.format(
654 log.success('{} Done (Time: {:4.2f}s)'.format(
655 self.name,
655 self.name,
656 time.time()-self.start_time), '')
656 time.time()-self.start_time), '')
General Comments 0
You need to be logged in to leave comments. Login now