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 ( ec954c...ff6a96 )
by Andrew
01:24
created

JugglerWorkingHours   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 21
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 21
rs 10
wmc 5

4 Methods

Rating   Name   Duplication   Size   Complexity  
A set_id() 0 3 2
A load_default_properties() 0 2 1
A set_hour_interval() 0 2 1
A set_weekday() 0 6 1
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
standard import "import logging, tempfile, subprocess, datetime, icalendar, shutil, os" should be placed before "import logging, tempfile, subprocess, datetime, icalendar, shutil, os"
Loading history...
10
from collections import OrderedDict
0 ignored issues
show
introduced by
standard import "from collections import OrderedDict" should be placed before "import logging, tempfile, subprocess, datetime, icalendar, shutil, os"
Loading history...
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
Coding Style introduced by
Trailing whitespace
Loading history...
38
def to_tj3time(dt):
39
    """
40
    Convert python date or datetime object to TJ3 format
41
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
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):
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.load_default_properties(issue)
98
99
        if issue:
100
            if self.load_from_issue(issue) is False:
101
                self.empty = True
102
103
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
104
        pass
105
106
    def load_from_issue(self, issue):
107
        '''
108
        Load the object with data from a generic issue
109
110
        Args:
111
            issue (class): The generic issue to load from
112
        '''
113
        pass
114
115
    def get_name(self):
116
        '''
117
        Get name for task juggler property
118
119
        Returns:
120
            str: Name of the task juggler property
121
        '''
122
        return self.name
123
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
124
    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...
125
        return ""
126
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
127
    def get_hash(self):
128
        return self.get_name() + repr(self.get_value())
129
130
    def set_value(self, value):
131
        '''
132
        Set value for task juggler property
133
134
        Args:
135
            value (object): New value of the property
136
        '''
137
        self.value = value
138
139
    def append_value(self, value):
140
        '''
141
        Append value for task juggler property
142
143
        Args:
144
            value (object): Value to append to the property
145
        '''
146
        self.value.append(value)
147
148
    def get_value(self):
149
        '''
150
        Get value for task juggler property
151
152
        Returns:
153
            str: Value of the task juggler property
154
        '''
155
        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...
156
        return self.value
157
158
    def validate(self, task, tasks):
159
        '''
160
        Validate (and correct) the current task property
161
162
        Args:
163
            task (JugglerTask): Task to which the property belongs
164
            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...
165
                                verify relations to other tasks.
166
        '''
167
        pass
168
169
    def __str__(self):
170
        '''
171
        Convert task property object to the task juggler syntax
172
173
        Returns:
174
            str: String representation of the task property in juggler syntax
175
        '''
176
177
        if self.get_value(): 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
178
            # TODO: list support (like allocate multiple) (copy from taskdepends)
179
            # TODO: identifier conversion support?
180
            return self.TEMPLATE.format(prop=self.get_name(),
181
                                        value=self.VALUE_TEMPLATE.format(prefix=self.PREFIX,
182
                                                                         value=self.get_value(),
183
                                                                         suffix=self.SUFFIX))
184
        return ''
185
186
class JugglerTaskAllocate(JugglerTaskProperty):
187
    '''Class for the allocate (assignee) of a juggler task'''
188
189
    DEFAULT_NAME = 'allocate'
190
    DEFAULT_VALUE = 'not initialized'
191
192
    def load_from_issue(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
193
        '''
194
        Load the object with data from a generic issue
195
196
        Args:
197
            issue (class): The generic issue to load from
198
        '''
199
        self.set_value(self.DEFAULT_VALUE)
200
        if not issue is None:
201
            self.set_value(issue) # TODO: this may be list or primitive
202
203
class JugglerTaskPriority(JugglerTaskProperty):
204
    '''Class for task priority'''
205
    LOG_STRING = "JugglerTaskPriority"
206
    DEFAULT_NAME = "priority"
207
    DEFAULT_VALUE = 1000
208
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
209
    def get_hash(self):
210
        return self.get_name()
211
212
class JugglerTaskStart(JugglerTaskProperty):
213
    LOG_STRING = "JugglerTaskStart"
214
    DEFAULT_NAME = "start"
215
    DEFAULT_VALUE = ""
216
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
217
    def set_value(self, dt):
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'set_value' method
Loading history...
218
        if not dt: 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
219
            self.value = ""
220
            return
221
        if not isinstance(dt, datetime.datetime):
222
            raise ValueError("Task start value should be datetime object")
223
        self.value = dt
224
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
225
    def get_value(self):
226
        # TODO: fix API
227
        # WARNING: get_value returns tjp value
228
        if not self.value:
229
            return ""
230
        return to_tj3time(self.value)
231
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
232
    def get_hash(self):
233
        return self.get_name()
234
235
class JugglerTaskEffort(JugglerTaskProperty):
236
    '''Class for the effort (estimate) of a juggler task'''
237
238
    #For converting the seconds (generic) to days
239
    UNIT = 'h'
240
241
    DEFAULT_NAME = 'effort'
242
    MINIMAL_VALUE = 1 # TODO: should be project resolution, add check
243
    DEFAULT_VALUE = -1 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
244
    SUFFIX = UNIT
245
246
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
247
        self.SUFFIX = self.UNIT
248
249
    def load_from_issue(self, issue):
250
        '''
251
        Load the object with data from a generic issue
252
253
        Args:
254
            issue (class): The generic issue to load from
255
        '''
256
        if issue: self.set_value(issue)
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
257
        else: self.set_value(self.DEFAULT_VALUE)
258
    def set_value(self, value):
259
        '''
260
        Set the value for effort. Will convert whatever number to integer.
261
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
262
        Default class unit is 'days'. Can be overrided by setting "UNIT" global class attribute
263
        '''
264
        self.value = int(value) # TODO: support for FP?
265
    def get_hash(self):
266
        return self.get_name()
267
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
268
    def decode(self):
269
        return self.value # only hours supported yet
270
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
271
    def validate(self, task, tasks):
272
        '''
273
        Validate (and correct) the current task property
274
275
        Args:
276
            task (JugglerTask): Task to which the property belongs
277
            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...
278
                                verify relations to other tasks.
279
        '''
280
        pass
281
        # if self.get_value() < self.MINIMAL_VALUE:
282
        #     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...
283
        #     self.set_value(self.MINIMAL_VALUE)
284
285
class JugglerTaskDepends(JugglerTaskProperty):
286
    '''Class for the effort (estimate) of a juggler task'''
287
288
    DEFAULT_NAME = 'depends'
289
    DEFAULT_VALUE = []
290
    PREFIX = '!'
291
292
    def set_value(self, value):
293
        '''
294
        Set value for task juggler property (deep copy)
295
296
        Args:
297
            value (object): New value of the property
298
        '''
299
        self.value = list(value)
300
301
    def load_from_issue(self, issue):
302
        '''
303
        Load the object with data from a generic issue
304
305
        Args:
306
            issue (class): The generic issue to load from
307
        '''
308
        raise NotImplementedError("load_from_issue is not implemented for this depend")
309
        # TODO: remove these - are for jira
310
        self.set_value(self.DEFAULT_VALUE)
0 ignored issues
show
Unused Code introduced by
This code does not seem to be reachable.
Loading history...
311
        if hasattr(issue.fields, 'issuelinks'):
312
            for link in issue.fields.issuelinks:
313
                if hasattr(link, 'inwardIssue') and link.type.name == 'Blocker':
314
                    self.append_value(to_identifier(link.inwardIssue.key))
315
316
    def validate(self, task, tasks):
317
        '''
318
        Validate (and correct) the current task property
319
320
        Args:
321
            task (JugglerTask): Task to which the property belongs
322
            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...
323
                                verify relations to other tasks.
324
        '''
325
        # TODO: add support for nested tasks with self.parent
326
        for val in list(self.get_value()):
327
             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...
328
                 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...
329
                 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...
330
331
    def __str__(self):
332
        '''
333
        Convert task property object to the task juggler syntax
334
335
        Returns:
336
            str: String representation of the task property in juggler syntax
337
        '''
338
339
        if self.get_value():
340
            valstr = ''
341
            for val in self.get_value():
342
                if valstr:
343
                    valstr += ', '
344
                valstr += self.VALUE_TEMPLATE.format(prefix=self.PREFIX,
345
                                                     value=to_identifier(val),
346
                                                     suffix=self.SUFFIX)
347
            return self.TEMPLATE.format(prop=self.get_name(),
348
                                        value=valstr)
349
        return ''
350
351
# class NonEmptyObject(object):
352
#     def __init__(self):
353
#         self.empty = False
354
#     def __bool__(self):
355
#         return not self.empty
356
#     __nonzero__=__bool__
357
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
358
359
class JugglerCompoundKeyword(object):
360
361
    '''Class for a general compound object in TJ syntax'''
362
363
    COMMENTS_HEADER = ""
364
    LOG_STRING = "DefaultKeyword"
365
    DEFAULT_KEYWORD = 'unknown_keyword'
366
    DEFAULT_ID = "" # id may be empty for some keywords
367
    DEFAULT_SUMMARY = '' # no summary is possible everywhere
368
    TEMPLATE = '''{header}\n{keyword} {id}'''
369
    ENCLOSED_BLOCK = True
370
371
    def __init__(self, issue=None):
372
        logging.debug('Create %s', self.LOG_STRING)
373
        self.empty = False
374
        self.parent = None
375
        self.keyword = self.DEFAULT_KEYWORD
376
        self.id = self.DEFAULT_ID
377
        self.summary = self.DEFAULT_SUMMARY
378
        self.option2 = ""
379
        self.properties = OrderedDict()
380
        self.load_default_properties(issue)
381
382
        if issue:
383
            if self.load_from_issue(issue) is False:
384
                self.empty = True
385
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
386
    def load_from_issue(self, issue):
387
        '''
388
        Load the object with data from a generic issue
389
390
        Args:
391
            issue (class): The generic issue to load from
392
        '''
393
        pass
394
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
395
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
396
        pass
397
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
398
    def get_name(self):
399
        return self.keyword
400
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
401
    def get_id(self):
402
        return self.id
403
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
404
    def get_hash(self):
405
        """Used to generate unique hash. 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
