1 | #! /usr/bin/python |
||
2 | |||
3 | """ |
||
4 | generic to task-juggler extraction script |
||
5 | |||
6 | This script queries generic, and generates a task-juggler input file in order to generate a gant-chart. |
||
0 ignored issues
–
show
|
|||
7 | """ |
||
8 | |||
9 | import logging,tempfile,subprocess,datetime,icalendar,shutil,os |
||
0 ignored issues
–
show
|
|||
10 | from collections import OrderedDict |
||
0 ignored issues
–
show
|
|||
11 | |||
12 | DEFAULT_LOGLEVEL = 'warning' |
||
13 | DEFAULT_OUTPUT = 'export.tjp' |
||
14 | |||
15 | TAB = ' ' * 4 |
||
16 | |||
17 | DEBUG = False |
||
18 | |||
19 | def is_number(s): |
||
20 | try: |
||
21 | float(s) |
||
22 | return True |
||
23 | except ValueError: |
||
24 | return False |
||
25 | |||
26 | def set_logging_level(loglevel): |
||
27 | ''' |
||
28 | Set the logging level |
||
29 | |||
30 | Args: |
||
31 | loglevel String representation of the loglevel |
||
32 | ''' |
||
33 | numeric_level = getattr(logging, loglevel.upper(), None) |
||
34 | if not isinstance(numeric_level, int): |
||
35 | raise ValueError('Invalid log level: %s' % loglevel) |
||
36 | logging.getLogger().setLevel(numeric_level) |
||
37 | |||
0 ignored issues
–
show
|
|||
38 | def to_tj3time(dt): |
||
39 | """ |
||
40 | Convert python date or datetime object to TJ3 format |
||
41 | |||
0 ignored issues
–
show
|
|||
42 | """ |
||
43 | return dt.isoformat().replace("T", "-").split(".")[0].split("+")[0] |
||
44 | |||
45 | def to_tj3interval(start, end): |
||
46 | return "%s - %s" % (to_tj3time(start), to_tj3time(end)) |
||
47 | |||
48 | TJP_NUM_ID_PREFIX = "tjp_numid_" |
||
49 | TJP_DASH_PREFIX = "__DASH__" |
||
50 | TJP_SPACE_PREFIX = "__SPACE__" |
||
51 | |||
52 | def to_identifier(key): |
||
53 | ''' |
||
54 | Convert given key to identifier, interpretable by TaskJuggler as a task-identifier |
||
55 | |||
56 | Args: |
||
57 | key (str): Key to be converted |
||
58 | |||
59 | Returns: |
||
60 | str: Valid task-identifier based on given key |
||
61 | ''' |
||
62 | if is_number(key): |
||
63 | key = TJP_NUM_ID_PREFIX+str(key) |
||
64 | key = key.replace('-', TJP_DASH_PREFIX).replace(" ", TJP_SPACE_PREFIX) |
||
65 | return key |
||
66 | |||
67 | def from_identifier(key): |
||
68 | if TJP_NUM_ID_PREFIX in key: |
||
69 | return int(key.replace(TJP_NUM_ID_PREFIX, "")) |
||
70 | return key.replace(TJP_DASH_PREFIX, "-").replace(TJP_SPACE_PREFIX, " ") |
||
71 | |||
72 | class JugglerTaskProperty(object): |
||
0 ignored issues
–
show
|
|||
73 | '''Class for a property of a Task Juggler''' |
||
74 | |||
75 | DEFAULT_NAME = 'property name' |
||
76 | DEFAULT_VALUE = 'not initialized' |
||
77 | PREFIX = '' |
||
78 | SUFFIX = '' |
||
79 | TEMPLATE = TAB + '{prop} {value}\n' |
||
80 | VALUE_TEMPLATE = '{prefix}{value}{suffix}' |
||
81 | LOG_STRING = "Default TaskProperty" |
||
82 | |||
83 | |||
84 | def __init__(self, issue=None): |
||
85 | ''' |
||
86 | Initialize task juggler property |
||
87 | |||
88 | Args: |
||
89 | issue (class): The generic issue to load from |
||
90 | value (object): Value of the property |
||
91 | ''' |
||
92 | logging.debug('Create %s', self.LOG_STRING) |
||
93 | self.name = self.DEFAULT_NAME |
||
94 | self.set_value(self.DEFAULT_VALUE) |
||
95 | self.empty = False |
||
96 | self.parent = None |
||
97 | self.top = None |
||
98 | self.load_default_properties(issue) |
||
99 | |||
100 | if issue: |
||
101 | if self.load_from_issue(issue) is False: |
||
102 | self.empty = True |
||
103 | |||
104 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
105 | pass |
||
106 | |||
107 | def load_from_issue(self, issue): |
||
108 | ''' |
||
109 | Load the object with data from a generic issue |
||
110 | |||
111 | Args: |
||
112 | issue (class): The generic issue to load from |
||
113 | ''' |
||
114 | pass |
||
0 ignored issues
–
show
|
|||
115 | |||
116 | def get_name(self): |
||
117 | ''' |
||
118 | Get name for task juggler property |
||
119 | |||
120 | Returns: |
||
121 | str: Name of the task juggler property |
||
122 | ''' |
||
123 | return self.name |
||
124 | |||
0 ignored issues
–
show
|
|||
125 | def get_id(self): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
126 | return "" |
||
127 | |||
0 ignored issues
–
show
|
|||
128 | def get_hash(self): |
||
129 | return self.get_name() + repr(self.get_value()) |
||
130 | |||
131 | def set_value(self, value): |
||
132 | ''' |
||
133 | Set value for task juggler property |
||
134 | |||
135 | Args: |
||
136 | value (object): New value of the property |
||
137 | ''' |
||
138 | self.value = value |
||
139 | |||
140 | def append_value(self, value): |
||
141 | ''' |
||
142 | Append value for task juggler property |
||
143 | |||
144 | Args: |
||
145 | value (object): Value to append to the property |
||
146 | ''' |
||
147 | self.value.append(value) |
||
148 | |||
149 | def get_value(self): |
||
150 | ''' |
||
151 | Get value for task juggler property |
||
152 | |||
153 | Returns: |
||
154 | str: Value of the task juggler property |
||
155 | ''' |
||
156 | if self.value == self.DEFAULT_VALUE: return "" |
||
0 ignored issues
–
show
|
|||
157 | return self.value |
||
158 | |||
159 | def validate(self, task, tasks): |
||
160 | ''' |
||
161 | Validate (and correct) the current task property |
||
162 | |||
163 | Args: |
||
164 | task (JugglerTask): Task to which the property belongs |
||
165 | tasks (list): List of JugglerTask's to which the current task belongs. Will be used to |
||
0 ignored issues
–
show
|
|||
166 | verify relations to other tasks. |
||
167 | ''' |
||
168 | pass |
||
0 ignored issues
–
show
|
|||
169 | |||
170 | def __str__(self): |
||
171 | ''' |
||
172 | Convert task property object to the task juggler syntax |
||
173 | |||
174 | Returns: |
||
175 | str: String representation of the task property in juggler syntax |
||
176 | ''' |
||
177 | |||
178 | if self.get_value(): |
||
0 ignored issues
–
show
|
|||
179 | # TODO: list support (like allocate multiple) (copy from taskdepends) |
||
180 | # TODO: identifier conversion support? |
||
181 | return self.TEMPLATE.format(prop=self.get_name(), |
||
182 | value=self.VALUE_TEMPLATE.format(prefix=self.PREFIX, |
||
183 | value=self.get_value(), |
||
184 | suffix=self.SUFFIX)) |
||
185 | return '' |
||
186 | |||
187 | class JugglerTaskAllocate(JugglerTaskProperty): |
||
188 | '''Class for the allocate (assignee) of a juggler task''' |
||
189 | |||
190 | DEFAULT_NAME = 'allocate' |
||
191 | DEFAULT_VALUE = 'not initialized' |
||
192 | |||
193 | def load_from_issue(self, issue = None): |
||
0 ignored issues
–
show
|
|||
194 | ''' |
||
195 | Load the object with data from a generic issue |
||
196 | |||
197 | Args: |
||
198 | issue (class): The generic issue to load from |
||
199 | ''' |
||
200 | self.set_value(self.DEFAULT_VALUE) |
||
201 | if not issue is None: |
||
202 | self.set_value(issue) # TODO: this may be list or primitive |
||
203 | |||
204 | class JugglerTaskPriority(JugglerTaskProperty): |
||
205 | '''Class for task priority''' |
||
206 | LOG_STRING = "JugglerTaskPriority" |
||
207 | DEFAULT_NAME = "priority" |
||
208 | DEFAULT_VALUE = 1000 |
||
209 | |||
0 ignored issues
–
show
|
|||
210 | def get_hash(self): |
||
211 | return self.get_name() |
||
212 | |||
213 | class JugglerTaskStart(JugglerTaskProperty): |
||
214 | LOG_STRING = "JugglerTaskStart" |
||
215 | DEFAULT_NAME = "start" |
||
216 | DEFAULT_VALUE = "" |
||
217 | |||
0 ignored issues
–
show
|
|||
218 | def set_value(self, dt): |
||
0 ignored issues
–
show
|
|||
219 | if not dt: |
||
0 ignored issues
–
show
|
|||
220 | self.value = "" |
||
221 | return |
||
222 | if not isinstance(dt, datetime.datetime): |
||
223 | raise ValueError("Task start value should be datetime object") |
||
224 | self.value = dt |
||
225 | |||
0 ignored issues
–
show
|
|||
226 | def get_value(self): |
||
227 | # TODO: fix API |
||
228 | # WARNING: get_value returns tjp value |
||
229 | if not self.value: |
||
230 | return "" |
||
231 | return to_tj3time(self.value) |
||
232 | |||
0 ignored issues
–
show
|
|||
233 | def get_hash(self): |
||
234 | return self.get_name() |
||
235 | |||
236 | class JugglerTaskEffort(JugglerTaskProperty): |
||
237 | '''Class for the effort (estimate) of a juggler task''' |
||
238 | |||
239 | #For converting the seconds (generic) to days |
||
240 | UNIT = 'h' |
||
241 | |||
242 | DEFAULT_NAME = 'effort' |
||
243 | MINIMAL_VALUE = 1 # TODO: should be project resolution, add check |
||
244 | DEFAULT_VALUE = -1 |
||
0 ignored issues
–
show
|
|||
245 | SUFFIX = UNIT |
||
246 | |||
247 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
248 | self.SUFFIX = self.UNIT |
||
249 | |||
250 | def load_from_issue(self, issue): |
||
251 | ''' |
||
252 | Load the object with data from a generic issue |
||
253 | |||
254 | Args: |
||
255 | issue (class): The generic issue to load from |
||
256 | ''' |
||
257 | if issue: self.set_value(issue) |
||
0 ignored issues
–
show
|
|||
258 | else: self.set_value(self.DEFAULT_VALUE) |
||
259 | def set_value(self, value): |
||
260 | ''' |
||
261 | Set the value for effort. Will convert whatever number to integer. |
||
262 | |||
0 ignored issues
–
show
|
|||
263 | Default class unit is 'days'. Can be overrided by setting "UNIT" global class attribute |
||
264 | ''' |
||
265 | self.value = int(value) # TODO: support for FP? |
||
266 | def get_hash(self): |
||
267 | return self.get_name() |
||
268 | |||
0 ignored issues
–
show
|
|||
269 | def decode(self): |
||
270 | return self.value # only hours supported yet |
||
271 | |||
0 ignored issues
–
show
|
|||
272 | def validate(self, task, tasks): |
||
273 | ''' |
||
274 | Validate (and correct) the current task property |
||
275 | |||
276 | Args: |
||
277 | task (JugglerTask): Task to which the property belongs |
||
278 | tasks (list): List of JugglerTask's to which the current task belongs. Will be used to |
||
0 ignored issues
–
show
|
|||
279 | verify relations to other tasks. |
||
280 | ''' |
||
281 | pass |
||
0 ignored issues
–
show
|
|||
282 | # if self.get_value() < self.MINIMAL_VALUE: |
||
283 | # logging.warning('Estimate %s%s too low for %s, assuming %s%s', self.get_value(), self.UNIT, task.key, self.MINIMAL_VALUE, self.UNIT) |
||
0 ignored issues
–
show
|
|||
284 | # self.set_value(self.MINIMAL_VALUE) |
||
285 | |||
286 | class JugglerTaskDepends(JugglerTaskProperty): |
||
287 | '''Class for the effort (estimate) of a juggler task''' |
||
288 | |||
289 | DEFAULT_NAME = 'depends' |
||
290 | DEFAULT_VALUE = [] |
||
291 | PREFIX = '!' |
||
292 | |||
293 | def set_value(self, value): |
||
294 | ''' |
||
295 | Set value for task juggler property (deep copy) |
||
296 | |||
297 | Args: |
||
298 | value (object): New value of the property |
||
299 | ''' |
||
300 | self.value = list(value) |
||
301 | |||
302 | def load_from_issue(self, issue): |
||
303 | ''' |
||
304 | Load the object with data from a generic issue |
||
305 | |||
306 | Args: |
||
307 | issue (class): The generic issue to load from |
||
308 | ''' |
||
309 | raise NotImplementedError("load_from_issue is not implemented for this depend") |
||
310 | # TODO: remove these - are for jira |
||
311 | self.set_value(self.DEFAULT_VALUE) |
||
0 ignored issues
–
show
|
|||
312 | if hasattr(issue.fields, 'issuelinks'): |
||
313 | for link in issue.fields.issuelinks: |
||
314 | if hasattr(link, 'inwardIssue') and link.type.name == 'Blocker': |
||
315 | self.append_value(to_identifier(link.inwardIssue.key)) |
||
316 | |||
317 | def validate(self, task, tasks): |
||
318 | ''' |
||
319 | Validate (and correct) the current task property |
||
320 | |||
321 | Args: |
||
322 | task (JugglerTask): Task to which the property belongs |
||
323 | tasks (list): List of JugglerTask's to which the current task belongs. Will be used to |
||
0 ignored issues
–
show
|
|||
324 | verify relations to other tasks. |
||
325 | ''' |
||
326 | # TODO: add support for nested tasks with self.parent |
||
327 | for val in list(self.get_value()): |
||
328 | if val not in [tsk.get_id() for tsk in tasks]: |
||
0 ignored issues
–
show
|
|||
329 | logging.warning('Removing link to %s for %s, as not within scope', val, task.get_id()) |
||
0 ignored issues
–
show
|
|||
330 | self.value.remove(val) |
||
0 ignored issues
–
show
|
|||
331 | |||
332 | def __str__(self): |
||
333 | ''' |
||
334 | Convert task property object to the task juggler syntax |
||
335 | |||
336 | Returns: |
||
337 | str: String representation of the task property in juggler syntax |
||
338 | ''' |
||
339 | |||
340 | if self.get_value(): |
||
341 | valstr = '' |
||
342 | for val in self.get_value(): |
||
343 | if valstr: |
||
344 | valstr += ', ' |
||
345 | valstr += self.VALUE_TEMPLATE.format(prefix=self.PREFIX, |
||
346 | value=to_identifier(val), |
||
347 | suffix=self.SUFFIX) |
||
348 | return self.TEMPLATE.format(prop=self.get_name(), |
||
349 | value=valstr) |
||
350 | return '' |
||
351 | |||
352 | # class NonEmptyObject(object): |
||
353 | # def __init__(self): |
||
354 | # self.empty = False |
||
355 | # def __bool__(self): |
||
356 | # return not self.empty |
||
357 | # __nonzero__=__bool__ |
||
358 | |||
0 ignored issues
–
show
|
|||
359 | |||
360 | class JugglerCompoundKeyword(object): |
||
0 ignored issues
–
show
|
|||
361 | |||
362 | '''Class for a general compound object in TJ syntax''' |
||
363 | |||
364 | COMMENTS_HEADER = "" |
||
365 | LOG_STRING = "DefaultKeyword" |
||
366 | DEFAULT_KEYWORD = 'unknown_keyword' |
||
367 | DEFAULT_ID = "" # id may be empty for some keywords |
||
368 | DEFAULT_SUMMARY = '' # no summary is possible everywhere |
||
369 | TEMPLATE = '''{header}\n{keyword} {id}''' |
||
370 | ENCLOSED_BLOCK = True |
||
371 | |||
372 | def __init__(self, issue=None): |
||
373 | logging.debug('Create %s', self.LOG_STRING) |
||
374 | self.empty = False |
||
375 | self.parent = None |
||
376 | self.top = None |
||
377 | self.keyword = self.DEFAULT_KEYWORD |
||
378 | self.id = self.DEFAULT_ID |
||
379 | self.summary = self.DEFAULT_SUMMARY |
||
380 | self.option2 = "" |
||
381 | self.properties = OrderedDict() |
||
382 | self.load_default_properties(issue) |
||
383 | self._post_init(issue) |
||
384 | |||
385 | if issue: |
||
386 | if self.load_from_issue(issue) is False: |
||
387 | self.empty = True |
||
388 | |||
0 ignored issues
–
show
|
|||
389 | def _post_init(self, issue): |
||
390 | pass |
||
391 | |||
0 ignored issues
–
show
|
|||
392 | def load_from_issue(self, issue): |
||
393 | ''' |
||
394 | Load the object with data from a generic issue |
||
395 | |||
396 | Args: |
||
397 | issue (class): The generic issue to load from |
||
398 | ''' |
||
399 | pass |
||
0 ignored issues
–
show
|
|||
400 | |||
0 ignored issues
–
show
|
|||
401 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
402 | pass |
||
403 | |||
0 ignored issues
–
show
|
|||
404 | def get_name(self): |
||
405 | return self.keyword |
||
406 | |||
0 ignored issues
–
show
|
|||
407 | def get_id(self): |
||
408 | return self.id |
||
409 | |||
0 ignored issues
–
show
|
|||
410 | def get_hash(self): |
||
411 | """Used to generate unique hash. |
||
0 ignored issues
–
show
|
|||
412 | |||
0 ignored issues
–
show
|
|||
413 | If set_property should replace this (only single property of this type is supported) - |
||
0 ignored issues
–
show
|
|||
414 | - the hash should only return the keyword |
||
415 | |||
0 ignored issues
–
show
|
|||
416 | By default, multiple compound properties are allowed. |
||
417 | """ |
||
418 | return self.get_name() + repr(self.get_id()) |
||
419 | |||
420 | def set_property(self, prop): |
||
421 | if prop: |
||
0 ignored issues
–
show
|
|||
422 | self.properties[prop.get_hash()] = prop |
||
423 | prop.parent = self # TODO: control un-set?, GC? |
||
424 | prop.top = self.top |
||
425 | |||
0 ignored issues
–
show
|
|||
426 | def set_id(self, id): |
||
0 ignored issues
–
show
|
|||
427 | self.id = id |
||
428 | |||
0 ignored issues
–
show
|
|||
429 | def decode(self): |
||
430 | return self.option2 |
||
431 | |||
0 ignored issues
–
show
|
|||
432 | def walk(self, cls, ls = None): |
||
0 ignored issues
–
show
|
|||
433 | if ls is None: |
||
0 ignored issues
–
show
|
|||
434 | ls = [] |
||
435 | if isinstance(self, cls): |
||
436 | ls.append(self) |
||
437 | for key, item in self.properties.items(): |
||
0 ignored issues
–
show
|
|||
438 | if isinstance(item, JugglerCompoundKeyword): |
||
439 | ls = item.walk(cls, ls) |
||
440 | if isinstance(item, cls): |
||
441 | ls.append(item) |
||
442 | return ls |
||
443 | |||
0 ignored issues
–
show
|
|||
444 | def __str__(self): |
||
445 | if self.empty: return "" |
||
0 ignored issues
–
show
|
|||
446 | out = self.TEMPLATE.format(header=self.COMMENTS_HEADER,keyword=self.keyword, id=to_identifier(self.id)) |
||
0 ignored issues
–
show
|
|||
447 | if self.summary: |
||
448 | out += ' "%s"' % self.summary.replace('\"', '\\\"') |
||
449 | if self.option2: |
||
450 | out += ' %s ' % self.option2 |
||
451 | if self.properties and self.ENCLOSED_BLOCK: out += " {\n" |
||
0 ignored issues
–
show
|
|||
452 | for prop in self.properties: |
||
453 | out += str(self.properties[prop]) |
||
454 | if self.properties and self.ENCLOSED_BLOCK: out += "\n}" |
||
0 ignored issues
–
show
|
|||
455 | return out |
||
456 | |||
457 | class JugglerSimpleProperty(JugglerCompoundKeyword): |
||
458 | """By default only one simple property is allowed.""" |
||
459 | LOG_STRING = "Default Simple Property" |
||
460 | DEFAULT_NAME = 'unknown_property' |
||
461 | DEFAULT_VALUE = '' |
||
462 | |||
0 ignored issues
–
show
|
|||
463 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
464 | self.keyword = self.DEFAULT_NAME |
||
465 | self.set_value(self.DEFAULT_VALUE) |
||
466 | |||
0 ignored issues
–
show
|
|||
467 | def load_from_issue(self, value): |
||
0 ignored issues
–
show
|
|||
468 | self.set_value(value) |
||
469 | |||
0 ignored issues
–
show
|
|||
470 | def get_name(self): |
||
471 | return self.keyword |
||
472 | |||
0 ignored issues
–
show
|
|||
473 | def get_hash(self): |
||
474 | return self.get_name() |
||
475 | |||
0 ignored issues
–
show
|
|||
476 | def decode(self): |
||
477 | return self.id |
||
478 | |||
0 ignored issues
–
show
|
|||
479 | def get_value(self): |
||
480 | return self.id |
||
481 | |||
0 ignored issues
–
show
|
|||
482 | def set_value(self, val): |
||
483 | if val or val == 0: self.id = repr(val).replace("'",'"') |
||
0 ignored issues
–
show
|
|||
484 | |||
0 ignored issues
–
show
|
|||
485 | class JugglerTimezone(JugglerSimpleProperty): |
||
486 | ''' |
||
487 | Sets the project timezone. |
||
488 | Default value is UTC. |
||
489 | |||
0 ignored issues
–
show
|
|||
490 | Supports all tzdata values, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones |
||
491 | or https://stackoverflow.com/q/13866926 |
||
492 | ''' |
||
493 | DEFAULT_NAME = 'timezone' |
||
494 | DEFAULT_VALUE = 'UTC' |
||
495 | # DEFAULT_VALUE = 'Europe/Dublin' |
||
496 | |||
0 ignored issues
–
show
|
|||
497 | # TODO: checks! |
||
498 | |||
499 | class JugglerOutputdir(JugglerSimpleProperty): |
||
500 | LOG_STRING = "outputdir property" |
||
501 | DEFAULT_NAME = 'outputdir' |
||
502 | DEFAULT_VALUE = 'REPORT' |
||
503 | # TODO HERE: need to create the outputdir folder for this to execute! |
||
504 | |||
505 | class JugglerIcalreport(JugglerSimpleProperty): |
||
506 | LOG_STRING = "icalreport property" |
||
507 | DEFAULT_NAME = 'icalreport' |
||
508 | DEFAULT_VALUE = 'calendar' |
||
509 | |||
510 | class JugglerResource(JugglerCompoundKeyword): |
||
511 | DEFAULT_KEYWORD = "resource" |
||
512 | DEFAULT_ID = "me" |
||
513 | DEFAULT_SUMMARY = "Default Resource" |
||
514 | |||
0 ignored issues
–
show
|
|||
515 | def set_value(self, value): |
||
516 | self.summary = value |
||
517 | |||
518 | class JugglerWorkingHours(JugglerCompoundKeyword): |
||
519 | DEFAULT_KEYWORD = "workinghours" |
||
520 | DEFAULT_ID = "mon" |
||
521 | DEFAULT_SUMMARY = "" |
||
522 | |||
0 ignored issues
–
show
|
|||
523 | def set_id(self, id): |
||
0 ignored issues
–
show
|
|||
524 | if not id in ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]: raise ValueError('id for workinghours must be one of: "mon", "tue", "wed", "thu", "fri", "sat", "sun"') |
||
0 ignored issues
–
show
|
|||
525 | self.id = id |
||
526 | |||
0 ignored issues
–
show
|
|||
527 | def set_weekday(self, d = "mon"): |
||
0 ignored issues
–
show
|
|||
528 | """ |
||
529 | Set week day for workinghours |
||
530 | "mon", "tue", "wed", "thu", "fri", "sat", "sun" |
||
531 | """ |
||
532 | self.set_id(d) |
||
533 | |||
0 ignored issues
–
show
|
|||
534 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
535 | self.set_hour_interval() |
||
536 | |||
0 ignored issues
–
show
|
|||
537 | def set_hour_interval(self, start = 9, end = 19): |
||
0 ignored issues
–
show
|
|||
538 | self.option2 = "%s:00 - %s:00" % (start, end) |
||
539 | |||
540 | class JugglerTask(JugglerCompoundKeyword): |
||
541 | |||
542 | '''Class for a task for Task-Juggler''' |
||
543 | |||
544 | LOG_STRING = "JugglerTask" |
||
545 | DEFAULT_KEYWORD = 'task' |
||
546 | DEFAULT_ID = "unknown_task" |
||
547 | DEFAULT_SUMMARY = 'Task is not initialized' |
||
548 | |||
0 ignored issues
–
show
|
|||
549 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
550 | if not issue: |
||
551 | self.set_property(JugglerTaskAllocate("me")) |
||
552 | self.set_property(JugglerTaskEffort(1)) |
||
553 | else: |
||
554 | self.set_property(JugglerTaskAllocate(issue)) |
||
555 | self.set_property(JugglerTaskEffort(issue)) |
||
556 | self.set_property(JugglerTaskDepends(issue)) |
||
557 | |||
0 ignored issues
–
show
|
|||
558 | def load_from_issue(self, issue): |
||
559 | ''' |
||
560 | Load the object with data from a generic issue |
||
561 | |||
562 | Args: |
||
563 | issue (?): The generic issue to load from |
||
564 | ''' |
||
565 | self.load_default_properties(issue) |
||
566 | |||
0 ignored issues
–
show
|
|||
567 | def validate(self, tasks): |
||
568 | ''' |
||
569 | Validate (and correct) the current task |
||
570 | |||
571 | Args: |
||
572 | tasks (list): List of JugglerTask's to which the current task belongs. Will be used to |
||
573 | verify relations to other tasks. |
||
574 | ''' |
||
575 | if self.id == self.DEFAULT_ID: |
||
576 | logging.error('Found a task which is not initialized') |
||
577 | |||
578 | for prop in self.properties: |
||
579 | self.properties[prop].validate(self, tasks) |
||
580 | |||
581 | # def __str__(self): |
||
582 | # ''' |
||
583 | # Convert task object to the task juggler syntax |
||
584 | |||
585 | # Returns: |
||
586 | # str: String representation of the task in juggler syntax |
||
587 | # ''' |
||
588 | # props = '' |
||
589 | # for prop in self.properties: |
||
590 | # props += str(self.properties[prop]) |
||
591 | # return self.TEMPLATE.format(id=to_identifier(self.key), |
||
592 | # key=self.key, |
||
593 | # description=self.summary.replace('\"', '\\\"'), |
||
594 | # props=props) |
||
595 | |||
596 | class JugglerTimesheet(): |
||
597 | pass |
||
598 | |||
599 | class JugglerBooking(JugglerCompoundKeyword): |
||
600 | LOG_STRING = "JugglerBooking" |
||
601 | DEFAULT_KEYWORD = "booking" |
||
602 | DEFAULT_ID = "me" # resource |
||
603 | |||
0 ignored issues
–
show
|
|||
604 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
605 | self.start = None |
||
606 | self.end = None |
||
607 | |||
0 ignored issues
–
show
|
|||
608 | def set_resource(self, res): |
||
609 | self.set_id(res) |
||
610 | |||
0 ignored issues
–
show
|
|||
611 | def set_interval(self, start, end): |
||
612 | self.start = start |
||
0 ignored issues
–
show
|
|||
613 | self.end = end |
||
0 ignored issues
–
show
|
|||
614 | self.option2 = to_tj3interval(self.start, self.end) |
||
615 | |||
0 ignored issues
–
show
|
|||
616 | def decode(self): |
||
617 | return [self.start, self.end] |
||
618 | |||
0 ignored issues
–
show
|
|||
619 | def load_from_issue(self, issue = None): |
||
0 ignored issues
–
show
|
|||
620 | start = issue["start"] |
||
621 | end = issue["end"] |
||
622 | self.set_interval(start, end) |
||
623 | self.set_resource(issue["resource"]) |
||
624 | |||
625 | class JugglerProject(JugglerCompoundKeyword): |
||
626 | |||
0 ignored issues
–
show
|
|||
627 | '''Template for TaskJuggler project''' |
||
628 | |||
0 ignored issues
–
show
|
|||
629 | LOG_STRING = "JugglerProject" |
||
630 | DEFAULT_KEYWORD = 'project' |
||
631 | DEFAULT_ID = "default" # id may be empty for some keywords |
||
632 | DEFAULT_SUMMARY = 'Default Project' # no summary is possible everywhere |
||
633 | |||
0 ignored issues
–
show
|
|||
634 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
635 | self.set_property(JugglerTimezone()) |
||
636 | self.set_property(JugglerOutputdir()) |
||
637 | self.set_interval() |
||
638 | |||
0 ignored issues
–
show
|
|||
639 | def set_interval(self, start = datetime.datetime.now().replace(microsecond=0,second=0,minute=0), end = datetime.datetime(2035, 1, 1)): |
||
0 ignored issues
–
show
|
|||
640 | self.option2 = to_tj3interval(start, end) |
||
641 | |||
0 ignored issues
–
show
|
|||
642 | |||
643 | class JugglerSource(JugglerCompoundKeyword): |
||
644 | """ |
||
645 | The entire project skeleton |
||
646 | |||
0 ignored issues
–
show
|
|||
647 | Sets reports and folders for use with parser |
||
648 | |||
0 ignored issues
–
show
|
|||
649 | Must be extended with load_from_issue(self,issue) appending tasks |
||
0 ignored issues
–
show
|
|||
650 | """ |
||
651 | |||
0 ignored issues
–
show
|
|||
652 | LOG_STRING = "JugglerSource" |
||
653 | DEFAULT_KEYWORD = '' |
||
654 | DEFAULT_ID = '' |
||
655 | COMMENTS_HEADER = ''' |
||
656 | // TaskJuggler 3 source |
||
657 | // generated by python-juggler (c) 2017 Andrew Gryaznov and others |
||
658 | // https://github.com/grandrew/taskjuggler-python |
||
659 | ''' |
||
660 | ENCLOSED_BLOCK = False |
||
661 | |||
0 ignored issues
–
show
|
|||
662 | def load_default_properties(self, issue = None): |
||
0 ignored issues
–
show
|
|||
663 | self.set_property(JugglerProject()) |
||
664 | self.set_property(JugglerResource()) |
||
665 | self.set_property(JugglerIcalreport()) |
||
666 | |||
0 ignored issues
–
show
|
|||
667 | def _post_init(self, issue = None): |
||
0 ignored issues
–
show
|
|||
668 | self.top = self |
||
669 | |||
0 ignored issues
–
show
|
|||
670 | class GenericJuggler(object): |
||
0 ignored issues
–
show
|
|||
671 | |||
672 | '''Class for task-juggling generic results''' |
||
673 | |||
0 ignored issues
–
show
|
|||
674 | src = None |
||
675 | |||
0 ignored issues
–
show
|
|||
676 | def __init__(self): |
||
677 | ''' |
||
678 | Construct a generic juggler object |
||
679 | |||
680 | Args: |
||
681 | none |
||
682 | ''' |
||
683 | |||
684 | logging.info('generic load') |
||
685 | |||
686 | def set_query(self, query): |
||
687 | ''' |
||
688 | Set the query for the generic juggler object |
||
689 | |||
690 | Args: |
||
691 | query (str): The Query to run on generic server |
||
692 | ''' |
||
693 | |||
694 | logging.info('Query: %s', query) |
||
695 | self.query = query |
||
0 ignored issues
–
show
|
|||
696 | self.issue_count = 0 |
||
0 ignored issues
–
show
|
|||
697 | |||
698 | @staticmethod |
||
699 | def validate_tasks(tasks): |
||
700 | ''' |
||
701 | Validate (and correct) tasks |
||
702 | |||
703 | Args: |
||
704 | tasks (list): List of JugglerTask's to validate |
||
705 | ''' |
||
706 | for task in tasks: |
||
707 | task.validate(tasks) |
||
708 | |||
0 ignored issues
–
show
|
|||
709 | def load_issues(self): |
||
710 | raise NotImplementedError |
||
711 | |||
0 ignored issues
–
show
|
|||
712 | def add_task(self, task): |
||
713 | ''' |
||
714 | Add task to current project |
||
715 | |||
0 ignored issues
–
show
|
|||
716 | Args: |
||
717 | task (JugglerTask): a task to add |
||
718 | ''' |
||
719 | if not self.src: |
||
720 | self.juggle() |
||
721 | self.src.set_property(task) |
||
722 | |||
723 | def load_issues_incremetal(self): |
||
724 | if self.loaded: return [] |
||
0 ignored issues
–
show
|
|||
725 | self.loaded = True |
||
0 ignored issues
–
show
|
|||
726 | return self.load_issues() |
||
727 | |||
0 ignored issues
–
show
|
|||
728 | def create_task_instance(self, issue): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
729 | return JugglerTask(issue) |
||
730 | |||
731 | def load_issues_from_generic(self): |
||
732 | ''' |
||
733 | Load issues from generic |
||
734 | |||
735 | Returns: |
||
736 | list: A list of dicts containing the generic tickets |
||
737 | ''' |
||
738 | tasks = [] |
||
739 | busy = True |
||
740 | self.loaded = False |
||
0 ignored issues
–
show
|
|||
741 | self.issue_count = 0 |
||
0 ignored issues
–
show
|
|||
742 | |||
0 ignored issues
–
show
|
|||
743 | while busy: |
||
744 | try: |
||
745 | issues = self.load_issues_incremetal() |
||
746 | except NotImplementedError: |
||
747 | logging.error('Loading Issues is not implemented in upstream library') |
||
748 | return None |
||
749 | |||
0 ignored issues
–
show
|
|||
750 | except: |
||
0 ignored issues
–
show
General except handlers without types should be used sparingly.
Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of. ![]() |
|||
751 | logging.error('Could not get issue') |
||
752 | return None |
||
753 | |||
754 | if len(issues) <= 0: |
||
0 ignored issues
–
show
|
|||
755 | busy = False |
||
756 | |||
757 | self.issue_count += len(issues) |
||
758 | |||
759 | for issue in issues: |
||
760 | logging.debug('Retrieved %s', repr(issue)) |
||
761 | tasks.append(self.create_task_instance(issue)) |
||
762 | |||
763 | self.validate_tasks(tasks) |
||
764 | |||
765 | return tasks |
||
766 | |||
0 ignored issues
–
show
|
|||
767 | def create_jugglersource_instance(self): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
![]() |
|||
768 | return JugglerSource() |
||
769 | |||
0 ignored issues
–
show
|
|||
770 | def juggle(self): |
||
771 | """ |
||
772 | Export the loaded issues onto the JuggleSource structure |
||
773 | """ |
||
774 | self.src = self.create_jugglersource_instance() |
||
775 | issues = self.load_issues_from_generic() |
||
776 | if not issues: |
||
777 | # return None |
||
778 | issues = [] |
||
779 | for issue in issues: |
||
780 | self.src.set_property(issue) |
||
781 | return self.src |
||
782 | |||
783 | def write_file(self, output=None): |
||
784 | ''' |
||
785 | Query generic and generate task-juggler output from given issues |
||
786 | |||
787 | Args: |
||
788 | output (str): Name of output file, for task-juggler |
||
789 | ''' |
||
790 | |||
0 ignored issues
–
show
|
|||
791 | if not self.src: |
||
792 | self.juggle() |
||
793 | |||
0 ignored issues
–
show
|
|||
794 | s = str(self.src) |
||
795 | |||
0 ignored issues
–
show
|
|||
796 | if output and isinstance(output, str): |
||
797 | with open(output, 'w') as out: |
||
798 | out.write(s) |
||
799 | elif output and isinstance(output, file): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
800 | output.write(s) |
||
801 | # else: |
||
802 | # raise ValueError("output should be a filename string or a file handler") |
||
803 | return s |
||
804 | |||
0 ignored issues
–
show
|
|||
805 | def read_ical_result(self, icalfile): |
||
806 | tasks = self.src.walk(JugglerTask) |
||
807 | cal = icalendar.Calendar.from_ical(file(icalfile).read()) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
808 | for ev in cal.walk('VEVENT'): # pylint:disable=no-member |
||
809 | start_date = ev.decoded("DTSTART") |
||
810 | end_date = ev.decoded("DTEND") |
||
811 | id = from_identifier(ev.decoded("UID").split("-")[1]) |
||
0 ignored issues
–
show
|
|||
812 | for t in tasks: |
||
813 | if t.id == id: |
||
814 | # logging.info("Testing %s" % str(t)) |
||
815 | # logging.info("Got %s" % repr(t.walk(JugglerTaskAllocate))) |
||
816 | |||
0 ignored issues
–
show
|
|||
817 | # ical does not support resource allocation reporting |
||
818 | # so we do not support multiple resource here |
||
819 | # and we don't support them yet anyways |
||
820 | t.set_property(JugglerBooking({ |
||
821 | "resource":t.walk(JugglerTaskAllocate)[0].get_value(), |
||
822 | "start": start_date, |
||
823 | "end": end_date |
||
824 | })) |
||
825 | |||
0 ignored issues
–
show
|
|||
826 | def run(self, outfolder=None, infile=None): |
||
827 | ''' |
||
828 | Run the taskjuggler task |
||
829 | |||
830 | Args: |
||
831 | output (str): Name of output file, for task-juggler |
||
832 | ''' |
||
833 | if not self.src: |
||
834 | self.juggle() |
||
835 | |||
0 ignored issues
–
show
|
|||
836 | if outfolder is None: |
||
837 | outfolder = tempfile.mkdtemp("TJP") |
||
838 | self.outfolder = outfolder |
||
0 ignored issues
–
show
|
|||
839 | |||
0 ignored issues
–
show
|
|||
840 | if infile is None: |
||
841 | infile = tempfile.mkstemp(".tjp")[1] |
||
842 | self.infile = infile |
||
0 ignored issues
–
show
|
|||
843 | |||
0 ignored issues
–
show
|
|||
844 | reportdir = self.src.walk(JugglerOutputdir) |
||
845 | orig_rep = reportdir[0].get_value() |
||
846 | reportdir[0].set_value(outfolder) |
||
847 | |||
0 ignored issues
–
show
|
|||
848 | ical_report_name = "calendar_out" |
||
849 | ical_report_path = os.path.join(outfolder, ical_report_name) |
||
850 | icalreport = self.src.walk(JugglerIcalreport) |
||
851 | orig_cal = icalreport[0].get_value() |
||
852 | icalreport[0].set_value(ical_report_name) |
||
853 | |||
0 ignored issues
–
show
|
|||
854 | self.write_file(infile) |
||
855 | |||
0 ignored issues
–
show
|
|||
856 | logging.debug("Running from %s to out %s" % (self.infile, self.outfolder)) |
||
0 ignored issues
–
show
|
|||
857 | |||
0 ignored issues
–
show
|
|||
858 | subprocess.call(["/usr/bin/env", "tj3", infile]) |
||
859 | |||
0 ignored issues
–
show
|
|||
860 | self.read_ical_result(ical_report_path+".ics") |
||
861 | |||
0 ignored issues
–
show
|
|||
862 | icalreport[0].set_value(orig_cal) |
||
863 | reportdir[0].set_value(orig_rep) |
||
864 | |||
0 ignored issues
–
show
|
|||
865 | if DEBUG or logging.getLogger().getEffectiveLevel() >= logging.DEBUG: return |
||
0 ignored issues
–
show
|
|||
866 | shutil.rmtree(self.outfolder) |
||
867 | os.remove(self.infile) |
||
868 | |||
0 ignored issues
–
show
|
|||
869 | # TODO HERE: load the ical file back to the actual tree (no tree support yet?) |
||
870 | |||
0 ignored issues
–
show
|
|||
871 | def clean(self): |
||
872 | "clean after running" |
||
873 | if DEBUG or logging.getLogger().getEffectiveLevel() >= logging.DEBUG: return |
||
0 ignored issues
–
show
|
|||
874 | try: shutil.rmtree(self.outfolder) |
||
0 ignored issues
–
show
|
|||
875 | except: pass |
||
0 ignored issues
–
show
General except handlers without types should be used sparingly.
Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of. ![]() |
|||
876 | try: os.remove(self.infile) |
||
0 ignored issues
–
show
|
|||
877 | except: pass |
||
0 ignored issues
–
show
General except handlers without types should be used sparingly.
Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of. ![]() |
|||
878 | |||
0 ignored issues
–
show
|
|||
879 | def walk(self, cls): |
||
880 | if not self.src: |
||
881 | self.juggle() |
||
882 | return self.src.walk(cls) |
||
883 | |||
0 ignored issues
–
show
|
|||
884 | def __inter__(self): |
||
885 | "provide dict(j) method to generate dictionary structure for tasks" |
||
886 | raise NotImplementedError |
||
887 | |||
0 ignored issues
–
show
|
|||
888 | def __del__(self): |
||
889 | self.clean() |
||
890 | |||
891 | |||
0 ignored issues
–
show
|
|||
892 |
This check looks for lines that are too long. You can specify the maximum line length.