GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 73a429...82b18e )
by Andrew
01:13
created

JugglerTaskEffort.set_value()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
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
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
7
"""
8
9
import logging,tempfile,subprocess,datetime,icalendar,shutil,os
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
introduced by
Multiple imports on one line (logging, tempfile, subprocess, datetime, icalendar, shutil, os)
Loading history...
introduced by
Unable to import 'icalendar'
Loading history...
Unused Code introduced by
The import datetime seems to be unused.
Loading history...
Unused Code introduced by
The import shutil seems to be unused.
Loading history...
introduced by
Imports from package logging are not grouped
Loading history...
10
from collections import OrderedDict
11
12
DEFAULT_LOGLEVEL = 'warning'
13
DEFAULT_OUTPUT = 'export.tjp'
14
15
TAB = ' ' * 4
16
17
def is_number(s):
18
    try:
19
        float(s)
20
        return True
21
    except ValueError:
22
        return False
23
24
def set_logging_level(loglevel):
25
    '''
26
    Set the logging level
27
28
    Args:
29
        loglevel String representation of the loglevel
30
    '''
31
    numeric_level = getattr(logging, loglevel.upper(), None)
32
    if not isinstance(numeric_level, int):
33
        raise ValueError('Invalid log level: %s' % loglevel)
34
    logging.basicConfig(level=numeric_level)
35
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
36
def to_tj3time(dt):
37
    """
38
    Convert python date or datetime object to TJ3 format