406
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
407
        If set_property should replace this (only single property of this type is supported) - 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
408
            - the hash should only return the keyword
409
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
410
        By default, multiple compound properties are allowed.
411
        """
412
        return self.get_name() + repr(self.get_id())
413
414
    def set_property(self, prop):
415
        if prop: 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
416
            self.properties[prop.get_hash()] = prop
417
            prop.parent = self # TODO: control un-set?, GC?
418
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
419
    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...
420
        self.id = id
421
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
422
    def decode(self):
423
        return self.option2
424
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
425
    def walk(self, cls, ls = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
426
        if ls is None: ls = []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
427
        for key, item in self.properties.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
428
            if isinstance(item, JugglerCompoundKeyword):
429
                ls = item.walk(cls, ls)
430
            if isinstance(item, cls):
431
                ls.append(item)
432
        return ls
433
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
434
    def __str__(self):
435
        if self.empty: return ""
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
436
        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...
437
        if self.summary:
438
            out += ' "%s"' % self.summary.replace('\"', '\\\"')
439
        if self.option2:
440
            out += ' %s ' % self.option2
441
        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...
442
        for prop in self.properties:
443
            out += str(self.properties[prop])
444
        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...
445
        return out
446
447
class JugglerSimpleProperty(JugglerCompoundKeyword):
448
    """By default only one simple property is allowed."""
449
    LOG_STRING = "Default Simple Property"
450
    DEFAULT_NAME = 'unknown_property'
451
    DEFAULT_VALUE = ''
452
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
453
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
454
        self.keyword = self.DEFAULT_NAME
455
        self.set_value(self.DEFAULT_VALUE)
456
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
457
    def load_from_issue(self, value):
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'load_from_issue' method
Loading history...
458
        self.set_value(value)
459
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
460
    def get_name(self):
461
        return self.keyword
462
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
463
    def get_hash(self):
464
        return self.get_name()
465
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
466
    def decode(self):
467
        return self.id
468
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
469
    def get_value(self):
470
        return self.id
471
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
472
    def set_value(self, val):
473
        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...
474
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
475
class JugglerTimezone(JugglerSimpleProperty):
476
    '''
477
    Sets the project timezone.
478
    Default value is UTC.
479
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
480
    Supports all tzdata values, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
481
    or https://stackoverflow.com/q/13866926
482
    '''
483
    DEFAULT_NAME = 'timezone'
484
    DEFAULT_VALUE = 'UTC'
485
    # DEFAULT_VALUE = 'Europe/Dublin'
486
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
487
    # TODO: checks!
488
489
class JugglerOutputdir(JugglerSimpleProperty):
490
    LOG_STRING = "outputdir property"
491
    DEFAULT_NAME = 'outputdir'
492
    DEFAULT_VALUE = 'REPORT'
493
    # TODO HERE: need to create the outputdir folder for this to execute!
494
495
class JugglerIcalreport(JugglerSimpleProperty):
496
    LOG_STRING = "icalreport property"
497
    DEFAULT_NAME = 'icalreport'
498
    DEFAULT_VALUE = 'calendar'
499
500
class JugglerResource(JugglerCompoundKeyword):
501
    DEFAULT_KEYWORD = "resource"
502
    DEFAULT_ID = "me"
503
    DEFAULT_SUMMARY = "Default Resource"
504
505
class JugglerWorkingHours(JugglerCompoundKeyword):
506
    DEFAULT_KEYWORD = "workinghours"
507
    DEFAULT_ID = "mon"
508
    DEFAULT_SUMMARY = ""
509
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
510
    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...
511
        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
Coding Style introduced by
This line is too long as per the coding-style (175/100).

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

Loading history...
Coding Style introduced by
More than one statement on a single line
Loading history...
512
        self.id = id
513
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
514
    def set_weekday(self, d = "mon"):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
515
        """
