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 ( 6df1c2...1a0021 )
by Andrew
59s
created

JugglerSimpleProperty.get_hash()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
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...
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
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)
265
    def get_hash(self):
266
        return self.get_name()
267
    def validate(self, task, tasks):
268
        '''
269
        Validate (and correct) the current task property
270
271
        Args:
272
            task (JugglerTask): Task to which the property belongs
273
            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...
274
                                verify relations to other tasks.
275
        '''
276
        pass
277
        # if self.get_value() < self.MINIMAL_VALUE:
278
        #     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...
279
        #     self.set_value(self.MINIMAL_VALUE)
280
281
class JugglerTaskDepends(JugglerTaskProperty):
282
    '''Class for the effort (estimate) of a juggler task'''
283
284
    DEFAULT_NAME = 'depends'
285
    DEFAULT_VALUE = []
286
    PREFIX = '!'
287
288
    def set_value(self, value):
289
        '''
290
        Set value for task juggler property (deep copy)
291
292
        Args:
293
            value (object): New value of the property
294
        '''
295
        self.value = list(value)
296
297
    def load_from_issue(self, issue):
298
        '''
299
        Load the object with data from a generic issue
300
301
        Args:
302
            issue (class): The generic issue to load from
303
        '''
304
        raise NotImplementedError("load_from_issue is not implemented for this depend")
305
        # TODO: remove these - are for jira
306
        self.set_value(self.DEFAULT_VALUE)
0 ignored issues
show
Unused Code introduced by
This code does not seem to be reachable.
Loading history...
307
        if hasattr(issue.fields, 'issuelinks'):
308
            for link in issue.fields.issuelinks:
309
                if hasattr(link, 'inwardIssue') and link.type.name == 'Blocker':
310
                    self.append_value(to_identifier(link.inwardIssue.key))
311
312
    def validate(self, task, tasks):
313
        '''
314
        Validate (and correct) the current task property
315
316
        Args:
317
            task (JugglerTask): Task to which the property belongs
318
            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...
319
                                verify relations to other tasks.
320
        '''
321
        # TODO: add support for nested tasks with self.parent
322
        for val in list(self.get_value()):
323
             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...
324
                 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...
325
                 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...
326
327
    def __str__(self):
328
        '''
329
        Convert task property object to the task juggler syntax
330
331
        Returns:
332
            str: String representation of the task property in juggler syntax
333
        '''
334
335
        if self.get_value():
336
            valstr = ''
337
            for val in self.get_value():
338
                if valstr:
339
                    valstr += ', '
340
                valstr += self.VALUE_TEMPLATE.format(prefix=self.PREFIX,
341
                                                     value=to_identifier(val),
342
                                                     suffix=self.SUFFIX)
343
            return self.TEMPLATE.format(prop=self.get_name(),
344
                                        value=valstr)
345
        return ''
346
347
# class NonEmptyObject(object):
348
#     def __init__(self):
349
#         self.empty = False
350
#     def __bool__(self):
351
#         return not self.empty
352
#     __nonzero__=__bool__
353
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
354
355
class JugglerCompoundKeyword(object):
356
357
    '''Class for a general compound object in TJ syntax'''
358
359
    COMMENTS_HEADER = ""
360
    LOG_STRING = "DefaultKeyword"
361
    DEFAULT_KEYWORD = 'unknown_keyword'
362
    DEFAULT_ID = "" # id may be empty for some keywords
363
    DEFAULT_SUMMARY = '' # no summary is possible everywhere
364
    TEMPLATE = '''{header}\n{keyword} {id}'''
365
    ENCLOSED_BLOCK = True
366
367
    def __init__(self, issue=None):
368
        logging.debug('Create %s', self.LOG_STRING)
369
        self.empty = False
370
        self.parent = None
371
        self.keyword = self.DEFAULT_KEYWORD
372
        self.id = self.DEFAULT_ID
373
        self.summary = self.DEFAULT_SUMMARY
374
        self.option2 = ""
375
        self.properties = OrderedDict()
376
        self.load_default_properties(issue)
377
378
        if issue:
379
            if self.load_from_issue(issue) is False:
380
                self.empty = True
381
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
382
    def load_from_issue(self, issue):
383
        '''
384
        Load the object with data from a generic issue
385
386
        Args:
387
            issue (class): The generic issue to load from
388
        '''
389
        pass
390
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
391
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
392
        pass
393
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
394
    def get_name(self):
395
        return self.keyword
396
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
397
    def get_id(self):
398
        return self.id
399
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
400
    def get_hash(self):