39
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
40
    """
41
    return dt.isoformat().replace("T", "-").split(".")[0]
42
43
def to_tj3interval(start, end):
44
    return "%s - %s" % (to_tj3time(start), to_tj3time(end))
45
46
TJP_NUM_ID_PREFIX = "tjp_numid_"
47
TJP_DASH_PREFIX = "__DASH__"
48
TJP_SPACE_PREFIX = "__SPACE__"
49
50
def to_identifier(key):
51
    '''
52
    Convert given key to identifier, interpretable by TaskJuggler as a task-identifier
53
54
    Args:
55
        key (str): Key to be converted
56
57
    Returns:
58
        str: Valid task-identifier based on given key
59
    '''
60
    if is_number(key):
61
        key = TJP_NUM_ID_PREFIX+str(key)
62
    key = key.replace('-', TJP_DASH_PREFIX).replace(" ", TJP_SPACE_PREFIX)
63
    return key
64
65
def from_identifier(key):
66
    if TJP_NUM_ID_PREFIX in key:
67
        return int(key.replace(TJP_NUM_ID_PREFIX, ""))
68
    return key.replace(TJP_DASH_PREFIX, "-").replace(TJP_SPACE_PREFIX, " ")
69
70
class JugglerTaskProperty(object):
71
    '''Class for a property of a Task Juggler'''
72
73
    DEFAULT_NAME = 'property name'
74
    DEFAULT_VALUE = 'not initialized'
75
    PREFIX = ''
76
    SUFFIX = ''
77
    TEMPLATE = TAB + '{prop} {value}\n'
78
    VALUE_TEMPLATE = '{prefix}{value}{suffix}'
79
80
81
    def __init__(self, issue=None):
82
        '''
83
        Initialize task juggler property
84
85
        Args:
86
            issue (class): The generic issue to load from
87
            value (object): Value of the property
88
        '''
89
        self.name = self.DEFAULT_NAME
90
        self.set_value(self.DEFAULT_VALUE)
91
        self.empty = False
92
        self.parent = None
93
        self.load_default_properties(issue)
94
95
        if issue:
96
            if self.load_from_issue(issue) is False:
97
                self.empty = True
98
99
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
100
        pass
101
102
    def load_from_issue(self, issue):
103
        '''
104
        Load the object with data from a generic issue
105
106
        Args:
107
            issue (class): The generic issue to load from
108
        '''
109
        pass
110
111
    def get_name(self):
112
        '''
113
        Get name for task juggler property
114
115
        Returns:
116
            str: Name of the task juggler property
117
        '''
118
        return self.name
119
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
120
    def get_id(self):
0 ignored issues
show
Coding Style introduced by
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;
Loading history...
121
        return ""
122
123
    def set_value(self, value):
124
        '''
125
        Set value for task juggler property
126
127
        Args:
128
            value (object): New value of the property
129
        '''
130
        self.value = value
131
132
    def append_value(self, value):
133
        '''
134
        Append value for task juggler property
135
136
        Args:
137
            value (object): Value to append to the property
138
        '''
139
        self.value.append(value)
140
141
    def get_value(self):
142
        '''
143
        Get value for task juggler property
144
145
        Returns:
146
            str: Value of the task juggler property
147
        '''
148
        if self.value == self.DEFAULT_VALUE: return ""
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
149
        return self.value
150
151
    def validate(self, task, tasks):
152
        '''
153
        Validate (and correct) the current task property
154
155
        Args:
156
            task (JugglerTask): Task to which the property belongs
157
            tasks (list):       List of JugglerTask's to which the current task belongs. Will be used to
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
158
                                verify relations to other tasks.
159
        '''
160
        pass
161
162
    def __str__(self):
163
        '''
164
        Convert task property object to the task juggler syntax
165
166
        Returns:
167
            str: String representation of the task property in juggler syntax
168
        '''
169
170
        if self.get_value(): 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
171
            # TODO: list support (like allocate multiple) (copy from taskdepends)
172
            # TODO: identifier conversion support?
173
            return self.TEMPLATE.format(prop=self.get_name(),
174
                                        value=self.VALUE_TEMPLATE.format(prefix=self.PREFIX,
175
                                                                         value=self.get_value(),
176
                                                                         suffix=self.SUFFIX))
177
        return ''
178
179
class JugglerTaskAllocate(JugglerTaskProperty):
180
    '''Class for the allocate (assignee) of a juggler task'''
181
182
    DEFAULT_NAME = 'allocate'
183
    DEFAULT_VALUE = 'not assigned'
184
185
    def load_from_issue(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
186
        '''
187
        Load the object with data from a generic issue
188
189
        Args:
190
            issue (class): The generic issue to load from
191
        '''
192
        self.set_value(self.DEFAULT_VALUE)
193
        if not issue is None:
194
            self.set_value(issue) # TODO: this may be list or primitive
195
196
class JugglerTaskEffort(JugglerTaskProperty):
197
    '''Class for the effort (estimate) of a juggler task'''
198
199
    #For converting the seconds (generic) to days
200
    UNIT = 'd'
201
    FACTOR = 8.0 * 60 * 60
202
203
    DEFAULT_NAME = 'effort'
204
    MINIMAL_VALUE = 1.0 / 8
205
    DEFAULT_VALUE = MINIMAL_VALUE
206
    SUFFIX = UNIT
207
208
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
209
        self.SUFFIX = self.UNIT
210
211
    def load_from_issue(self, issue):
212
        '''
213
        Load the object with data from a generic issue
214
215
        Args:
216
            issue (class): The generic issue to load from
217
        '''
218
        self.set_value(self.DEFAULT_VALUE)
219
        if hasattr(issue.fields, 'aggregatetimeoriginalestimate') and issue.fields.aggregatetimeoriginalestimate:
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (113/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
220
            val = issue.fields.aggregatetimeoriginalestimate
221
            self.set_value(val / self.FACTOR)
222
        else:
223
            logging.warning('No estimate found for %s, assuming %s%s', issue.key, self.DEFAULT_VALUE, self.UNIT)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (112/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
224
    def set_value(self, value):
225
        '''
226
        Set the value for effort. Will convert whatever number to integer.
227
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
228
        Default class unit is 'days'. Can be overrided by setting "UNIT" global class attribute
229
        '''
230
        self.value = int(value)
231
    def validate(self, task, tasks):
232
        '''
233
        Validate (and correct) the current task property
234
235
        Args:
236
            task (JugglerTask): Task to which the property belongs
237
            tasks (list):       List of JugglerTask's to which the current task belongs. Will be used to
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
238
                                verify relations to other tasks.
239
        '''
240
        pass
241
        # if self.get_value() < self.MINIMAL_VALUE:
242
        #     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
Coding Style introduced by
This line is too long as per the coding-style (146/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
243
        #     self.set_value(self.MINIMAL_VALUE)
244
245
class JugglerTaskDepends(JugglerTaskProperty):
246
    '''Class for the effort (estimate) of a juggler task'''
247
248
    DEFAULT_NAME = 'depends'
249
    DEFAULT_VALUE = []
250
    PREFIX = '!'
251
252
    def set_value(self, value):
253
        '''
254
        Set value for task juggler property (deep copy)
255
256
        Args:
257
            value (object): New value of the property
258
        '''
259
        self.value = list(value)
260
261
    def load_from_issue(self, issue):
262
        '''
263
        Load the object with data from a generic issue
264
265
        Args:
266
            issue (class): The generic issue to load from
267
        '''
268
        raise NotImplementedError("load_from_issue is not implemented for this depend")
269
        # TODO: remove these - are for jira
270
        self.set_value(self.DEFAULT_VALUE)
0 ignored issues
show
Unused Code introduced by
This code does not seem to be reachable.
Loading history...
271
        if hasattr(issue.fields, 'issuelinks'):
272
            for link in issue.fields.issuelinks:
273
                if hasattr(link, 'inwardIssue') and link.type.name == 'Blocker':
274
                    self.append_value(to_identifier(link.inwardIssue.key))
275
276
    def validate(self, task, tasks):
277
        '''
278
        Validate (and correct) the current task property
279
280
        Args:
281
            task (JugglerTask): Task to which the property belongs
282
            tasks (list):       List of JugglerTask's to which the current task belongs. Will be used to
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
283
                                verify relations to other tasks.
284
        '''
285
        # TODO: add support for nested tasks with self.parent
286
        for val in list(self.get_value()):
287
             if val not in [tsk.get_id() for tsk in tasks]:
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 12 spaces were expected, but 13 were found.
Loading history...
288
                 logging.warning('Removing link to %s for %s, as not within scope', val, task.get_id())
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 17 were found.
Loading history...
289
                 self.value.remove(val)
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 17 were found.
Loading history...
290
291
    def __str__(self):
292
        '''
293
        Convert task property object to the task juggler syntax
294
295
        Returns:
296
            str: String representation of the task property in juggler syntax
297
        '''
298
299
        if self.get_value():
300
            valstr = ''
301
            for val in self.get_value():
302
                if valstr:
303
                    valstr += ', '
304
                valstr += self.VALUE_TEMPLATE.format(prefix=self.PREFIX,
305
                                                     value=to_identifier(val),
306
                                                     suffix=self.SUFFIX)
307
            return self.TEMPLATE.format(prop=self.get_name(),
308
                                        value=valstr)
309
        return ''
310
311
# class NonEmptyObject(object):
312
#     def __init__(self):
313
#         self.empty = False
314
#     def __bool__(self):
315
#         return not self.empty
316
#     __nonzero__=__bool__
317
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
318
319
class JugglerCompoundKeyword(object):
320
321
    '''Class for a general compound object in TJ syntax'''
322
323
    COMMENTS_HEADER = ""
324
    LOG_STRING = "DefaultKeyword"
325
    DEFAULT_KEYWORD = 'unknown_keyword'
326
    DEFAULT_ID = "" # id may be empty for some keywords
327
    DEFAULT_SUMMARY = '' # no summary is possible everywhere
328
    TEMPLATE = '''{header}\n{keyword} {id}'''
329
    ENCLOSED_BLOCK = True
330
331
    def __init__(self, issue=None):
332
        logging.info('Create %s', self.LOG_STRING)
333
        self.empty = False
334
        self.parent = None
335
        self.keyword = self.DEFAULT_KEYWORD
336
        self.id = self.DEFAULT_ID
337
        self.summary = self.DEFAULT_SUMMARY
338
        self.option2 = ""
339
        self.properties = OrderedDict()
340
        self.load_default_properties(issue)
341
342
        if issue:
343
            if self.load_from_issue(issue) is False:
344
                self.empty = True
345
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
346
    def load_from_issue(self, issue):
347
        '''
348
        Load the object with data from a generic issue
349
350
        Args:
351
            issue (class): The generic issue to load from
352
        '''
353
        pass
354
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
355
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
356
        pass
357
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
358
    def get_name(self):
359
        return self.keyword
360
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
361
    def get_id(self):
362
        return self.id
363
364
    def set_property(self, prop):
365
        if prop: 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
366
            self.properties[prop.get_name() + repr(prop.get_id())] = prop
367
            prop.parent = self # TODO: control un-set?, GC?
368
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
369
    def set_id(self, id):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in id.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
370
        self.id = id
371
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
372
    def decode(self):
373
        return self.option2
374
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
375
    def walk(self, cls, ls = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
376
        if ls is None: ls = []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
377
        for key, item in self.properties.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
378
            if isinstance(item, JugglerCompoundKeyword):
379
                ls = item.walk(cls, ls)
380
            if isinstance(item, cls):
381
                ls.append(item)
382
        return ls
383
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
384
    def __str__(self):
385
        if self.empty: return ""
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
386
        out = self.TEMPLATE.format(header=self.COMMENTS_HEADER,keyword=self.keyword, id=to_identifier(self.id))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (111/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
Coding Style introduced by
Exactly one space required after comma
Loading history...
387
        if self.summary:
388
            out += ' "%s"' % self.summary.replace('\"', '\\\"')
389
        if self.option2:
390
            out += ' %s ' % self.option2
391
        if self.properties and self.ENCLOSED_BLOCK: out += " {\n"
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
392
        for prop in self.properties:
393
            out += str(self.properties[prop])
394
        if self.properties and self.ENCLOSED_BLOCK: out += "\n}"
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
395
        return out
396
397
class JugglerSimpleProperty(JugglerCompoundKeyword):
398
    LOG_STRING = "Default Simple Property"
399
    DEFAULT_NAME = 'unknown_property'
400
    DEFAULT_VALUE = ''
401
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
402
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
403
        self.keyword = self.DEFAULT_NAME
404
        self.set_value(self.DEFAULT_VALUE)
405
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
406
    def load_from_issue(self, value):
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'load_from_issue' method
Loading history...
407
        self.set_value(value)
408
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
409
    def get_name(self):
410
        return self.keyword
411
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
412
    def decode(self):
413
        return self.id
414
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
415
    def get_value(self):
416
        return self.id
417
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
418
    def set_value(self, val):
419
        if val or val == 0: self.id = repr(val).replace("'",'"')
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
Coding Style introduced by
More than one statement on a single line
Loading history...
420
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
421
class JugglerTimezone(JugglerSimpleProperty):
422
    DEFAULT_NAME = 'timezone'
423
    DEFAULT_VALUE = 'Europe/Dublin'
424
425
class JugglerOutputdir(JugglerSimpleProperty):
426
    LOG_STRING = "outputdir property"
427
    DEFAULT_NAME = 'outputdir'
428
    DEFAULT_VALUE = 'REPORT'
429
    # TODO HERE: need to create the outputdir folder for this to execute!
430
431
class JugglerIcalreport(JugglerSimpleProperty):
432
    LOG_STRING = "icalreport property"
433
    DEFAULT_NAME = 'icalreport'
434
    DEFAULT_VALUE = 'calendar'
435
436
class JugglerResource(JugglerCompoundKeyword):
437
    DEFAULT_KEYWORD = "resource"
438
    DEFAULT_ID = "me"
439
    DEFAULT_SUMMARY = "Default Resource"
440
441
class JugglerTask(JugglerCompoundKeyword):
442
443
    '''Class for a task for Task-Juggler'''
444
445
    LOG_STRING = "JugglerTask"
446
    DEFAULT_KEYWORD = 'task'
447
    DEFAULT_ID = "unknown_task"
448
    DEFAULT_SUMMARY = 'Task is not initialized'
449
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
450
    # def load_default_properties(self, issue):
451
    #     self.set_property(JugglerTaskAllocate(issue))
452
    #     self.set_property(JugglerTaskEffort(issue))
453
    #     self.set_property(JugglerTaskDepends(issue))
454
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
455
    # def load_from_issue(self, issue):
456
    #     '''
457
    #     Load the object with data from a generic issue
458
459
    #     Args:
460
    #         issue (?): The generic issue to load from
461
    #     '''
462
    #     self.id = hash(issue)
463
    #     self.load_default_properties(issue)
464
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
465
    def validate(self, tasks):
466
        '''
467
        Validate (and correct) the current task
468
469
        Args:
470
            tasks (list): List of JugglerTask's to which the current task belongs. Will be used to
471
                          verify relations to other tasks.
472
        '''
473
        if self.id == self.DEFAULT_ID:
474
            logging.error('Found a task which is not initialized')
475
476
        for prop in self.properties:
477
            self.properties[prop].validate(self, tasks)
478
479
    # def __str__(self):
480
    #     '''
481
    #     Convert task object to the task juggler syntax
482
483
    #     Returns:
484
    #         str: String representation of the task in juggler syntax
485
    #     '''
486
    #     props = ''
487
    #     for prop in self.properties:
488
    #         props += str(self.properties[prop])
489
    #     return self.TEMPLATE.format(id=to_identifier(self.key),
490
    #                                 key=self.key,
491
    #                                 description=self.summary.replace('\"', '\\\"'),
492
    #                                 props=props)
493
494
class JugglerTimesheet():
495
    pass
496
497
class JugglerBooking(JugglerCompoundKeyword):
498
    LOG_STRING = "JugglerBooking"
499
    DEFAULT_KEYWORD = "booking"
500
    DEFAULT_ID = "me" # resource
501
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
502
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
503
        self.start = None
504
        self.end = None
505
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
506
    def set_resource(self, res):
507
        self.set_id(res)
508
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
509
    def set_interval(self, start, end):
510
        self.start = start
0 ignored issues
show
Coding Style introduced by
The attribute start was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
511
        self.end = end
0 ignored issues
show
Coding Style introduced by
The attribute end was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
512
        self.option2 = to_tj3interval(self.start, self.end)
513
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
514
    def decode(self):
515
        return [self.start, self.end]
516
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
517
    def load_from_issue(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
518
        start = issue["start"]
519
        end = issue["end"]
520
        self.set_interval(start, end)
521
        self.set_resource(issue["resource"])
522
523
class JugglerProject(JugglerCompoundKeyword):
524
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
525
    '''Template for TaskJuggler project'''
526
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
527
    LOG_STRING = "JugglerProject"
528
    DEFAULT_KEYWORD = 'project'
529
    DEFAULT_ID = "default" # id may be empty for some keywords
530
    DEFAULT_SUMMARY = 'Default Project' # no summary is possible everywhere
531
    DEFAULT_INTERVAL = "2017-10-10 - 2035-10-10"
532
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
533
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
534
        self.set_property(JugglerTimezone())
535
        self.set_property(JugglerOutputdir())
536
        self.option2 = self.DEFAULT_INTERVAL
537
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
538
    def set_interval(self, start, end):
539
        self.option2 = to_tj3interval(start, end)
540
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
541
542
class JugglerSource(JugglerCompoundKeyword):
543
    """