516
        Set week day for workinghours
517
        "mon", "tue", "wed", "thu", "fri", "sat", "sun"
518
        """
519
        self.set_id(d)
520
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
521
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
522
        self.set_hour_interval()
523
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
524
    def set_hour_interval(self, start = 9, end = 19):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
525
        self.option2 = "%s:00 - %s:00" % (start, end)
526
527
class JugglerTask(JugglerCompoundKeyword):
528
529
    '''Class for a task for Task-Juggler'''
530
531
    LOG_STRING = "JugglerTask"
532
    DEFAULT_KEYWORD = 'task'
533
    DEFAULT_ID = "unknown_task"
534
    DEFAULT_SUMMARY = 'Task is not initialized'
535
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
536
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
537
        if not issue:
538
            self.set_property(JugglerTaskAllocate("me"))
539
            self.set_property(JugglerTaskEffort(1))
540
        else:
541
            self.set_property(JugglerTaskAllocate(issue))
542
            self.set_property(JugglerTaskEffort(issue))
543
            self.set_property(JugglerTaskDepends(issue))
544
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
545
    def load_from_issue(self, issue):
546
        '''
547
        Load the object with data from a generic issue
548
549
        Args:
550
            issue (?): The generic issue to load from
551
        '''
552
        self.load_default_properties(issue)
553
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
554
    def validate(self, tasks):
555
        '''
556
        Validate (and correct) the current task
557
558
        Args:
559
            tasks (list): List of JugglerTask's to which the current task belongs. Will be used to
560
                          verify relations to other tasks.
561
        '''
562
        if self.id == self.DEFAULT_ID:
563
            logging.error('Found a task which is not initialized')
564
565
        for prop in self.properties:
566
            self.properties[prop].validate(self, tasks)
567
568
    # def __str__(self):
569
    #     '''
570
    #     Convert task object to the task juggler syntax
571
572
    #     Returns:
573
    #         str: String representation of the task in juggler syntax
574
    #     '''
575
    #     props = ''
576
    #     for prop in self.properties:
577
    #         props += str(self.properties[prop])
578
    #     return self.TEMPLATE.format(id=to_identifier(self.key),
579
    #                                 key=self.key,
580
    #                                 description=self.summary.replace('\"', '\\\"'),
581
    #                                 props=props)
582
583
class JugglerTimesheet():
584
    pass
585
586
class JugglerBooking(JugglerCompoundKeyword):
587
    LOG_STRING = "JugglerBooking"
588
    DEFAULT_KEYWORD = "booking"
589
    DEFAULT_ID = "me" # resource
590
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
591
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
592
        self.start = None
593
        self.end = None
594
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
595
    def set_resource(self, res):
596
        self.set_id(res)
597
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
598
    def set_interval(self, start, end):
599
        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...
600
        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...
601
        self.option2 = to_tj3interval(self.start, self.end)
602
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
603
    def decode(self):
604
        return [self.start, self.end]
605
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
606
    def load_from_issue(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
607
        start = issue["start"]
608
        end = issue["end"]
609
        self.set_interval(start, end)
610
        self.set_resource(issue["resource"])
611
612
class JugglerProject(JugglerCompoundKeyword):
613
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
614
    '''Template for TaskJuggler project'''
615
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
616
    LOG_STRING = "JugglerProject"
617
    DEFAULT_KEYWORD = 'project'
618
    DEFAULT_ID = "default" # id may be empty for some keywords
619
    DEFAULT_SUMMARY = 'Default Project' # no summary is possible everywhere
620
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
621
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
622
        self.set_property(JugglerTimezone())
623
        self.set_property(JugglerOutputdir())
624
        self.set_interval()
625
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
626
    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
Coding Style introduced by
This line is too long as per the coding-style (138/100).

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

Loading history...
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Coding Style introduced by
Exactly one space required after comma
Loading history...
627
        self.option2 = to_tj3interval(start, end)
628
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
629
630
class JugglerSource(JugglerCompoundKeyword):
631
    """