401
        """Used to generate unique hash. 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
402
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
403
        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...
404
            - the hash should only return the keyword
405
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
406
        By default, multiple compound properties are allowed.
407
        """
408
        return self.get_name() + repr(self.get_id())
409
410
    def set_property(self, prop):
411
        if prop: 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
412
            self.properties[prop.get_hash()] = prop
413
            prop.parent = self # TODO: control un-set?, GC?
414
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
415
    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...
416
        self.id = id
417
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
418
    def decode(self):
419
        return self.option2
420
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
421
    def walk(self, cls, ls = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
422
        if ls is None: ls = []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
423
        for key, item in self.properties.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
424
            if isinstance(item, JugglerCompoundKeyword):
425
                ls = item.walk(cls, ls)
426
            if isinstance(item, cls):
427
                ls.append(item)
428
        return ls
429
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
430
    def __str__(self):
431
        if self.empty: return ""
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
432
        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...
433
        if self.summary:
434
            out += ' "%s"' % self.summary.replace('\"', '\\\"')
435
        if self.option2:
436
            out += ' %s ' % self.option2
437
        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...
438
        for prop in self.properties:
439
            out += str(self.properties[prop])
440
        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...
441
        return out
442
443
class JugglerSimpleProperty(JugglerCompoundKeyword):
444
    """By default only one simple property is allowed."""
445
    LOG_STRING = "Default Simple Property"
446
    DEFAULT_NAME = 'unknown_property'
447
    DEFAULT_VALUE = ''
448
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
449
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
450
        self.keyword = self.DEFAULT_NAME
451
        self.set_value(self.DEFAULT_VALUE)
452
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
453
    def load_from_issue(self, value):
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'load_from_issue' method
Loading history...
454
        self.set_value(value)
455
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
456
    def get_name(self):
457
        return self.keyword
458
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
459
    def get_hash(self):
460
        return self.get_name()
461
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
462
    def decode(self):
463
        return self.id
464
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
465
    def get_value(self):
466
        return self.id
467
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
468
    def set_value(self, val):
469
        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...
470
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
471
class JugglerTimezone(JugglerSimpleProperty):
472
    '''
473
    Sets the project timezone.
474
    Default value is UTC.
475
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
476
    Supports all tzdata values, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
477
    or https://stackoverflow.com/q/13866926
478
    '''
479
    DEFAULT_NAME = 'timezone'
480
    DEFAULT_VALUE = 'UTC'
481
    # DEFAULT_VALUE = 'Europe/Dublin'
482
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
483
    # TODO: checks!
484
485
class JugglerOutputdir(JugglerSimpleProperty):
486
    LOG_STRING = "outputdir property"
487
    DEFAULT_NAME = 'outputdir'
488
    DEFAULT_VALUE = 'REPORT'
489
    # TODO HERE: need to create the outputdir folder for this to execute!
490
491
class JugglerIcalreport(JugglerSimpleProperty):
492
    LOG_STRING = "icalreport property"
493
    DEFAULT_NAME = 'icalreport'
494
    DEFAULT_VALUE = 'calendar'
495
496
class JugglerResource(JugglerCompoundKeyword):
497
    DEFAULT_KEYWORD = "resource"
498
    DEFAULT_ID = "me"
499
    DEFAULT_SUMMARY = "Default Resource"
500
501
class JugglerTask(JugglerCompoundKeyword):
502
503
    '''Class for a task for Task-Juggler'''
504
505
    LOG_STRING = "JugglerTask"
506
    DEFAULT_KEYWORD = 'task'
507
    DEFAULT_ID = "unknown_task"
508
    DEFAULT_SUMMARY = 'Task is not initialized'
509
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
510
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
511
        if not issue:
512
            self.set_property(JugglerTaskAllocate("me"))
513
            self.set_property(JugglerTaskEffort(1))
514
        else:
515
            self.set_property(JugglerTaskAllocate(issue))
516
            self.set_property(JugglerTaskEffort(issue))
517
            self.set_property(JugglerTaskDepends(issue))
518
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
519
    def load_from_issue(self, issue):
520
        '''
521
        Load the object with data from a generic issue
522
523
        Args:
524
            issue (?): The generic issue to load from
525
        '''
526
        self.load_default_properties(issue)
527
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
528
    def validate(self, tasks):
529
        '''
530
        Validate (and correct) the current task
531
532
        Args:
533
            tasks (list): List of JugglerTask's to which the current task belongs. Will be used to
534
                          verify relations to other tasks.
535
        '''
536
        if self.id == self.DEFAULT_ID:
537
            logging.error('Found a task which is not initialized')
538
539
        for prop in self.properties:
540
            self.properties[prop].validate(self, tasks)
541
542
    # def __str__(self):
543
    #     '''
544
    #     Convert task object to the task juggler syntax
545
546
    #     Returns:
547
    #         str: String representation of the task in juggler syntax
548
    #     '''
549
    #     props = ''
550
    #     for prop in self.properties:
551
    #         props += str(self.properties[prop])
552
    #     return self.TEMPLATE.format(id=to_identifier(self.key),
553
    #                                 key=self.key,
554
    #                                 description=self.summary.replace('\"', '\\\"'),
555
    #                                 props=props)
556
557
class JugglerTimesheet():
558
    pass
559
560
class JugglerBooking(JugglerCompoundKeyword):
561
    LOG_STRING = "JugglerBooking"
562
    DEFAULT_KEYWORD = "booking"
563
    DEFAULT_ID = "me" # resource
564
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
565
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
566
        self.start = None
567
        self.end = None
568
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
569
    def set_resource(self, res):
570
        self.set_id(res)
571
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
572
    def set_interval(self, start, end):
573
        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...
574
        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...
575
        self.option2 = to_tj3interval(self.start, self.end)
576
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
577
    def decode(self):
578
        return [self.start, self.end]
579
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
580
    def load_from_issue(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
581
        start = issue["start"]
582
        end = issue["end"]
583
        self.set_interval(start, end)
584
        self.set_resource(issue["resource"])
585
586
class JugglerProject(JugglerCompoundKeyword):
587
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
588
    '''Template for TaskJuggler project'''
589
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
590
    LOG_STRING = "JugglerProject"
591
    DEFAULT_KEYWORD = 'project'
592
    DEFAULT_ID = "default" # id may be empty for some keywords
593
    DEFAULT_SUMMARY = 'Default Project' # no summary is possible everywhere
594
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
595
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
596
        self.set_property(JugglerTimezone())
597
        self.set_property(JugglerOutputdir())
598
        self.set_interval()
599
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
600
    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...
601
        self.option2 = to_tj3interval(start, end)
602
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
603
604
class JugglerSource(JugglerCompoundKeyword):
605
    """
606
    The entire project skeleton
607
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
608
    Sets reports and folders for use with parser
609
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
610
    Must be extended with load_from_issue(self,issue) appending tasks 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
611
    """
612
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
613
    LOG_STRING = "JugglerSource"
614
    DEFAULT_KEYWORD = ''
615
    DEFAULT_ID = ''
616
    COMMENTS_HEADER = '''
617
    // TaskJuggler 3 source
618
    // generated by python-juggler (c) 2017 Andrew Gryaznov and others
619
    // https://github.com/grandrew/taskjuggler-python
620
    '''
621
    ENCLOSED_BLOCK = False
622
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
623
    def load_default_properties(self, issue = None):
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
624
        self.set_property(JugglerProject())
625
        self.set_property(JugglerResource())
626
        self.set_property(JugglerIcalreport())
627
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
628
class GenericJuggler(object):
629
630
    '''Class for task-juggling generic results'''
631
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
632
    src = None
633
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
634
    def __init__(self):
635
        '''
636
        Construct a generic juggler object
637
638
        Args:
639
            none
640
        '''
641
642
        logging.info('generic load')
643
644
    def set_query(self, query):
645
        '''
646
        Set the query for the generic juggler object
647
648
        Args:
649
            query (str): The Query to run on generic server
650
        '''
651
652
        logging.info('Query: %s', query)
653
        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...
654
        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...
655
656
    @staticmethod
657
    def validate_tasks(tasks):
658
        '''
659
        Validate (and correct) tasks
660
661
        Args:
662
            tasks (list): List of JugglerTask's to validate
663
        '''
664
        for task in tasks:
665
            task.validate(tasks)
666
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
667
    def load_issues(self):
668
        raise NotImplementedError
669
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
670
    def add_task(self, task):
671
        '''
672
        Add task to current project
673
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
674
        Args:
675
            task (JugglerTask): a task to add
676
        '''
677
        if not self.src:
678
            self.juggle()
679
        self.src.set_property(task)
680
681
    def load_issues_incremetal(self):
682
        if self.loaded: return []
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
683
        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...
684
        return self.load_issues()
685
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
686
    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...
687
        return JugglerTask(issue)
688
689
    def load_issues_from_generic(self):
690
        '''
691
        Load issues from generic
692
693
        Returns:
694
            list: A list of dicts containing the generic tickets
695
        '''
696
        tasks = []
697
        busy = True
698
        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...
699
        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...
700
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
701
        while busy:
702
            try:
703
                issues = self.load_issues_incremetal()
704
            except NotImplementedError:
705
                logging.error('Loading Issues is not implemented in upstream library')
706
                return None
707
                
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
708
            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...
709
                logging.error('Could not get issue')
710
                return None
711
712
            if len(issues) <= 0:
713
                busy = False
714
715
            self.issue_count += len(issues)
716
717
            for issue in issues:
718
                logging.debug('Retrieved %s', repr(issue))
719
                tasks.append(self.create_task_instance(issue))
720
721
        self.validate_tasks(tasks)
722
723
        return tasks
724
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
725
    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...
726
        return JugglerSource()
727
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
728
    def juggle(self):
729
        """
730
        Export the loaded issues onto the JuggleSource structure
731
        """
732
        issues = self.load_issues_from_generic()
733
        if not issues:
734
            # return None
735
            issues = []
736
        self.src = self.create_jugglersource_instance()
737
        for issue in issues:
738
            self.src.set_property(issue)
739
        return self.src
740
741
    def write_file(self, output=None):
742
        '''
743
        Query generic and generate task-juggler output from given issues
744
745
        Args:
746
            output (str): Name of output file, for task-juggler
747
        '''
748
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
749
        if not self.src:
750
            self.juggle()
751
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
752
        s = str(self.src)
753
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
754
        if output and isinstance(output, str):
755
            with open(output, 'w') as out:
756
                out.write(s)
757
        elif output and isinstance(output, file):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
758
            output.write(s)
759
        # else:
760
        #     raise ValueError("output should be a filename string or a file handler")
761
        return s
762
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
763
    def read_ical_result(self, icalfile):
764
        tasks = self.src.walk(JugglerTask)
765
        cal = icalendar.Calendar.from_ical(file(icalfile).read())
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'file'
Loading history...
766
        for ev in cal.walk('VEVENT'): # pylint:disable=no-member
767
            start_date = ev.decoded("DTSTART")
768
            end_date = ev.decoded("DTEND")
769
            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...
770
            for t in tasks:
771
                if t.id == id:
772
                    # logging.info("Testing %s" % str(t))
773
                    # logging.info("Got %s" % repr(t.walk(JugglerTaskAllocate)))
774
                    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
775
                    # ical does not support resource allocation reporting
776
                    # so we do not support multiple resource here
777
                    # and we don't support them yet anyways
778
                    t.set_property(JugglerBooking({
779
                        "resource":t.walk(JugglerTaskAllocate)[0].get_value(),
780
                        "start": start_date,
781
                        "end": end_date
782
                        }))
783
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
784
    def run(self, outfolder=None, infile=None):
785
        '''
786
        Run the taskjuggler task
787
788
        Args:
789
            output (str): Name of output file, for task-juggler
790
        '''
791
        if not self.src:
792
            self.juggle()
793
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
794
        if outfolder is None:
795
            outfolder = tempfile.mkdtemp("TJP")
796
        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...
797
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
798
        if infile is None:
799
            infile = tempfile.mkstemp(".tjp")[1]
800
        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...
801
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
802
        reportdir = self.src.walk(JugglerOutputdir)
803
        orig_rep = reportdir[0].get_value()
804
        reportdir[0].set_value(outfolder)
805
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
806
        ical_report_name = "calendar_out"
807
        ical_report_path = os.path.join(outfolder, ical_report_name)
808
        icalreport = self.src.walk(JugglerIcalreport)
809
        orig_cal = icalreport[0].get_value()
810
        icalreport[0].set_value(ical_report_name)
811
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
812
        self.write_file(infile)
813
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
814
        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...
815
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
816
        subprocess.call(["/usr/bin/env", "tj3", infile])
817
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
818
        self.read_ical_result(ical_report_path+".ics")
819
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
820
        icalreport[0].set_value(orig_cal)
821
        reportdir[0].set_value(orig_rep)
822
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
823
        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...
824
        shutil.rmtree(self.outfolder)
825
        os.remove(self.infile)
826
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
827
        # TODO HERE: load the ical file back to the actual tree (no tree support yet?)
828
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
829
    def clean(self):
830
        "clean after running"
831
        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...
832
        try: shutil.rmtree(self.outfolder)
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
833
        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...
834
        try: os.remove(self.infile)
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
835
        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...
836
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
837
    def walk(self, cls):
838
        if not self.src:
839
            self.juggle()
840
        return self.src.walk(cls)
841
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
842
    def __inter__(self):
843
        "provide dict(j) method to generate dictionary structure for tasks"
844
        raise NotImplementedError
845
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
846
    def __del__(self):
847
        self.clean()
848
849
0 ignored issues
show
coding-style introduced by
Trailing newlines
Loading history...
850