544
    The entire project skeleton
545
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
546
    Sets reports and folders for use with parser
547
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
548
    Must be extended with load_from_issue(self,issue) appending tasks 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
549
    """
550
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
551
    LOG_STRING = "JugglerSource"
552
    DEFAULT_KEYWORD = ''
553
    DEFAULT_ID = ''
554
    COMMENTS_HEADER = '''
555
    // TaskJuggler 3 source
556
    // generated by python-juggler (c) 2017 Andrew Gryaznov and others
557
    // https://github.com/grandrew/taskjuggler-python
558
    '''
559
    ENCLOSED_BLOCK = False
560
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
561
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
562
        self.set_property(JugglerProject())
563
        self.set_property(JugglerResource())
564
        self.set_property(JugglerIcalreport())
565
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
566
class GenericJuggler(object):
567
568
    '''Class for task-juggling generic results'''
569
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
570
    src = None
571
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
572
    def __init__(self):
573
        '''
574
        Construct a generic juggler object
575
576
        Args:
577
            none
578
        '''
579
580
        logging.info('generic load')
581
582
    def set_query(self, query):
583
        '''
584
        Set the query for the generic juggler object
585
586
        Args:
587
            query (str): The Query to run on generic server
588
        '''
589
590
        logging.info('Query: %s', query)
591
        self.query = query
0 ignored issues
show
Coding Style introduced by
The attribute query was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
592
        self.issue_count = 0
0 ignored issues
show
Coding Style introduced by
The attribute issue_count was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
593
594
    @staticmethod
595
    def validate_tasks(tasks):
596
        '''
597
        Validate (and correct) tasks
598
599
        Args:
600
            tasks (list): List of JugglerTask's to validate
601
        '''
602
        for task in tasks:
603
            task.validate(tasks)
604
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
605
    def load_issues(self):
606
        raise NotImplementedError
607
608
    def load_issues_incremetal(self):
609
        if self.loaded: return []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
610
        self.loaded = True
0 ignored issues
show
Coding Style introduced by
The attribute loaded was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
611
        return self.load_issues()
612
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
613
    def create_task_instance(self, issue):
0 ignored issues
show
Coding Style introduced by
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;
Loading history...
614
        return JugglerTask(issue)
615
616
    def load_issues_from_generic(self):
617
        '''
618
        Load issues from generic
619
620
        Returns:
621
            list: A list of dicts containing the generic tickets
622
        '''
623
        tasks = []
624
        busy = True
625
        self.loaded = False
0 ignored issues
show
Coding Style introduced by
The attribute loaded was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
626
        self.issue_count = 0
0 ignored issues
show
Coding Style introduced by
The attribute issue_count was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
627
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
628
        while busy:
629
            try:
630
                issues = self.load_issues_incremetal()
631
            except NotImplementedError:
632
                logging.error('Loading Issues is not implemented in upstream library')
633
                return None
634
                
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
635
            except:
0 ignored issues
show
Coding Style Best Practice introduced by
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.

Loading history...
636
                logging.error('Could not get issue')
637
                return None
638
639
            if len(issues) <= 0:
640
                busy = False
641
642
            self.issue_count += len(issues)
643
644
            for issue in issues:
645
                logging.debug('Retrieved %s', repr(issue))
646
                tasks.append(self.create_task_instance(issue))
647
648
        self.validate_tasks(tasks)
649
650
        return tasks
651
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
652
    def create_jugglersource_instance(self):
0 ignored issues
show
Coding Style introduced by
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;
Loading history...
653
        return JugglerSource()
654
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
655
    def juggle(self):
656
        """