632
    The entire project skeleton
633
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
634
    Sets reports and folders for use with parser
635
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
636
    Must be extended with load_from_issue(self,issue) appending tasks 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
637
    """
638
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
639
    LOG_STRING = "JugglerSource"
640
    DEFAULT_KEYWORD = ''
641
    DEFAULT_ID = ''
642
    COMMENTS_HEADER = '''
643
    // TaskJuggler 3 source
644
    // generated by python-juggler (c) 2017 Andrew Gryaznov and others
645
    // https://github.com/grandrew/taskjuggler-python
646
    '''
647
    ENCLOSED_BLOCK = False
648
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
649
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
650
        self.set_property(JugglerProject())
651
        self.set_property(JugglerResource())
652
        self.set_property(JugglerIcalreport())
653
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
654
class GenericJuggler(object):
655
656
    '''Class for task-juggling generic results'''
657
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
658
    src = None
659
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
660
    def __init__(self):
661
        '''
662
        Construct a generic juggler object
663
664
        Args:
665
            none
666
        '''
667
668
        logging.info('generic load')
669
670
    def set_query(self, query):
671
        '''
672
        Set the query for the generic juggler object
673
674
        Args:
675
            query (str): The Query to run on generic server
676
        '''
677
678
        logging.info('Query: %s', query)
679
        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...
680
        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...
681
682
    @staticmethod
683
    def validate_tasks(tasks):
684
        '''
685
        Validate (and correct) tasks
686
687
        Args:
688
            tasks (list): List of JugglerTask's to validate
689
        '''
690
        for task in tasks:
691
            task.validate(tasks)
692
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
693
    def load_issues(self):
694
        raise NotImplementedError
695
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
696
    def add_task(self, task):
697
        '''
698
        Add task to current project
699
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
700
        Args:
701
            task (JugglerTask): a task to add
702
        '''
703
        if not self.src:
704
            self.juggle()
705
        self.src.set_property(task)
706
707
    def load_issues_incremetal(self):
708
        if self.loaded: return []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
709
        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...
710
        return self.load_issues()
711
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
712
    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...
713
        return JugglerTask(issue)
714
715
    def load_issues_from_generic(self):
716
        '''
717
        Load issues from generic
718
719
        Returns:
720
            list: A list of dicts containing the generic tickets
721
        '''
722
        tasks = []
723
        busy = True
724
        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...
725
        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...
726
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
727
        while busy:
728
            try:
729
                issues = self.load_issues_incremetal()
730
            except NotImplementedError:
731
                logging.error('Loading Issues is not implemented in upstream library')
732
                return None
733
                
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
734
            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...
735
                logging.error('Could not get issue')
736
                return None
737
738
            if len(issues) <= 0:
739
                busy = False
740
741
            self.issue_count += len(issues)
742
743
            for issue in issues:
744
                logging.debug('Retrieved %s', repr(issue))
745
                tasks.append(self.create_task_instance(issue))
746
747
        self.validate_tasks(tasks)
748
749
        return tasks
750
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
751
    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...
752
        return JugglerSource()
753
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
754
    def juggle(self):
755
        """
756
        Export the loaded issues onto the JuggleSource structure
757
        """
758
        issues = self.load_issues_from_generic()
759
        if not issues:
760
            # return None
761
            issues = []
762
        self.src = self.create_jugglersource_instance()
763
        for issue in issues:
764
            self.src.set_property(issue)
765
        return self.src
766
767
    def write_file(self, output=None):
768
        '''
769
        Query generic and generate task-juggler output from given issues
770
771
        Args:
772
            output (str): Name of output file, for task-juggler
773
        '''
774
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
775
        if not self.src:
776
            self.juggle()
777
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
778
        s = str(self.src)
779
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
780
        if output and isinstance(output, str):
781
            with open(output, 'w') as out:
782
                out.write(s)
783
        elif output and isinstance(output, file):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
784
            output.write(s)
785
        # else:
786
        #     raise ValueError("output should be a filename string or a file handler")
787
        return s
788
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
789
    def read_ical_result(self, icalfile):
790
        tasks = self.src.walk(JugglerTask)
791
        cal = icalendar.Calendar.from_ical(file(icalfile).read())
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
792
        for ev in cal.walk('VEVENT'): # pylint:disable=no-member
793
            start_date = ev.decoded("DTSTART")
794
            end_date = ev.decoded("DTEND")
795
            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...
796
            for t in tasks:
797
                if t.id == id:
798
                    # logging.info("Testing %s" % str(t))
799
                    # logging.info("Got %s" % repr(t.walk(JugglerTaskAllocate)))
800
                    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
801
                    # ical does not support resource allocation reporting
802
                    # so we do not support multiple resource here
803
                    # and we don't support them yet anyways
804
                    t.set_property(JugglerBooking({
805
                        "resource":t.walk(JugglerTaskAllocate)[0].get_value(),
806
                        "start": start_date,
807
                        "end": end_date
808
                        }))
809
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
810
    def run(self, outfolder=None, infile=None):
811
        '''
812
        Run the taskjuggler task
813
814
        Args:
815
            output (str): Name of output file, for task-juggler
816
        '''
817
        if not self.src:
818
            self.juggle()
819
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
820
        if outfolder is None:
821
            outfolder = tempfile.mkdtemp("TJP")
822
        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...
823
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
824
        if infile is None:
825
            infile = tempfile.mkstemp(".tjp")[1]
826
        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...
827
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
828
        reportdir = self.src.walk(JugglerOutputdir)
829
        orig_rep = reportdir[0].get_value()
830
        reportdir[0].set_value(outfolder)
831
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
832
        ical_report_name = "calendar_out"
833
        ical_report_path = os.path.join(outfolder, ical_report_name)
834
        icalreport = self.src.walk(JugglerIcalreport)
835
        orig_cal = icalreport[0].get_value()
836
        icalreport[0].set_value(ical_report_name)
837
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
838
        self.write_file(infile)
839
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
840
        logging.debug("Running from %s to out %s" % (self.infile, self.outfolder))
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
841
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
842
        subprocess.call(["/usr/bin/env", "tj3", infile])
843
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
844
        self.read_ical_result(ical_report_path+".ics")
845
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
846
        icalreport[0].set_value(orig_cal)
847
        reportdir[0].set_value(orig_rep)
848
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
849
        if DEBUG or logging.getLogger().getEffectiveLevel() >= logging.DEBUG: return
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
850
        shutil.rmtree(self.outfolder)
851
        os.remove(self.infile)
852
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
853
        # TODO HERE: load the ical file back to the actual tree (no tree support yet?)
854
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
855
    def clean(self):
856
        "clean after running"
857
        if DEBUG or logging.getLogger().getEffectiveLevel() >= logging.DEBUG: return
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
858
        try: shutil.rmtree(self.outfolder)
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
859
        except:  pass
0 ignored issues
show
Coding Style introduced by
Exactly one space required after :
Loading history...
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...
Coding Style introduced by
More than one statement on a single line
Loading history...
860
        try: os.remove(self.infile)
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
861
        except: pass
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...
Coding Style introduced by
More than one statement on a single line
Loading history...
862
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
863
    def walk(self, cls):
864
        if not self.src:
865
            self.juggle()
866
        return self.src.walk(cls)
867
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
868
    def __inter__(self):
869
        "provide dict(j) method to generate dictionary structure for tasks"
870
        raise NotImplementedError
871
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
872
    def __del__(self):
873
        self.clean()
874
875
0 ignored issues
show
coding-style introduced by
Trailing newlines
Loading history...
876