657
        Export the loaded issues onto the JuggleSource structure
658
        """
659
        issues = self.load_issues_from_generic()
660
        if not issues:
661
            return None
662
        # TODO HERE set reportfolder from parameters
663
        self.src = self.create_jugglersource_instance()
664
        for issue in issues:
665
            self.src.set_property(issue)
666
        return self.src
667
668
    def write_file(self, output=None):
669
        '''
670
        Query generic and generate task-juggler output from given issues
671
672
        Args:
673
            output (str): Name of output file, for task-juggler
674
        '''
675
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
676
        s = str(self.src)
677
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
678
        if output and isinstance(output, str):
679
            with open(output, 'w') as out:
680
                out.write(s)
681
        elif output and isinstance(output, file):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
682
            output.write(s)
683
        # else:
684
        #     raise ValueError("output should be a filename string or a file handler")
685
        return s
686
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
687
    def read_ical_result(self, icalfile):
688
        tasks = self.src.walk(JugglerTask)
689
        cal = icalendar.Calendar.from_ical(file(icalfile).read())
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
690
        for ev in cal.walk('VEVENT'):
691
            start_date = ev.decoded("DTSTART")
692
            end_date = ev.decoded("DTEND")
693
            id = from_identifier(ev.decoded("UID").split("-")[1])
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in id.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
694
            for t in tasks:
695
                if t.id == id:
696
                    # logging.info("Testing %s" % str(t))
697
                    # logging.info("Got %s" % repr(t.walk(JugglerTaskAllocate)))
698
                    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
699
                    # ical does not support resource allocation reporting
700
                    # so we do not support multiple resource here
701
                    # and we don't support them yet anyways
702
                    t.set_property(JugglerBooking({
703
                        "resource":t.walk(JugglerTaskAllocate)[0].get_value(),
704
                        "start": start_date,
705
                        "end": end_date
706
                        }))
707
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
708
    def run(self, outfolder=None, infile=None):
709
        '''
710
        Run the taskjuggler task
711
712
        Args:
713
            output (str): Name of output file, for task-juggler
714
        '''
715
        if not self.src:
716
            self.juggle()
717
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
718
        if outfolder is None:
719
            outfolder = tempfile.mkdtemp("TJP")
720
        self.outfolder = outfolder
0 ignored issues
show
Coding Style introduced by
The attribute outfolder was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
721
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
722
        if infile is None:
723
            infile = tempfile.mkstemp(".tjp")[1]
724
        self.infile = infile
0 ignored issues
show
Coding Style introduced by
The attribute infile was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
725
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
726
        reportdir = self.src.walk(JugglerOutputdir)
727
        orig_rep = reportdir[0].get_value()
728
        reportdir[0].set_value(outfolder)
729
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
730
        ical_report_name = "calendar_out"
731
        ical_report_path = os.path.join(outfolder, ical_report_name)
732
        icalreport = self.src.walk(JugglerIcalreport)
733
        orig_cal = icalreport[0].get_value()
734
        icalreport[0].set_value(ical_report_name)
735
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
736
        self.write_file(infile)
737
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
738
        subprocess.call(["/usr/bin/env", "tj3", infile])
739
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
740
        self.read_ical_result(ical_report_path+".ics")
741
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
742
        # shutil.rmtree(self.outfolder)
743
        # os.remove(self.infile)
744
        icalreport[0].set_value(orig_cal)
745
        reportdir[0].set_value(orig_rep)
746
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
747
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
748
        # TODO HERE: load the ical file back to the actual tree (no tree support yet?)
749
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
750
    def clean(self):
751
        "clean after running"
752
        # try: shutil.rmtree(self.outfolder)
753
        # except:  pass
754
        # try: os.remove(self.infile)
755
        # except: pass
756
        raise NotImplementedError
757
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
758
    def walk(self, cls):
759
        return self.src.walk(cls)
760
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
761
    def __inter__(self):
762
        "provide dict(j) method to generate dictionary structure for tasks"
763
        raise NotImplementedError
764
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
765
    def __del__(self):
766
        self.clean()
767
768
0 ignored issues
show
coding-style introduced by
Trailing newlines
Loading history...
769