#@+leo-ver=4 #@+node:@file task.py #@@language python #@<< Copyright >> #@+node:<< Copyright >> ############################################################################ # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH # mreithinger@web.de # # This file is part of faces. # # faces is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # faces is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the # Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ############################################################################ #@-node:<< Copyright >> #@nl """ This module contains all classes for project plan objects """ #@<< Imports >> #@+node:<< Imports >> import pcalendar import resource import types import sys import datetime import operator as op import warnings import locale import weakref import opcode import new try: set except NameError: from sets import Set as set #@-node:<< Imports >> #@nl _is_source = True STRICT = 3 SLOPPY = 2 SMART = 1 #@+others #@+node:Exceptions #@+node:class AttributeError class AttributeError(AttributeError): #@ << class AttributeError declarations >> #@+node:<< class AttributeError declarations >> is_frozen = False #@-node:<< class AttributeError declarations >> #@nl #@-node:class AttributeError #@+node:class RecursionError class RecursionError(Exception): """This exception is raised in cas of cirular dependencies within an project""" #@ << class RecursionError declarations >> #@+node:<< class RecursionError declarations >> pass #@-node:<< class RecursionError declarations >> #@nl #@-node:class RecursionError #@+node:class _IncompleteError class _IncompleteError(Exception): """This exception is raised, when there is not enough data specified to calculate as task""" #@ @+others #@+node:__init__ def __init__(self, *args): if isinstance(args[0], (basestring)): Exception.__init__(self, *args) else: Exception.__init__(self, "Not enough data for calculating task, "\ "maybe you have a recursive reference.", *args) #@-node:__init__ #@-others #@-node:class _IncompleteError #@-node:Exceptions #@+node:Proxies for self referencing #@+node:class _MeProxy class _MeProxy(object): """ A Proxy class for the me attribute of tasks in the compile case """ #@ << declarations >> #@+node:<< declarations >> __slots__ = "task" #@-node:<< declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, task): object.__setattr__(self, "task", task) #@-node:__init__ #@+node:__getattr__ def __getattr__(self, name): if self.task._is_frozen: return getattr(self.task, name) if name in ("name", "up", "root", "path", "depth", "index", "calendar", "children", "resource", "balance"): return getattr(self.task, name) value = self.task.__dict__.get(name, _NEVER_USED_) def make_val(default): if value is _NEVER_USED_: return default return value if name in ("start", "end"): return self.task._to_start(make_val("1.1.2006")) if name in ("length", "effort", "duration", "todo", "done", "buffer", "performed", "performed_effort", "performed_end", "performed_start", "performed_work_time" ): return self.task._to_delta(make_val("0d")) if name in ("complete", "priority", "efficiency"): return make_val(0) if value is _NEVER_USED_: raise AttributeError("'%s' is not a valid attribute." % (name)) return value #@-node:__getattr__ #@+node:__setattr__ def __setattr__(self, name, value): self.task._set_attrib(name, value) #@-node:__setattr__ #@+node:__iter__ def __iter__(self): return iter(self.task) #@nonl #@-node:__iter__ #@+node:add_attrib def add_attrib(self, name_or_iter, val=None): if not isinstance(name_or_iter, str): for n, v in name_or_iter: setattr(self, n, v) else: setattr(self, name_or_iter, val) #@-node:add_attrib #@-others #@nonl #@-node:class _MeProxy #@+node:class _MeProxyRecalc class _MeProxyRecalc(_MeProxy): """ A Proxy class for the me attribute of tasks in the recalc case """ #@ @+others #@+node:__setattr__ def __setattr__(self, name, value): if self.task._properties.has_key(name): self.task._set_attrib(name, value) #@-node:__setattr__ #@-others #@-node:class _MeProxyRecalc #@+node:class _MeProxyError class _MeProxyError(_MeProxy): #@ << declarations >> #@+node:<< declarations >> __slots__ = ("task", "attrib", "exc") #@-node:<< declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, task, attrib, exc): _MeProxy.__init__(self, task) object.__setattr__(self, "attrib", attrib) object.__setattr__(self, "exc", exc) #@-node:__init__ #@+node:__setattr__ def __setattr__(self, name, value): if name == self.attrib or not self.attrib: raise self.exc #@-node:__setattr__ #@-others #@-node:class _MeProxyError #@+node:class _MeProxyWarn class _MeProxyWarn(_MeProxy): #@ << declarations >> #@+node:<< declarations >> __slots__ = ("task", "attrib", "message") #@-node:<< declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, task, attrib, message): _MeProxy.__init__(self, task) object.__setattr__(self, "attrib", attrib) object.__setattr__(self, "message", message) #@-node:__init__ #@+node:__setattr__ def __setattr__(self, name, value): if name == self.attrib or not self.attrib: warnings.warn(self.message, RuntimeWarning, 2) if not self.attrib: #warn only one time! object.__setattr__(self, "attrib", 1) #@-node:__setattr__ #@-others #@-node:class _MeProxyWarn #@-node:Proxies for self referencing #@+node:Task instrumentation #@+doc # This section contains code for byte code instrumenting # the task functions #@-doc #@nonl #@+node:_int_to_arg def _int_to_arg(value): return value % 256, value / 256 #@-node:_int_to_arg #@+node:_correct_labels def _correct_labels(old_code, new_code): #@ << localize dot variables >> #@+node:<< localize dot variables >> hasjrel = opcode.hasjrel hasjabs = opcode.hasjabs HAVE_ARGUMENT = opcode.HAVE_ARGUMENT #@nonl #@-node:<< localize dot variables >> #@nl #@ << loop initialization >> #@+node:<< loop initialization >> labels = {} old_new_map = {} # map old code offset to new code offset n = len(old_code) i = 0 j = 0 #@nonl #@-node:<< loop initialization >> #@nl while i < n: op = old_code[i] nop = new_code[j] old_new_map[i] = j i = i + 1 j = j + 1 if op >= HAVE_ARGUMENT: oparg = old_code[i] + old_code[i + 1] * 256 i = i + 2 j = j + 2 if nop != op: j += 3 # skip the 3 addition opcodes for attrib access else: #@ << add label if necessary >> #@+node:<< add label if necessary >> label = -1 if op in hasjrel: label = i + oparg elif op in hasjabs: label = oparg if label >= 0: labels[i] = label #@nonl #@-node:<< add label if necessary >> #@nl for offset, label in labels.iteritems(): new_offset = old_new_map[offset] new_label = old_new_map[label] op = new_code[new_offset - 3] #change jump arguments if op in hasjrel: jump = _int_to_arg(new_label - new_offset) new_code[new_offset - 2:new_offset] = jump elif op in hasjabs: new_code[new_offset - 2:new_offset] = _int_to_arg(new_label) #@nonl #@-node:_correct_labels #@+node:_instrument def _instrument(func): #@ << localize dot variables >> #@+node:<< localize dot variables >> opname = opcode.opname opmap = opcode.opmap jumps = opcode.hasjrel + opcode.hasjabs HAVE_ARGUMENT = opcode.HAVE_ARGUMENT co = func.func_code local_names = co.co_varnames all_names = list(co.co_names) global_names = set() #@-node:<< localize dot variables >> #@nl #@ << define local functions list_to_dict and is_local >> #@+node:<< define local functions list_to_dict and is_local >> def list_to_dict(l): return dict([(t[1], t[0]) for t in enumerate(l)]) def is_local(name): return name[0] == "_" and name != "__constraint__" #@nonl #@-node:<< define local functions list_to_dict and is_local >> #@nl #convert code #@ << loop initialization >> #@+node:<< loop initialization >> # all_name_map maps names to the all_names index # (same like all_names.index()) all_name_map = list_to_dict(all_names) if not all_name_map.has_key("me"): all_name_map["me"] = len(all_names) all_names.append("me") # for ln in local_names: if not all_name_map.has_key(ln): all_name_map[ln] = len(all_names) all_names.append(ln) # new_local_names = filter(is_local, local_names) new_local_name_map = list_to_dict(new_local_names) me_arg = _int_to_arg(all_name_map["me"]) old_lnotab = map(ord, co.co_lnotab) new_lnotab = [] tab_pos = 0 try: next_tab_point = old_lnotab[0] except IndexError: next_tab_point = None last_tab_point = 0 code = map(ord, co.co_code) new_code = [] has_labels = False n = len(code) i = 0 #@nonl #@-node:<< loop initialization >> #@nl while i < n: if i == next_tab_point: #@ << calculate new tab point >> #@+node:<< calculate new tab point >> increment = len(new_code) - last_tab_point new_lnotab.extend((increment, old_lnotab[tab_pos + 1])) tab_pos += 2 try: next_tab_point = i + old_lnotab[tab_pos] last_tab_point = len(new_code) except IndexError: next_tab_point = -1 #@nonl #@-node:<< calculate new tab point >> #@nl op = code[i] i += 1 if op >= HAVE_ARGUMENT: #@ << calculate argument >> #@+node:<< calculate argument >> arg0 = code[i] arg1 = code[i+1] oparg = arg0 + arg1 * 256 #@nonl #@-node:<< calculate argument >> #@nl i += 2 if opname[op] == "LOAD_GLOBAL": global_names.add(oparg) elif opname[op] == "STORE_FAST": #@ << change "store fast" to "store attribute" >> #@+node:<< change "store fast" to "store attribute" >> name = local_names[oparg] if not is_local(name): new_code.append(opmap["LOAD_GLOBAL"]) new_code.extend(me_arg) op = opmap["STORE_ATTR"] arg0, arg1 = _int_to_arg(all_name_map[name]) else: arg0, arg1 = _int_to_arg(new_local_name_map[name]) #@nonl #@-node:<< change "store fast" to "store attribute" >> #@nl elif opname[op] == "LOAD_FAST": #@ << change "load fast" to "load attribute" >> #@+node:<< change "load fast" to "load attribute" >> name = local_names[oparg] if not is_local(name): new_code.append(opmap["LOAD_GLOBAL"]) new_code.extend(me_arg) op = opmap["LOAD_ATTR"] arg0, arg1 = _int_to_arg(all_name_map[name]) else: arg0, arg1 = _int_to_arg(new_local_name_map[name]) #@nonl #@-node:<< change "load fast" to "load attribute" >> #@nl elif op in jumps: has_labels = True new_code.extend((op, arg0, arg1)) else: new_code.append(op) if has_labels: _correct_labels(code, new_code) #@ << create new code and function objects and return >> #@+node:<< create new code and function objects and return >> new_code = "".join(map(chr, new_code)) new_lnotab = "".join(map(chr, new_lnotab)) new_co = new.code(co.co_argcount, len(new_local_names), max(co.co_stacksize, 2), co.co_flags, new_code, co.co_consts, tuple(all_names), tuple(new_local_names), co.co_filename, co.co_name, co.co_firstlineno, new_lnotab, co.co_freevars, co.co_cellvars) func = new.function(new_co, func.func_globals, func.func_name, func.func_defaults, func.func_closure) func.global_names = tuple([all_names[index] for index in global_names]) return func #@nonl #@-node:<< create new code and function objects and return >> #@nl #@nonl #@-node:_instrument #@-node:Task instrumentation #@+node:Wrappers #@+node:class _Path class _Path(object): """ This class represents an instrumented path, to a task. If it points to an attribute of a task, it not only returns the value of the attribute. You can also find out the source attribute (task and attribute name) of the value. """ #@ @+others #@+node:__init__ def __init__(self, task, path_str): self._task = task self._path_str = path_str #@-node:__init__ #@+node:__getattr__ def __getattr__(self, name): new = getattr(self._task, name) if isinstance(new, Task): return _Path(new, self._path_str + "." + name) return _ValueWrapper(new, [(self._task, name)]) #@-node:__getattr__ #@+node:__str__ def __str__(self): return self._path_str #@-node:__str__ #@+node:__iter__ def __iter__(self): return iter(self._task) #@nonl #@-node:__iter__ #@-others #@-node:class _Path #@+node:_val #helper functions for _ValueWrapper #---------------------------------- def _val(val): if isinstance(val, _ValueWrapper): return val._value return val #@-node:_val #@+node:_ref def _ref(val): if isinstance(val, _ValueWrapper): return val._ref return [] #@-node:_ref #@+node:_sref def _sref(val, ref): if isinstance(val, _ValueWrapper): val._ref = ref #@nonl #@-node:_sref #@+node:_refsum def _refsum(refs): return reduce(lambda a, b: a + b, refs, []) #@nonl #@-node:_refsum #@+node:class _ValueWrapper class _ValueWrapper(object): """ This class represents a value, of a task attribute or a return value of a task method. It contains the value, and the supplier of that value """ #@ @+others #@+node:__init__ def __init__(self, value, ref): self._value = value self._ref = ref #@-node:__init__ #@+node:unicode def unicode(self, *args): if isinstance(self._value, str): return unicode(self._value, *args) return unicode(self._value) #@nonl #@-node:unicode #@+node:_vw def _vw(self, operand, *args): refs = _refsum(map(_ref, args)) vals = map(_val, args) result = operand(*vals) return self.__class__(result, refs) #@-node:_vw #@+node:_cmp def _cmp(self, operand, *args): refs = _refsum(map(_ref, args)) vals = map(_val, args) result = operand(*vals) map(lambda a: _sref(a, refs), args) return result #@-node:_cmp #@+node:__getattr__ def __getattr__(self, name): return getattr(self._value, name) #@-node:__getattr__ #@+node:__getitem__ def __getitem__(self, slice): return self.__class__(self._value[slice], self._ref) #@nonl #@-node:__getitem__ #@+node:__str__ def __str__(self): return str(self._value) #@-node:__str__ #@+node:__unicode__ def __unicode__(self): return unicode(self._value) #@nonl #@-node:__unicode__ #@+node:__repr__ def __repr__(self): return repr(self._value) #@-node:__repr__ #@+node:__nonzero__ def __nonzero__(self): return bool(self._value) #@-node:__nonzero__ #@+node:__lt__ def __lt__(self, other): return self._cmp(op.lt, self, other) #@-node:__lt__ #@+node:__le__ def __le__(self, other): return self._cmp(op.le, self, other) #@-node:__le__ #@+node:__eq__ def __eq__(self, other): return self._cmp(op.eq, self, other) #@-node:__eq__ #@+node:__ne__ def __ne__(self, other): return self._cmp(op.ne, self, other) #@-node:__ne__ #@+node:__gt__ def __gt__(self, other): return self._cmp(op.gt, self, other) #@-node:__gt__ #@+node:__ge__ def __ge__(self, other): return self._cmp(op.ge, self, other) #@-node:__ge__ #@+node:__add__ def __add__(self, other): return self._vw(op.add, self, other) #@nonl #@-node:__add__ #@+node:__sub__ def __sub__(self, other): return self._vw(op.sub, self, other) #@-node:__sub__ #@+node:__mul__ def __mul__(self, other): return self._vw(op.mul, self, other) #@-node:__mul__ #@+node:__floordiv__ def __floordiv__(self, other): return self._vw(op.floordiv, self, other) #@-node:__floordiv__ #@+node:__mod__ def __mod__(self, other): return self._vw(op.mod, self, other) #@-node:__mod__ #@+node:__divmod__ def __divmod__(self, other): return self._vw(op.divmod, self, other) #@-node:__divmod__ #@+node:__pow__ def __pow__(self, other): return self._vw(op.pow, self, other) #@-node:__pow__ #@+node:__lshift__ def __lshift__(self, other): return self._vw(op.lshift, self, other) #@-node:__lshift__ #@+node:__rshift__ def __rshift__(self, other): return self._vw(op.rshift, self, other) #@-node:__rshift__ #@+node:__and__ def __and__(self, other): return self._vw(op.and_, self, other) #@-node:__and__ #@+node:__xor__ def __xor__(self, other): return self._vw(op.xor, self, other) #@-node:__xor__ #@+node:__or__ def __or__(self, other): return self._vw(op.or_, self, other) #@-node:__or__ #@+node:__div__ def __div__(self, other): return self._vw(op.div, self, other) #@-node:__div__ #@+node:__radd__ def __radd__(self, other): return self._vw(op.add, other, self) #@-node:__radd__ #@+node:__rsub__ def __rsub__(self, other): return self._vw(op.sub, other, self) #@-node:__rsub__ #@+node:__rmul__ def __rmul__(self, other): return self._vw(op.mul, other, self) #@-node:__rmul__ #@+node:__rdiv__ def __rdiv__(self, other): return self._vw(op.div, other, self) #@-node:__rdiv__ #@+node:__rtruediv__ def __rtruediv__(self, other): return self._vw(op.truediv, other, self) #@-node:__rtruediv__ #@+node:__rfloordiv__ def __rfloordiv__(self, other): return self._vw(op.floordiv, other, self) #@-node:__rfloordiv__ #@+node:__rmod__ def __rmod__(self, other): return self._vw(op.mod, other, self) #@-node:__rmod__ #@+node:__rdivmod__ def __rdivmod__(self, other): return self._vw(op.divmod, other, self) #@-node:__rdivmod__ #@+node:__rpow__ def __rpow__(self, other): return self._vw(op.pow, other, self) #@-node:__rpow__ #@+node:__rlshift__ def __rlshift__(self, other): return self._vw(op.lshift, other, self) #@-node:__rlshift__ #@+node:__rrshift__ def __rrshift__(self, other): return self._vw(op.rshift, other, self) #@-node:__rrshift__ #@+node:__rand__ def __rand__(self, other): return self._vw(op.and_, other, self) #@-node:__rand__ #@+node:__rxor__ def __rxor__(self, other): return self._vw(op.xor, other, self) #@-node:__rxor__ #@+node:__ror__ def __ror__(self, other): return self._vw(op.or_, other, self) #@-node:__ror__ #@+node:__int__ def __int__(self): return int(self._value) #@-node:__int__ #@+node:__long__ def __long__(self): return long(self._value) #@-node:__long__ #@+node:__float__ def __float__(self): return float(self._value) #@-node:__float__ #@+node:__len__ def __len__(self): return len(self._value) #@-node:__len__ #@+node:__iter__ def __iter__(self): return iter(self._value) #@-node:__iter__ #@+node:__hash__ def __hash__(self): return hash(self._value) #@-node:__hash__ #@-others #@-node:class _ValueWrapper #@-node:Wrappers #@+node:Utilities #@+node:class _NEVER_USED_ class _NEVER_USED_: pass #@-node:class _NEVER_USED_ #@+node:class _StringConverter class _StringConverter(object): """This class is a helper for the to_string mechanism of tasks""" #@ @+others #@+node:__init__ def __init__(self, source, format=None): self.source = source self.format = format #@-node:__init__ #@+node:__getitem__ def __getitem__(self, format): return _StringConverter(self.source, format) #@-node:__getitem__ #@+node:__getattr__ def __getattr__(self, name): class StrWrapper(object): def __init__(self, value, name, source, format): self._value = value self.name = name self.source = source self.format = format def __call__(self, arg): formatter = self.source.formatter(self.name, arg, self.format) return formatter(self._value(arg)) value = getattr(self.source, name) if callable(value): #for methods the wrapper has to return StrWrapper(value, name, self.source, self.format) formatter = self.source.formatter(name, format=self.format) return formatter(value) #@-node:__getattr__ #@-others #@-node:class _StringConverter #@+node:Multi def Multi(val, **kwargs): """returns a directory for mutlivalued attributes""" return dict(_default=val, **kwargs) #@nonl #@-node:Multi #@+node:create_relative_path def create_relative_path(from_, to_): """ creates a relative path from absolute path from_ to absolute path to_ """ from_ = from_.split(".") to_ = to_.split(".") for i, parts in enumerate(zip(from_, to_)): from_part, to_part = parts if from_part != to_part: break from_ = from_[i:] to_ = to_[i:] return "up." * len(from_) + ".".join(to_) #@nonl #@-node:create_relative_path #@+node:create_absolute_path def create_absolute_path(from_, to_): """ creates a absolute path from absolute path from_ to relative path to_ """ from_ = from_.split(".") to_ = to_.split(".") for i, part in enumerate(to_): if part != "up": break from_ = from_[:-i] to_ = to_[i:] return "%s.%s" % (".".join(from_), ".".join(to_)) #@-node:create_absolute_path #@+node:_split_path def _split_path(path): try: index = path.rindex(".") return path[:index], path[index + 1:] except: return path #@-node:_split_path #@+node:_to_datetime _to_datetime = pcalendar.to_datetime #@nonl #@-node:_to_datetime #@+node:_get_tasks_of_sources def _get_tasks_of_sources(task, attrib_filter="end,start,effort,length,duration"): #return all source tasks, this task is dependend on dep_tasks = {} while task: for dep in task._sources.values(): for d in dep: path, attrib = _split_path(d) if attrib and attrib_filter.find(attrib) >= 0: dep_tasks[path] = True task = task.up return dep_tasks.keys() #@-node:_get_tasks_of_sources #@+node:_build_balancing_list def _build_balancing_list(tasks): """ Returns a specialy sorted list of tasks. If the tasks will allocate resources in the sorting order of that list correct balancing is ensured """ # first sort the list for attributes index = 0 balancing_list = [(-t.priority, t.balance, index, t) for index, t in enumerate(tasks)] balancing_list.sort() #print #for p, b, i, t in balancing_list: # print p, b, i, t.path balancing_list = [ t for p, b, i, t in balancing_list ] #now correct the presorted list: #if task a is dependent on task b, b will be moved before a done_map = { } count = len(balancing_list) while len(done_map) < count: for i in range(count): to_inspect = balancing_list[i] if done_map.has_key(to_inspect): continue done_map[to_inspect] = True break else: break #@ << define inspect_depends_on >> #@+node:<< define inspect_depends_on >> inspect_path = to_inspect.path + "." sources = _get_tasks_of_sources(to_inspect) sources = [ s + "." for s in sources if not inspect_path.startswith(s) ] # the if in the later line ignores assignments like # like start = up.start (i.e. references to parents) # this will be handled in the second if of inspect_depends_on # and can cause errors otherwise def inspect_depends_on(task): cmp_path = task.path + "." for src in sources: if cmp_path.startswith(src): #task is a source of to_inspect return True if inspect_path.startswith(cmp_path): #to_inspect is a child of task return True return False #@nonl #@-node:<< define inspect_depends_on >> #@nl for j in range(i + 1, count): check_task = balancing_list[j] if done_map.has_key(check_task): continue if inspect_depends_on(check_task): del balancing_list[j] balancing_list.insert(i, check_task) i += 1 # to_inspect is now at i + 1 return balancing_list #@-node:_build_balancing_list #@+node:_as_string def _as_string(val): if isinstance(val, basestring): return '"""%s"""' % val.replace("\n", "\\n") if isinstance(val, pcalendar._WorkingDateBase): return '"%s"' % val.strftime("%Y-%m-%d %H:%M") if isinstance(val, datetime.datetime): return '"%s"' % val.strftime("%Y-%m-%d %H:%M") if isinstance(val, datetime.timedelta): return '"%id %iM"' % (val.days, val.seconds / 60) if isinstance(val, tuple): result = map(_as_string, val) return "(%s)" % ", ".join(result) if isinstance(val, list): result = map(_as_string, val) return "[%s]" % ", ".join(result) if isinstance(val, resource.Resource): return val._as_string() if isinstance(val, Task): return val.path return str(val) #@-node:_as_string #@+node:_step_tasks def _step_tasks(task): if isinstance(task, Task): yield task stack = [iter(task.children)] while stack: for task in stack[-1]: yield task if task.children: stack.append(iter(task.children)) break else: stack.pop() #@-node:_step_tasks #@-node:Utilities #@+node:Cache instrumentation_cache = {} balancing_cache = {} def clear_cache(): instrumentation_cache.clear() balancing_cache.clear() #@nonl #@-node:Cache #@+node:Resource Allocators #@+others #@+node:VariableLoad def VariableLoad(limit=0): """ Allocates the resource with maximal possible load. If limit is given, a the load is at least limit or more. """ try: balance = me.balance except NameError: balance = SLOPPY if balance != SLOPPY: raise RuntimeError("You may specify variable_load only with balance=SLOPPY") return -limit #@-node:VariableLoad #@+node:_calc_load def _calc_load(task, resource): #changed at the resource instance load = resource.__dict__.get("load") if load is not None: return load load = task.__dict__.get("load") if load is not None: return load #inherited by the task return min(task.load, task.max_load, resource.max_load or 100.0) #@-node:_calc_load #@+node:_calc_maxload def _calc_maxload(task, resource): #changed at the resource instance max_load = resource.__dict__.get("max_load") if max_load: return max_load #an explicit load can overwrite max_load load = max(resource.__dict__.get("load", 0), task.__dict__.get("load"), 0) #change at the task max_load = task.__dict__.get("max_load") if max_load: return max(max_load, load) #inherited by the resource max_load = resource.max_load if max_load: return max(max_load, load) #inherited by the task return max(task.max_load, load) #@-node:_calc_maxload #@+node:class AllocationAlgorithm class AllocationAlgorithm(object): """This class is a base for resource allocation algorithms""" #@ @+others #@+node:test_allocation def test_allocation(self, task, resource): """This method simulates the allocation of a specific resource. It returns a list of values representing the state of the allocation. The task allocator calls test_allocation for every alternative resource. It compares the first items of all return lists, and allocates the resource with the minum first item value""" return (task.end, ) #@-node:test_allocation #@+node:allocate def allocate(self, task, state): """This method eventually allocates a specific resource. State is the return list of test_allocation""" pass #@-node:allocate #@-others #@-node:class AllocationAlgorithm #@+node:class StrictAllocator class StrictAllocator(AllocationAlgorithm): """This class implements the STRICT resource allocation""" #@ @+others #@+node:_distribute_len_loads def _distribute_len_loads(self, task, resource, effort, length): # A special load calculation, if effort and length are given. # and the resources have a defined maxload, the load must be # individually calculated for each resource. # Formulars: r=resources, t=task # effort = length * efficiency(t) * sum[load(r) * effiency(r)] # ==> sum_load = sum[load(r) * effiency(r)] # = effort / (length * efficiency(t)) # sum_load = float(effort) / (task.efficiency * length) # algorithm: # The goal is to distribute the load (norm_load) equally # to all resources. If a resource has a max_load(r) < norm_load # the load of this resource will be max_load(r), and the other # resources will have another (higher) norm_load max_loads = map(lambda r: (_calc_maxload(task, r), r), resource) max_loads.sort() efficiency_sum = sum(map(lambda r: r.efficiency, resource)) norm_load = sum_load / efficiency_sum loads = {} for max_load, r in max_loads[:-1]: if max_load < norm_load: loads[r] = max_load efficiency_sum -= r.efficiency sum_load -= max_load * r.efficiency norm_load = sum_load / efficiency_sum else: loads[r] = norm_load max_load, r = max_loads[-1] loads[r] = norm_load return loads #@-node:_distribute_len_loads #@+node:test_allocation def test_allocation(self, task, resource): effort = task.__dict__.get("effort") to_start = task._to_start to_end = task._to_end to_delta = task._to_delta if task.performed_end: start = to_start(max(task.performed_end, task.root.calendar.now, task.start)) else: start = task.start if task.root.has_actual_data and task.complete == 0: start = max(start, to_start(task.root.calendar.now)) base_start = to_start(task.performed_start or task.start) calc_load = lambda r: _calc_load(task, r) loads = map(lambda r: (r, calc_load(r)), resource) length = task.__dict__.get("length") duration = task.__dict__.get("duration") end = task.__dict__.get("end") #@ << correct length >> #@+node:<< correct length >> if length is not None: length = to_delta(max(length - (task.start - base_start), 0)) #@nonl #@-node:<< correct length >> #@nl #@ << correct duration >> #@+node:<< correct duration >> if duration is not None: delta = task.start.to_datetime() - base_start.to_datetime() delta = to_delta(delta, True) duration = to_delta(max(duration - delta, 0), True) #@nonl #@-node:<< correct duration >> #@nl #@ << check end >> #@+node:<< check end >> if end is not None: length = end - start if length <= 0: return False #@nonl #@-node:<< check end >> #@nl #@ << correct effort and (re)calculate length >> #@+node:<< correct effort and (re)calculate length >> if effort is not None: effort -= task.performed_effort effort = to_delta(max(effort, 0)) if effort <= 0: return False if length is not None: #if length and effort is set, the load will be calculated length = length or task.calendar.minimum_time_unit loads = self._distribute_len_loads(task, resource, effort, length) def calc_load(res): return loads[res] else: #the length depends on the count of resources factor = sum(map(lambda a: a[0].efficiency * a[1], loads)) * task.efficiency length = effort / factor #@nonl #@-node:<< correct effort and (re)calculate length >> #@nl #@ << set adjust_date and delta >> #@+node:<< set adjust_date and delta >> if length is not None: adjust_date = lambda date: date delta = to_delta(length).round() else: assert(duration is not None) adjust_date = _to_datetime delta = datetime.timedelta(minutes=duration) #@nonl #@-node:<< set adjust_date and delta >> #@nl # find the earliest start date start, book_load\ = self.balance(task, start, delta, adjust_date, calc_load, resource) end = to_end(start + delta) start = to_start(start) if effort is None: #length is frozen ==> a new effort will be calculated factor = sum(map(lambda a: a[1], loads)) length = end - start effort = to_delta(length * factor\ + task.performed_effort).round() return (end, book_load), resource, calc_load, start, effort #@-node:test_allocation #@+node:allocate def allocate(self, task, state): # now really book the resource end_bl, resource, calc_load, start, effort = state end = end_bl[0] cal = task.root.calendar to_start = task._to_start to_end = task._to_end to_delta = task._to_delta task.start = task.performed_start \ and to_start(task.performed_start) \ or to_start(start) task.end = end task._unfreeze("length") task._unfreeze("duration") length = end - start for r in resource: book_load = calc_load(r) work_time = to_delta(length * book_load).round() r.book_task(task, start, end, book_load, work_time, False) #the following lines are important to be exactly at this #positions in that order: # done and todo are dependend on: # - the existence of effort (if effort was set or not set) # - book_task (they can only be calculated, if the task is booked) # - booked_resource (to get the booked tasks) task.booked_resource = resource task.done = task.done task.todo = task.todo task.length = end - task.start task.effort = to_delta(effort + task.performed_effort) #@-node:allocate #@+node:balance #now effort exists always def balance(self, task, start, delta, adjust_date, calc_load, resource): book_load = max(map(lambda r: r.get_load(task.start, task.scenario), resource)) return start, book_load #@-node:balance #@-others #@-node:class StrictAllocator #@+node:class SmartAllocator class SmartAllocator(StrictAllocator): #@ @+others #@+node:balance def balance(self, task, start, delta, adjust_date, calc_load, resource): #find the earliest start date, at which all #resources in the team are free cal = task.root.calendar to_start = task._to_start start = adjust_date(start) scenario = task.scenario while True: #we have finished, when all resources have the #same next free start date for r in resource: max_load = _calc_maxload(task, r) load = calc_load(r) #find the next free time of the resource s = r.find_free_time(start, delta, load, max_load, scenario) if s != start: s = to_start(s) start = adjust_date(s) break else: #only one resource break return start, 1.0 #@-node:balance #@-others #@-node:class SmartAllocator #@+node:class SloppyAllocator class SloppyAllocator(AllocationAlgorithm): #@ @+others #@+node:test_allocation def test_allocation(self, task, resource): if task.__dict__.has_key("effort"): return self.test_allocation_effort(task, resource) return self.test_allocation_length(task, resource) #@-node:test_allocation #@+node:test_allocation_length def test_allocation_length(self, task, resource): #length is frozen ==> effort will be calculated to_start = task._to_start to_end = task._to_end to_delta = task._to_delta end = task.end if task.performed_end: start = to_start(max(task.performed_end, task.root.calendar.now, start)) else: start = task.start base_start = to_start(task.performed_start or task.start) length = to_delta(max(task.length - (start - base_start), 0)) sum_effort = 0 intervals = [] scenario = task.scenario for r in resource: date = start max_load = _calc_maxload(task, r) book_load = _calc_load(task, r) while date < end: #find free time intervals and add them for booking endi, load = r.end_of_booking_interval(date, task) endi = min(endi, end) endi = to_end(endi) if book_load <= 0: #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) used_book_load = - book_load diff_load = max_load - load if diff_load and diff_load >= book_load: used_book_load = diff_load else: used_book_load = max_load else: used_book_load = book_load if max_load - load >= used_book_load: intervals.append((r, used_book_load, date, endi)) sum_effort = (endi - date) * used_book_load date = to_start(endi) return -sum_effort, end, resource, intervals #@-node:test_allocation_length #@+node:test_allocation_effort def test_allocation_effort(self, task, resource): #effort is frozen ==> length will be calculated to_start = task._to_start to_end = task._to_end to_delta = task._to_delta intervals = [] effort = task.__dict__.get("effort") if task.performed_end: next_date = to_start(max(task.performed_end, task.root.calendar.now, task.start)) else: next_date = task.start if task.root.has_actual_data and task.complete == 0: next_date = max(next_date, to_start(task.root.calendar.now)) #walks chronologicly through the booking #intervals of each resource, and reduces #the effort for each free interval #until it becomes 0 alloc_effort = effort effort -= task.performed_effort while effort > 0: date = next_date interval_resource = [] interval_end = to_start(sys.maxint) factor = 0 for r in resource: max_load = _calc_maxload(task, r) book_load = _calc_load(task, r) end, load = r.end_of_booking_interval(date, task) interval_end = to_start(min(end, interval_end)) if book_load <= 0: #variable book_load ==> calc the maxmimal possible book_load >= (the given book_load) book_load = - book_load diff_load = max_load - load if diff_load and diff_load >= book_load: book_load = diff_load else: book_load = max_load if book_load + load <= max_load: resource_factor = book_load * r.efficiency interval_resource.append((r, book_load, resource_factor)) factor += resource_factor next_date = interval_end if factor: factor *= task.efficiency length = to_delta(effort / factor).round() end = date + length if interval_end >= end: next_date = interval_end = end effort = 0 book_end = end else: book_end = interval_end length = book_end - date minus_effort = length * factor effort -= minus_effort book_end = to_end(book_end) intervals.append((date, book_end, length, interval_resource)) return next_date, alloc_effort, resource, intervals #@-node:test_allocation_effort #@+node:allocate def allocate(self, task, state): if task.__dict__.has_key("effort"): self.allocate_effort(task, state) else: self.allocate_length(task, state) #@-node:allocate #@+node:allocate_length def allocate_length(self, task, state): # now really book the resource neg_sum_effort, end, resource, intervals = state cal = task.root.calendar to_start = task._to_start to_end = task._to_end to_delta = task._to_delta task.start = to_start(task.performed_start or task.start) task.end = to_end(end) task._unfreeze("length") task._unfreeze("duration") effort = 0 for r, load, s, e in intervals: work_time = to_delta((e - s) * load).round() effort += work_time r.book_task(task, s, e, load, work_time, False) #see comment at StrictAllocator.allocate task.booked_resource = resource task.done = task.done task.todo = task.todo task.effort = to_delta(effort + task.performed_effort).round() #@-node:allocate_length #@+node:allocate_effort def allocate_effort(self, task, state): # now really book the resource end, effort, resource, intervals = state to_start = task._to_start to_end = task._to_end to_delta = task._to_delta task.start = task.performed_start \ and to_start(task.performed_start) \ or to_start(intervals[0][0]) task.end = to_end(end) task._unfreeze("length") task._unfreeze("duration") for start, end, length, resources in intervals: for r, load, factor in resources: work_time = to_delta(length * load) r.book_task(task, start, end, load, work_time, False) task.booked_resource = resource task.done = task.done task.todo = task.todo task.effort = to_delta(effort) task.length = task.end - task.start #@-node:allocate_effort #@-others #@-node:class SloppyAllocator #@-others _smart_allocator = SmartAllocator() _sloppy_allocator = SloppyAllocator() _strict_allocator = StrictAllocator() _allocators = { SMART: _smart_allocator, SLOPPY: _sloppy_allocator, STRICT: _strict_allocator } _allocator_strings = { SMART: "SMART", SLOPPY: "SLOPPY", STRICT: "STRICT" } #@-node:Resource Allocators #@+node:Load Calculators #@+node:YearlyMax def YearlyMax(value): """ Calculates a load parameter with a maximal yearly workload """ #@ << calculate calendar and time_diff >> #@+node:<< calculate calendar and time_diff >> try: cal = me.calendar except NameError: cal = pcalendar._default_calendar time_diff = cal.Minutes(value) #@nonl #@-node:<< calculate calendar and time_diff >> #@nl return float(time_diff) / \ (cal.working_days_per_year \ * cal.working_hours_per_day \ * 60) #@nonl #@-node:YearlyMax #@+node:WeeklyMax def WeeklyMax(value): """ Calculates a load parameter with a maximal weekly workload """ #@ << calculate calendar and time_diff >> #@+node:<< calculate calendar and time_diff >> try: cal = me.calendar except NameError: cal = pcalendar._default_calendar time_diff = cal.Minutes(value) #@nonl #@-node:<< calculate calendar and time_diff >> #@nl return float(time_diff) / \ (cal.working_days_per_week \ * cal.working_hours_per_day \ * 60) #@-node:WeeklyMax #@+node:MonthlyMax def MonthlyMax(value): """ Calculates a load parameter with a maximal monthly workload """ #@ << calculate calendar and time_diff >> #@+node:<< calculate calendar and time_diff >> try: cal = me.calendar except NameError: cal = pcalendar._default_calendar time_diff = cal.Minutes(value) #@nonl #@-node:<< calculate calendar and time_diff >> #@nl return float(time_diff) / \ (cal.working_days_per_month \ * cal.working_hours_per_day \ * 60) #@-node:MonthlyMax #@+node:DailyMax def DailyMax(value): """ Calculates a load parameter with a maximal daily workload """ #@ << calculate calendar and time_diff >> #@+node:<< calculate calendar and time_diff >> try: cal = me.calendar except NameError: cal = pcalendar._default_calendar time_diff = cal.Minutes(value) #@nonl #@-node:<< calculate calendar and time_diff >> #@nl return float(time_diff) / (cal.working_hours_per_day * 60) #@-node:DailyMax #@-node:Load Calculators #@+node:Task #@+node:class _TaskProperty class _TaskProperty(object): #@ @+others #@+node:__init__ def __init__(self, method): self.method = method #@-node:__init__ #@+node:__get__ def __get__(self, instance, owner): if not instance: return None return instance._wrap_attrib(self.method) #@-node:__get__ #@-others #@-node:class _TaskProperty #@+node:class _RoundingTaskProperty class _RoundingTaskProperty(object): #@ @+others #@+node:__init__ def __init__(self, method, name): self.method = method self.name = name #@-node:__init__ #@+node:__get__ def __get__(self, instance, owner): if not instance: return None result = instance._wrap_attrib(self.method).round() if instance._is_frozen: #correct the attrib to the rounded value setattr(instance, self.name, result) return result #@-node:__get__ #@-others #@-node:class _RoundingTaskProperty #@+node:class Task class Task(object): #@ << description >> #@+node:<< description >> """ This class represents a single task in the project tree. A task can have other child tasks, or is a leaf of the tree. Resources will be allocated only to leafes. You will never create task objects by your self, they are created indirectly by Projects. @var root: Returns the root project task. @var up: Returns the parent task. @var title: Specifies an alternative more descriptive name for the task. @var start: The start date of the task. Valid values are expressions and strings specifing a datatime @var end: The end date of the task. Valid values are expressions and strings. @var effort: Specifies the effort needed to complete the task. Valid values are expressions and strings. (Todo: What happens, in case of specified performance data...) @var length: Specifies the time the task occupies the resources. This is working time, not calendar time. 7d means 7 working days, not one week. Whether a day is considered a working day or not depends on the defined working hours and global vacations. @var duration: Specifies the time the task occupies the resources. This is calendar time, not working time. 7d means one week. @var buffer: Specifies the time a task can be delayed, without moving dependend milestones. A Task with a buffer S{<=} 0d is part of the critical chain. This attribute is readonly. @var complete: Specifies what percentage of the task is already completed. @var todo: Specifies the effort, which needs to be done to complete a task. This is another (indirect) way to specify the ME{complete} attribute. @var done: Specifies the work effort, which has been already done. This attribute is readonly. @var estimated_effort: Specifies the estimated_effort given by setting the effort property. @var performed: Specifies a list of actual working times performed on the task. The format is: C{[ (resource, from, to, time), ... ]} @var performed_work_time: Specifies the sum of all working times. This attribute is readonly. @var performed_effort: Specifies the complete effort of all working times. This attribute is readonly. @var performed_start: The start date of the performed data. @var performed_end: The end date of the performed data. @var performed_resource: The resources who have already performed on the task. This attribute is readonly. @var balance: Specifies the resource allocation type. Possible values are CO{STRICT}, CO{SLOPPY}, CO{SMART}. @var resource: Specifies the possible resources, that may be allocated for the task. @var booked_resource: Specifies the allocated resources of a task. This attribute is readonly. @var load: Specifies the daily load of a resource for an allocation of the specified task. A load of 1.0 (default) means the resource is allocated for as many hours as specified by ME{working_hours_per_day}. A load of 0.5 means half that many hours. @var max_load: Specify the maximal allowed load sum of all simultaneously allocated tasks of a resource. A ME{max_load} of 1.0 (default) means the resource may be fully allocated. A ME{max_load} of 1.3 means the resource may be allocated with 30% overtime. @var efficiency: The efficiency of a resource can be used for two purposes. First you can use it as a crude way to model a team. A team of 5 people should have an efficiency of 5.0. Keep in mind that you cannot track the member of the team individually if you use this feature. The other use is to model performance variations between your resources. @var milestone: Specified if the task is a milestone. The possible values are C{True} or "later". If the start date of the milestone is not a valid working date, the milestone will appear at the previous working date before the given start date. If "later" is specified the milestone will appear at the next valid working date. A milestone has always an effort of 0d. @var priority: Specifies a priority between 1 and 1000. A task with higher priority is more likely to get the requested resources. The default priority is 500. @var children: Specifies a list of all subtasks. A task without children is called a leaf task index{leaf task} otherwise it is called a parent task index{parent task}. This attribute is readonly. @var depth: Specifies the depth of the task within the hierachy. This attribute is readonly. @var index: Specifies a structural index number. This attribute is readonly. @var path: Specifies the path. @var copy_src: Specifies the path to an other task. When you set this attribute, all attributes (except of ME{start} and ME{end}) of copy_src will be copied to the current task. This is usefull if you want to define the same task, in diffent project definitions. It acts like a task link. @var scenario: The scenario which is currently evaluated. This attribute is readonly. @var dont_inherit: A list of attribute names, which will be not inherited by subtasks. @var calendar: Specifies the task calendar. @var working_days_per_week: Specifies the days within a working week. This value is used internally to convert time differences from weeks to days. The default value is 5 days. @var working_days_per_month: Specifies the days within a working month. This value is used internally to convert time differences from months to days. The default value is 20 days. @var working_days_per_year: Specifies the days within a working year. This value is used internally to convert time differences from years to days The default value is 200 days. @var working_hours_per_day: Specifies the hours within a working day. This value is used internally to convert time differences from are entered in days to hours. The default value is 8 hours. @var minimum_time_unit: Specifies the minimum resolution in minutes for the task scheduling. The default value is 15 minutes. @var vacation: Specifies a public vacation for the calendar. This attribute is specified as a list of date literals or date literal intervals. Be aware that the end of an interval is excluded, i.e. it is the first working date. @var extra_work: Specifies additional worktime. This attribute is specified as a list of date literals or date literal intervals. Be aware that the end of an interval is excluded, i.e. it is the first working date. @var working_days: Specifies the weekly working time within calendar. The format of this attribute is: [ (day_range, time_range, ...), (day_range, time_range, ...), ... ]. day_range is a comma sperated string of week days. Valid values are mon, tue, wed, thu, fri, sat, sun. time_range is string specifing a time interval like 8:00-10:00. You can specified any number of time_ranges, following the first. @var now: Specifies the current daytime and is a date literal. ME{now} is used to calculate several task attributes. """ #@nonl #@-node:<< description >> #@nl #@ << declarations >> #@+node:<< declarations >> # Variables for the gui interface _date_completion = { "Date": 'Date("|")', "max": "max(|)", "min": "min(|)", "Multi" : "Multi(|)" } _delta_completion = { "Delta" : 'Delta("|")', "Multi" : "Multi(|)" } __attrib_completions__ = { \ "def NewTask():" : "def |NewTask():\n", "milestone": 'milestone = True', "start": 'start = ', "end": 'end = ', "effort": 'effort = "|"', "duration": 'duration = "|"', "length": 'length = "|"', "todo": 'todo = "|"', "done": 'done = "|"', "title": 'title = "|"', "load": 'load = ', "max_load": 'max_load = ', "efficiency": 'efficiency = ', "complete": 'complete = ', "copy_src": 'copy_src =', "__constraint__": '__constraint__():\n|"', "priority": 'priority = ', "balance" : 'balance = ', "resource": 'resource = ', "performed" : 'performed = [(|resource, "2002-02-01", "2002-02-05", "2H"),]', "add_attrib": "add_attrib(|'name', None)", "working_days_per_week": 'working_days_per_week = ', "working_days_per_month": 'working_days_per_month = ', "working_days_per_year": 'working_days_per_year = ', "working_hours_per_day": 'working_hours_per_day = ', "minimum_time_unit": 'minimum_time_unit = ', "vacation": 'vacation = [("|2002-02-01", "2002-02-05")]', "extra_work": 'extra_work = [("|2002-02-01", "2002-02-05")]', "working_days" : 'working_days = ["|mon,tue,wed,thu,fri", "8:00-12:00", "13:00-17:00"]', "now": 'now = "|"', "calendar" : 'calendar = ', "#load": { "YearlyMax": 'YearlyMax("|")', "WeeklyMax": 'WeeklyMax("|")', "MonthlyMax": 'MonthlyMax("|")', "DailyMax": 'DailyMax("|")', "VariableLoad" : "VariableLoad(|)"}, "#max_load": { "YearlyMax": 'YearlyMax("|")', "WeeklyMax": 'WeeklyMax("|")', "MonthlyMax": 'MonthlyMax("|")', "DailyMax": 'DailyMax("|")' }, "#start": _date_completion, "#end": _date_completion, "#effort": _delta_completion, "#duration": _delta_completion, "#length": _delta_completion, "#todo": _delta_completion, "#done": _delta_completion, "#resource" : "get_resource_completions", "#calendar" : "get_calendar_completions", "#balance": { "STRICT": "STRICT", "SMART": "SMART", "SLOPPY": "SLOPPY" } } formats = { "start" : "%x %H:%M", "end" : "%x %H:%M", "performed_start" : "%x %H:%M", "performed_end" : "%x %H:%M", "load" : "%.2f", "length" : "%dd{ %HH}{ %MM}", "effort" : "%dd{ %HH}{ %MM}", "estimated_effort" : "%dd{ %HH}{ %MM}", "performed_effort" : "%dd{ %HH}{ %MM}", "duration" : "%dd{ %HH}{ %MM}", "complete" : "%i", "priority" : "%i", "todo" : "%dd{ %HH}{ %MM}", "done" : "%dd{ %HH}{ %MM}", "efficiency" : "%.2f", "buffer" : "%dd{ %HH}{ %MM}", "costs" : "%.2f", "sum" : "%.2f", "max" : "%.2f", "min" : "%.2f", "milestone" : "%s", "resource" : "%s", "booked_resource" : "%s", "performed_resource" : "%s" } _constraint = None _is_frozen = False _is_compiled = False _is_parent_referer = False scenario = None # only for autocompletion milestone = False performed = () performed_resource = () booked_resource = () _performed_resource_length = () _resource_length = () dont_inherit = () performed_start = None performed_end = None performed_work_time = pcalendar.Minutes(0) _setting_hooks = {} #@nonl #@-node:<< declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, func, name, parent=None, index=1): assert(type(func) == types.FunctionType) func_key = (func.func_code, func.func_closure and id(func.func_closure)) try: instrumented = instrumentation_cache[func_key] except KeyError: instrumented = _instrument(func) instrumented.org_code = func_key instrumentation_cache[func_key] = instrumented func.task_func = instrumented # will be used in the gui self._function = instrumented self.name = name self.up = parent self.children = [] self._sources = {} # all tasks, I am linked to self._dependencies = {} # all tasks that link to me self._original_values = {} self._properties = {} # a registry of all non standard attributes self.title = self.name self.root = parent and parent.root or self self.scenario = self.root.scenario self.path = parent and parent.path + "." + name or name self.depth = len(self.path.split(".")) - 1 self.index = parent and ("%s.%i" % (parent.index, index)) \ or str(index) if self.formats.has_key(name): raise AttributeError("Task name '%s' hides attribute of parent." \ % name) cal = self.calendar self._to_delta = cal.Minutes self._to_start = cal.StartDate self._to_end = cal.EndDate #@-node:__init__ #@+node:__iter__ def __iter__(self): return _step_tasks(self) #@-node:__iter__ #@+node:__repr__ def __repr__(self): return "" % self.name #@-node:__repr__ #@+node:__cmp__ def __cmp__(self, other): try: return cmp(self.path, other.path) except Exception: return cmp(self.path, other) #@-node:__cmp__ #@+node:__getattr__ def __getattr__(self, name): try: if name[0] != "_": parent = self.up while parent: if name not in parent.dont_inherit: result = getattr(parent, name) if not (isinstance(result, Task) and result.up == parent): return result parent = parent.up except AttributeError: pass except IndexError: raise AttributeError() exception = AttributeError("'%s' is not a valid attribute of '%s'." % (name, self.path)) exception.is_frozen = self._is_frozen raise exception #@-node:__getattr__ #@+node:_idendity_ def _idendity_(self): return self.root.id + self.path[4:] #@-node:_idendity_ #@+node:_set_hook def _set_hook(cls, attrib_name, function=None): if function: cls._setting_hooks[attrib_name] = function else: try: del cls._setting_hooks[attrib_name] except KeyError: pass _set_hook = classmethod(_set_hook) #@nonl #@-node:_set_hook #@+node:Public methods #@+node:to_string def to_string(self): return _StringConverter(self) to_string = property(to_string) #@nonl #@-node:to_string #@+node:indent_name def indent_name(self, ident=" "): """ returns a indented name, according to its depth in the hierachy. """ return ident * self.depth + self.name indent_name.attrib_method = True indent_name.__call_completion__ = "indent_name()" #@-node:indent_name #@+node:costs def costs(self, cost_name, mode="ep"): """ calculates the resource costs for the task. cost_name is the name of a rate attribute of the reosurce mode is character combination: e calculates the estimated costs p calculates the performed costs ==> pe calculates all costs """ if self.children: return sum([ c.costs(cost_name, mode) for c in self.children]) costs = 0 if 'e' in mode: costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], self._resource_length)) if 'p' in mode: costs += sum(map(lambda rl: getattr(rl[0], cost_name) * rl[1], self._performed_resource_length)) costs /= (60.0 * self.root.calendar.working_hours_per_day) return round(costs, 2) costs.attrib_method = True costs.__call_completion__ = 'costs("|")' #@-node:costs #@+node:sum def sum(self, attrib_name): val = 0 if self.children: val += sum(map(lambda c: c.sum(attrib_name), self.children)) if self.is_inherited(attrib_name): return val if attrib_name not in self.dont_inherit: return val return val + getattr(self, attrib_name) sum.attrib_method = True sum.__call_completion__ = 'sum("|")' #@-node:sum #@+node:min def min(self, attrib_name): if self.children: return min(map(lambda c: c.min(attrib_name), self.children)) return getattr(self, attrib_name) min.attrib_method = True min.__call_completion__ = 'min("|")' #@-node:min #@+node:max def max(self, attrib_name): if self.children: return max(map(lambda c: c.max(attrib_name), self.children)) return getattr(self, attrib_name) max.attrib_method = True max.__call_completion__ = 'max("|")' #@-node:max #@+node:all_resources def all_resources(self): result = self._all_resources_as_dict() result = result.keys() result.sort() return result #@-node:all_resources #@+node:get_task def get_task(self, path=None): """ Returns a task with the given path. """ if not path: return self names = path.split(".") rest = ".".join(names[1:]) result = getattr(self, names[0], None) return isinstance(result, Task) and result.get_task(rest) or None #@-node:get_task #@+node:snapshot def snapshot(self, indent="", name=None): text = indent + "def %s():\n" % (name or self.name) indent += " " for name in ("priority", "balance", "complete", "milestone", "end", "start", "effort", "load"): val = getattr(self, name, None) if val is None: continue if name[0] == "_": name = name[1:] text += "%s%s = %s\n" % (indent, name, _as_string(val)) for name in self._properties: if name.startswith("performed"): continue val = getattr(self, name, None) try: if issubclass(val, resource.Resource): continue except TypeError: pass text += "%s%s = %s\n" % (indent, name, _as_string(val)) resources = tuple(self._iter_booked_resources()) if resources: text += "%sresource = \\\n" % indent def make_resource(res): return "%s %s" \ % (indent, res.snapshot()) text += "&\\\n".join(map(make_resource, resources)) + "\n" def make_resource_booking(res): def make_booking(booking): return '%s (%s, "%s", "%s", "%sM"),' \ % (indent, res.name, booking.book_start.strftime("%Y%m%d %H:%M"), booking.book_end.strftime("%Y%m%d %H:%M"), booking.work_time) return "\n".join(map(make_booking, res.get_bookings(self))) text += "%sperformed = [\n" % indent text += "\n".join(map(make_resource_booking, resources)) + "]" child_text = map(lambda c: c.snapshot(indent), self.children) text += "\n\n" text += "".join(child_text) return text #@-node:snapshot #@+node:is_inherited def is_inherited(self, attrib_name): return not self.__dict__.has_key(attrib_name) #@-node:is_inherited #@+node:formatter def formatter(self, attrib_name, arg=None, format=None): """returns a function which is able to convert the value of the given attrib_name to a string""" formats = self.formats format = format or formats.get(attrib_name) if attrib_name in ("start", "end", "length", "effort", "done", "todo", "buffer", "estimated_effort", "performed_effort", "performed_start", "performed_end"): def save_strftime(v): try: return v.strftime(format) #except AttributeError: some bug avoid catching this exception except Exception: return str(v) return save_strftime if attrib_name == "duration": def save_strftime(v): try: return v.strftime(format, True) except AttributeError: return str(v) return save_strftime if attrib_name in ("booked_resource", "performed_resource"): def get_resource_name(v): title = getattr(v, "title", None) if title: return title return ", ".join([r.title for r in v]) return get_resource_name if arg and attrib_name in ("costs", "sum", "max", "min"): format = formats.get("%s(%s)" % (attrib_name, arg), format) if format: return lambda v: locale.format(format, v, True) return str #@-node:formatter #@-node:Public methods #@+node:Resource allocation Methods #@+node:_all_resources_as_dict def _all_resources_as_dict(self): if self.children: result = {} for c in self.children: result.update(c._all_resources_as_dict()) return result if self.resource: return dict(map(lambda r: (r, 1), self.resource.all_members())) return {} #@-node:_all_resources_as_dict #@+node:_test_allocation def _test_allocation(self, resource_state, allocator): resource = self.resource._get_resources(resource_state) if not resource: return False return allocator.test_allocation(self, resource) #@-node:_test_allocation #@+node:_allocate def _allocate(self, state, allocator): allocator.allocate(self, state) #activate cache for done and todo if self.start.to_datetime() > self.end.to_datetime(): #this can happen when performed effort are #during non working time tmp = self.start self.start = self.end self.end = tmp for r in self.performed_resource: r.correct_bookings(self) self._resource_length = map(lambda r: (weakref.proxy(r), \ r.length_of(self)), self._iter_booked_resources()) #@-node:_allocate #@+node:_convert_performed def _convert_performed(self, all_resources): performed = self.performed if not performed: return False if not isinstance(performed, (tuple, list)) \ or not isinstance(performed[0], (tuple, list)) \ or not len(performed[0]) >= 3: self._raise(TypeError("""The format of the performed attribute must be: [( res_name, start_literal, end_literal, working_time ), ... ]. """), "performed") round_down_delta = self.root.calendar.minimum_time_unit / 2 round_down_delta = datetime.timedelta(minutes=round_down_delta) def convert_item(index): item = performed[index] res, start, end = item[:3] if isinstance(res, str): found = filter(lambda r: r.name == res, all_resources) if found: res = found[0] try: if not isinstance(res, (resource.Resource, resource._MetaResource)): raise ValueError("the resource '%s' is unknown." % res) start = _to_datetime(start) end = _to_datetime(end) if len(item) > 3: working_time = self._to_delta(item[3]).round() else: working_time = self._to_delta(end - start, True) return ((res, start, end, working_time), index) except Exception, exc: self._raise(exc.__class__("Item %i: %s" \ % (index + 1, str(exc))), "performed") converted = dict(map(convert_item, range(len(performed)))) converted = converted.items() converted.sort() #check for overlapping items last_res = None for item, index in converted: res, start, end, work_time = item if last_res == res and start < last_end: self._warn("Items %i, %i: %s and %s are overlapping." \ % (last_index + 1, index + 1, str(performed[last_index]), str(performed[index])), "performed") last_res = res last_end = end last_index = index self._performed = map(lambda x: x[0], converted) return True #@-node:_convert_performed #@+node:_allocate_performed def _allocate_performed(self, performed): if not performed: return to_delta = self._to_delta to_start = self._to_start to_end = self._to_end last = datetime.datetime.min first = datetime.datetime.max effort = 0 work_time_sum = 0 zero_minutes = to_delta(0) minimum_time_unit = to_delta(self.calendar.minimum_time_unit) summary = {} for item in performed: res, start, end, work_time = item effort += work_time * self.efficiency * res.efficiency work_time_sum += work_time res = res() ss, es, wts = summary.get(res, (datetime.datetime.max, datetime.datetime.min, zero_minutes)) summary[res] = (min(ss, start), max(es, end), wts + work_time) for r, v in summary.iteritems(): start, end, work_time = v assert(start.__class__ is datetime.datetime) assert(end.__class__ is datetime.datetime) #the booking limits should be inside the workingtime #to display them correct in resource charts cstart = to_start(start).to_datetime() if cstart > start: cstart = to_end(start).to_datetime() cend = to_end(end).to_datetime() if cend < end: cend = to_start(end).to_datetime() if self.root.is_snapshot: delta = to_end(cend) - to_start(cstart) else: delta = to_delta(cend - cstart).round() if not delta: delta = minimum_time_unit book_load = float(work_time) / delta r().book_task(self, cstart, cend, book_load, work_time, True) last = max(end, last) first = min(start, first) self._performed_resource_length = tuple([ (r, v[2]) for r, v in summary.iteritems() ]) self.performed_resource = tuple(summary.keys()) self.performed_end = last self.performed_start = first self.performed_effort = to_delta(effort) self.performed_work_time = to_delta(work_time_sum) self._check_completion() #@-node:_allocate_performed #@+node:_iter_booked_resources def _iter_booked_resources(self): result = dict(map(lambda r: (r, 1), self.performed_resource)) result.update(dict(map(lambda r: (r, 1), self.booked_resource))) return result.iterkeys() #@-node:_iter_booked_resources #@-node:Resource allocation Methods #@+node:Compile Methods #@+node:_generate def _generate(self, deferred=None): do_raise = False deferred = deferred or [ self ] while deferred: new_deferred = [] for task in deferred: task._compile(new_deferred, do_raise) do_raise = deferred == new_deferred deferred = new_deferred #@-node:_generate #@+node:_recalc_properties def _recalc_properties(self): if not self._properties: return self.__compile_function([], False, _MeProxyRecalc(self)) self._is_compiled = True #@-node:_recalc_properties #@+node:_compile def _compile(self, deferred, do_raise): self.dont_inherit = () self._constraint = None self._original_values.clear() self._properties.clear() try: self.__at_compile #@ << raise child recursion error >> #@+node:<< raise child recursion error >> self._raise(RecursionError("A child defines a "\ "recursive definition at %s" % self.path)) #@-node:<< raise child recursion error >> #@nl except AttributeError: self.__at_compile = self, "" try: self.__compile_function(deferred, do_raise, _MeProxy(self)) finally: del self.__at_compile for c in self.children: if not c._is_compiled: c._compile(deferred, do_raise) if self._is_compiled: self.__check_milestone() self.__check_task() self.root.has_actual_data |= self.__dict__.has_key("performed") #@-node:_compile #@+node:__compile_function def __compile_function(self, deferred, do_raise, me_instance): self._is_compiled = self._is_frozen restore_globals = [] globals_ = self._function.func_globals #@ << set function global values >> #@+node:<< set function global values >> def to_value_wrapper(a): if isinstance(a, _ValueWrapper): return a return _ValueWrapper(a, [(None, None)]) def my_max(*args): return max(map(to_value_wrapper, args)) def my_min(*args): return min(map(to_value_wrapper, args)) globals_["me"] = me_instance if self._is_compiled: globals_["up"] = self.up globals_["root"] = self.root else: globals_["up"] = _Path(self.up, "up") globals_["root"] = _Path(self.root, "root") globals_["Delta"] = self._to_delta globals_["Date"] = self._to_start globals_["max"] = my_max globals_["min"] = my_min globals_["add_attrib"] = me_instance.add_attrib #@nonl #@-node:<< set function global values >> #@nl #@ << set me in global functions >> #@+node:<< set me in global functions >> #@+at # Is used for functions like YearlyMax, MonthlyMax, .... #@-at #@@code for name in self._function.global_names: try: obj = globals_[name] if isinstance(obj, types.FunctionType): fg = obj.func_globals if not fg.has_key("me") and "me" in obj.func_code.co_names: restore_globals.append(fg) fg["me"] = me_instance except KeyError: continue #@nonl #@-node:<< set me in global functions >> #@nl try: #@ << eval function >> #@+node:<< eval function >> if do_raise: try: self._function() self._is_compiled = True except _IncompleteError, e: src = e.args[1] if src is not self: self.__at_compile = e.args[1:] src._compile([], True) raise else: try: self._function() self._is_compiled = True except AttributeError, e: #print "AttributeError:", e, self.name, e.is_frozen, do_raise deferred.append(self) except _IncompleteError: #print "_IncompleteError:", id(self), self.name, do_raise deferred.append(self) except RecursionError: self._is_parent_referer = True deferred.append(self) #@nonl #@-node:<< eval function >> #@nl finally: for fg in restore_globals: del fg["me"] #@-node:__compile_function #@-node:Compile Methods #@+node:Setting methods #@+node:_set_attrib def _set_attrib(self, name, value): if value is _NEVER_USED_: return try: value = self._setting_hooks[name](self, name, value) except KeyError: pass if name == "__constraint__": self._constraint = value return if type(value) == types.FunctionType: if value.func_code.co_argcount == 0: #@ << add child task >> #@+node:<< add child task >> try: task = self.__dict__[value.func_name] except KeyError: task = Task(value, value.func_name, self, len(self.children) + 1) self.children.append(task) setattr(self, value.func_name, task) return #@nonl #@-node:<< add child task >> #@nl if name[0] == "_": #private vars will not be set return if isinstance(value, _Path): value = value._task set_method = getattr(self, "_set_" + name, None) if set_method: #@ << set standard attribute >> #@+node:<< set standard attribute >> if type(value) == types.DictionaryType: self.root.all_scenarios.update(value.keys()) value = value.get(self.scenario, value["_default"]) self.__set_sources(name, value) self._original_values[name] = value set_method(_val(value)) #@nonl #@-node:<< set standard attribute >> #@nl else: #@ << set userdefined attribute >> #@+node:<< set userdefined attribute >> if callable( getattr(self.__class__, name, None)): raise NameError('You may not use "%s" as attribute' % name) setattr(self, name, value) self._properties[name] = True self.__set_sources(name, value) #@nonl #@-node:<< set userdefined attribute >> #@nl #@-node:_set_attrib #@+node:read only attributes #@+node:_set_name def _set_name(self, value): raise AttributeError("The attribute 'name' is readonly.") #@nonl #@-node:_set_name #@+node:_set_done def _set_done(self, value): raise AttributeError("The attribute 'done' is readonly.") #@nonl #@-node:_set_done #@+node:_set_performed_work_time def _set_performed_work_time(self, value): raise AttributeError("The attribute 'performed_work_time' is readonly.") #@nonl #@-node:_set_performed_work_time #@+node:_set_booked_resource def _set_booked_resource(self, value): raise AttributeError("The attribute 'booked_resource' is readonly.") #@nonl #@-node:_set_booked_resource #@+node:_set_performed_effort def _set_performed_effort(self, value): raise AttributeError("The attribute 'performed_effort' is readonly.") #@nonl #@-node:_set_performed_effort #@+node:_set_children def _set_children(self, value): raise AttributeError("The attribute 'children' is readonly.") #@nonl #@-node:_set_children #@+node:_set_depth def _set_depth(self, value): raise AttributeError("The attribute 'depth' is readonly.") #@nonl #@-node:_set_depth #@+node:_set_index def _set_index(self, value): raise AttributeError("The attribute 'index' is readonly.") #@nonl #@-node:_set_index #@+node:_set_scenario def _set_scenario(self, value): raise AttributeError("The attribute 'scenario' is readonly.") #@nonl #@-node:_set_scenario #@+node:_set_buffer def _set_buffer(self, value): raise AttributeError("The attribute 'buffer' is readonly.") #@nonl #@-node:_set_buffer #@-node:read only attributes #@+node:_set_start def _set_start(self, value): self.__start_class = value.__class__ self.start = self._to_start(value).round() #@-node:_set_start #@+node:_set_end def _set_end(self, value): self.end = self._to_end(value) #@-node:_set_end #@+node:_set_max_load def _set_max_load(self, max_load): self.max_load = float(max_load) #@-node:_set_max_load #@+node:_set_load def _set_load(self, load): self.load = float(load) #@-node:_set_load #@+node:_set_length def _set_length(self, value): self.length = self._to_delta(value).round() #@-node:_set_length #@+node:_set_effort def _set_effort(self, value): self.effort = self._to_delta(value).round() #@-node:_set_effort #@+node:_set_duration def _set_duration(self, value): self.duration = self._to_delta(value, True).round() #@-node:_set_duration #@+node:_set_complete def _set_complete(self, value): self.complete = value #@-node:_set_complete #@+node:_set_done def _set_done(self, value): self.done = self._to_delta(value).round() #@-node:_set_done #@+node:_set_todo def _set_todo(self, value): self.todo = self._to_delta(value).round() #@-node:_set_todo #@+node:_set_milestone def _set_milestone(self, value): self.milestone = value #@-node:_set_milestone #@+node:_set_resource def _set_resource(self, value): if not value: self.resource = None return if isinstance(value, (tuple, list)): value = reduce(lambda a, b: a & b, value) self.resource = value() #@-node:_set_resource #@+node:_set_copy_src def _set_copy_src(self, value): if isinstance(value, _MeProxy): raise RuntimeError("Cannot copy me.") if not value._is_compiled: raise _IncompleteError(value, "copy_src") if value.resource and not self.resource: self.resource = value.resource if value.balance and not self.balance: self.balance = value.balance copy_parms = ("priority", "todo", "complete", "_constraint", "load", "length", "effort", "duration") for p in copy_parms: v = value.__dict__.get(p) if v: setattr(self, p, v) self.copy_src = value self._properties.update(value._properties) for k in value._properties.iterkeys(): setattr(self, k, getattr(value, k)) #@-node:_set_copy_src #@+node:__set_sources def __set_sources(self, attrib_name, value): #@ << find references >> #@+node:<< find references >> def make_ref(val): if isinstance(val, _ValueWrapper): return val._ref if isinstance(val, Task): return [(val, "")] return [] if isinstance(value, (list, tuple)): sources = _refsum(map(make_ref, value)) else: sources = make_ref(value) #@nonl #@-node:<< find references >> #@nl if not sources: return #track only dependcies within the same project root = self.root sources = [ task.path + "." + attrib for task, attrib in sources if task and task.root is root ] self._sources[attrib_name] = tuple(sources) attr_path = self.path + "." + attrib_name #set dependencies of my sources for d in sources: path, attrib = _split_path(d) task = self.get_task(path) r_d = task._dependencies d_l = r_d.setdefault(attrib, {}) d_l[attr_path] = True #@-node:__set_sources #@+node:Calendar Setters #@+node:_set_calendar def _set_calendar(self, value): self.calendar = value self._to_delta = value.Minutes self._to_start = value.StartDate self._to_end = value.EndDate self.__renew_dates() #@-node:_set_calendar #@+node:__renew_dates def __renew_dates(self): for attrib in ("effort", "start", "end", "length", "todo"): try: self._set_attrib(attrib, self._original_values[attrib]) except KeyError: pass #@-node:__renew_dates #@+node:__make_calendar def __make_calendar(self): if not "calendar" in self.__dict__: cal = self.calendar = self.calendar.clone() self._to_delta = cal.Minutes self._to_start = cal.StartDate self._to_end = cal.EndDate #@nonl #@-node:__make_calendar #@+node:_set_vacation def _set_vacation(self, value): self.__make_calendar() self.calendar.set_vacation(value) self._properties["vacation"] = True self.vacation = value self.__renew_dates() #@-node:_set_vacation #@+node:_set_extra_work def _set_extra_work(self, value): self.__make_calendar() self.calendar.set_extra_work(value) self._properties["extra_work"] = True self.extra_work = value self.__renew_dates() #@-node:_set_extra_work #@+node:_set_working_days def _set_working_days(self, value): if type(value[0]) is str: value = (value, ) self.working_days = value self._properties["working_days"] = True self.__make_calendar() for v in value: day_range = v[0] tranges = tuple(v[1:]) self.calendar.set_working_days(day_range, *tranges) self.__renew_dates() #@nonl #@-node:_set_working_days #@+node:_set_minimum_time_unit def _set_minimum_time_unit(self, value): self.__make_calendar() self.calendar.minimum_time_unit = value self._properties["minimum_time_unit"] = True #@-node:_set_minimum_time_unit #@+node:_get_minimum_time_unit def _get_minimum_time_unit(self): return self.calendar.minimum_time_unit minimum_time_unit = property(_get_minimum_time_unit) #@-node:_get_minimum_time_unit #@+node:_set_working_days_per_week def _set_working_days_per_week(self, value): self.__make_calendar() self.calendar.working_days_per_week = value self._properties["working_days_per_week"] = True #@-node:_set_working_days_per_week #@+node:_get_working_days_per_week def _get_working_days_per_week(self): return self.calendar.working_days_per_week working_days_per_week = property(_get_working_days_per_week) #@-node:_get_working_days_per_week #@+node:_set_working_days_per_month def _set_working_days_per_month(self, value): self.__make_calendar() self.calendar.working_days_per_month = value self._properties["working_days_per_month"] = True #@-node:_set_working_days_per_month #@+node:_get_working_days_per_month def _get_working_days_per_month(self): return self.calendar.working_days_per_month working_days_per_month = property(_get_working_days_per_month) #@-node:_get_working_days_per_month #@+node:_set_working_days_per_year def _set_working_days_per_year(self, value): self.__make_calendar() self.calendar.working_days_per_year = value self._properties["working_days_per_year"] = True #@-node:_set_working_days_per_year #@+node:_get_working_days_per_year def _get_working_days_per_year(self): return self.calendar.working_days_per_year working_days_per_year = property(_get_working_days_per_year) #@-node:_get_working_days_per_year #@+node:_set_working_hours_per_day def _set_working_hours_per_day(self, value): self.__make_calendar() self.calendar.working_hours_per_day = value self._properties["set_working_hours_per_day"] = True #@-node:_set_working_hours_per_day #@+node:_get_working_hours_per_day def _get_working_hours_per_day(self): return self.calendar.working_hours_per_day working_hours_per_day = property(_get_working_hours_per_day) #@-node:_get_working_hours_per_day #@+node:_set_now def _set_now(self, value): proxy = weakref.proxy self.calendar.now = _to_datetime(value) #@-node:_set_now #@-node:Calendar Setters #@-node:Setting methods #@+node:Freezer Methods #@+node:_unfreeze def _unfreeze(self, attrib_name): if self.__dict__.has_key(attrib_name): del self.__dict__[attrib_name] #@-node:_unfreeze #@+node:_wrap_attrib def _wrap_attrib(self, method): attrib_name = method.__name__[7:] recursion_attrib = "_rec" + attrib_name try: dest, dattr = self.__at_compile raise RecursionError("Recursive definition of %s(%s) and %s(%s)." \ % (self.path, attrib_name, dest.path, dattr)) except AttributeError: pass if not self._is_compiled: raise _IncompleteError(self, attrib_name) try: getattr(self, recursion_attrib) raise RecursionError(self, attrib_name) except AttributeError: pass setattr(self, recursion_attrib, True) try: result = method(self) if self._is_frozen: setattr(self, attrib_name, result) return result finally: delattr(self, recursion_attrib) #@-node:_wrap_attrib #@+node:_find_frozen def _find_frozen(self, attrib_name, default=None): value = self.__dict__.get(attrib_name) if value is not None: return value up = self.up return up and up._find_frozen(attrib_name) or default #@-node:_find_frozen #@-node:Freezer Methods #@+node:Calculation Methods #@+node:__calc_performed_effort def __calc_performed_effort(self): if self.children: return self._to_delta(sum([ t.performed_effort for t in self.children ])) return pcalendar.Minutes(0) performed_effort = _TaskProperty(__calc_performed_effort) #@-node:__calc_performed_effort #@+node:__calc_estimated_effort def __calc_estimated_effort(self): if self.children: return self._to_delta(sum([ t.estimated_effort for t in self.children ])) return self.effort estimated_effort = _TaskProperty(__calc_estimated_effort) #@-node:__calc_estimated_effort #@+node:__calc_start def __calc_start(self): to_start = self._to_start if self.children: try: return min([ to_start(t.start) for t in self.children if not t._is_parent_referer ]) except ValueError: #@ << raise child recursion error >> #@+node:<< raise child recursion error >> self._raise(RecursionError("A child defines a "\ "recursive definition at %s" % self.path)) #@-node:<< raise child recursion error >> #@nl try: end = self.end duration = self.__dict__.get("duration") if duration is not None: start = end.to_datetime() - datetime.timedelta(minutes=duration) else: start = end - self.length return to_start(start) except RecursionError: start = self._find_frozen("start") if start: return to_start(start) #@ << raise recursion error >> #@+node:<< raise recursion error >> raise RecursionError("you have to specify a "\ "start or an end at %s" % self.path) #@nonl #@-node:<< raise recursion error >> #@nl start = _TaskProperty(__calc_start) #@-node:__calc_start #@+node:__calc_end def __calc_end(self): to_end = self._to_end if self.children: try: return max([ to_end(t.end) for t in self.children if not t._is_parent_referer ]) except ValueError: #@ << raise child recursion error >> #@+node:<< raise child recursion error >> self._raise(RecursionError("A child defines a "\ "recursive definition at %s" % self.path)) #@-node:<< raise child recursion error >> #@nl try: start = self.start duration = self.__dict__.get("duration") if duration is not None: end = start.to_datetime() + datetime.timedelta(minutes=duration) else: end = start + self.length return to_end(end) except RecursionError: end = self._find_frozen("end") if end: return to_end(end) #@ << raise recursion error >> #@+node:<< raise recursion error >> raise RecursionError("you have to specify a "\ "start or an end at %s" % self.path) #@nonl #@-node:<< raise recursion error >> #@nl end = _TaskProperty(__calc_end) #@-node:__calc_end #@+node:__calc_load def __calc_load(self): length = self.__dict__.get("length") effort = self.__dict__.get("effort") if length is not None and effort is not None: return float(effort) / (float(length) or 1.0) load = self._find_frozen("load") if load is not None: return load return 1.0 load = _TaskProperty(__calc_load) #@-node:__calc_load #@+node:__calc_length def __calc_length(self): effort = self.__dict__.get("effort") if effort is None: return self.end - self.start return self._to_delta(effort / self.load) length = _RoundingTaskProperty(__calc_length, "length") #@-node:__calc_length #@+node:__calc_duration def __calc_duration(self): return self._to_delta(self.end.to_datetime()\ - self.start.to_datetime(), True) duration = _TaskProperty(__calc_duration) #@-node:__calc_duration #@+node:__calc_effort def __calc_effort(self): if self.children: return self._to_delta(sum([ t.effort for t in self.children ])) return self._to_delta(self.length * self.load) effort = _RoundingTaskProperty(__calc_effort, "effort") #@-node:__calc_effort #@+node:__calc_done def __calc_done(self): if self.children: dones = map(lambda t: t.done, self.children) return self._to_delta(sum(dones)) res = self._iter_booked_resources() done = sum(map(lambda r: r.done_of(self), res)) complete = self.__dict__.get("complete") todo = self.__dict__.get("todo") if not done and complete == 100 or todo == 0: #if now is not set done = self.effort return self._to_delta(done) done = _TaskProperty(__calc_done) #@-node:__calc_done #@+node:__calc_buffer def __calc_buffer(self): if self.children: return self._to_delta(min(map(lambda t: t.buffer, self.children))) scenario = self.scenario end = self.end old_end = self.__dict__.get("end") #@ << find all tasks, that depend on my end >> #@+node:<< find all tasks, that depend on my end >> deps = { } task = self while task: deps.update(task._dependencies.get("end", {})) task = task.up #@nonl #@-node:<< find all tasks, that depend on my end >> #@nl #@ << define unfreeze_parents >> #@+node:<< define unfreeze_parents >> def unfreeze_parents(): task = self.up while task: task._unfreeze("end") task = task.up #@nonl #@-node:<< define unfreeze_parents >> #@nl buffers = [ ] for d in deps.keys(): path, attrib = _split_path(d) if attrib != "start": continue #@ << calculate buffer to descendant 'd' >> #@+node:<< calculate buffer to descendant 'd' >> unfreeze_parents() # the following code considers a expressione like # start = predecessor.end + Delta("1d") the buffer # calculation must be aware of the 1d delay. # (therefore a simple succ_start - end would be # incorrect) # Solution: Simluate a later end and calculate the # real delay succ_task = self.get_task(path) simulated_task = Task(succ_task._function, succ_task.name, succ_task.up, 1) current_start = succ_task.start simulated_end = current_start self.end = current_start simulated_task._generate() simulated_start = simulated_task.start unfreeze_parents() if old_end: self.end = old_end else: self._unfreeze("end") del simulated_task current_delay = current_start - end simulated_delay = simulated_start - simulated_end real_delay = current_delay - simulated_delay try: buffer_ = real_delay + succ_task.buffer except RecursionError, err: self._raise(err) #@nonl #@-node:<< calculate buffer to descendant 'd' >> #@nl buffers.append(buffer_) if not buffer_: break if buffers: return self._to_delta(min(buffers)) return not self.milestone \ and self.root.end - end \ or self._to_delta(0) buffer = _TaskProperty(__calc_buffer) #@-node:__calc_buffer #@+node:__calc_complete def __calc_complete(self): done = self.done todo = self.todo return int(100.0 * done / ((done + todo) or 1)) complete = _TaskProperty(__calc_complete) #@-node:__calc_complete #@+node:__calc_todo def __calc_todo(self): complete = self.__dict__.get("complete") if complete: # effort = done + todo # done done # complete = ------ ==> todo = -------- - done # effort complete complete = float(complete) done = self.done if done: done = float(done) return self._to_delta(done * 100.0 / complete - done) return self._to_delta(self.effort * complete / 100.0) if self.children: todos = map(lambda t: t.todo, self.children) return self._to_delta(sum(todos)) todo = sum(map(lambda r: r.todo_of(self), self.booked_resource)) return self._to_delta(max(todo, self.effort - self.done)) todo = _TaskProperty(__calc_todo) #@-node:__calc_todo #@-node:Calculation Methods #@+node:Check Methods #@+node:__check_task def __check_task(self): if self.children: return start = self._find_frozen("start") end = self._find_frozen("end") if not (start or end): self._raise(ValueError("You must specify either a"\ " start or an end attribute")) if start and end: return length = self.__dict__.get("length") duration = self.__dict__.get("duration") effort = self.__dict__.get("effort") if not (effort or length or duration): #set a default value self._set_effort("1d") #self._raise(ValueError("You must specify either a"\ # " length or a duration or "\ # "an effort attribute")) #@-node:__check_task #@+node:__check_milestone def __check_milestone(self): if not self.milestone: return self.length = self._to_delta(0) start = self.__dict__.get("start") if not start: self._raise(ValueError("Milestone must have start attribute"), "milstone") if self.__start_class.__name__ == "edt": #the milestone is probably dependent on the end date of #an other task (see edt in pcalendar) ==> start at the end date self.start = self.end = self._to_end(self.start) else: self.start = self.end = self._to_start(self.start) #@-node:__check_milestone #@+node:_check_completion def _check_completion(self): if not self.performed_effort: return if self.root.is_snapshot: return # allocation is not done yet ==> self.todo, self.done, # self.complete cannot be calculated if self._find_frozen("complete", 0) < 100 \ and self.__dict__.get("todo", 1) > 0: return start = self.performed_start end = self.performed_end #ensure that self.start.to_datetime() < self.end.to_datetime() cstart = self._to_start(start) if cstart.to_datetime() > start: cstart = self._to_end(start) cend = self._to_end(end) if cend.to_datetime() < end: cend = self._to_start(end) self.start = cstart self.end = cend if self.performed_effort != self.effort: self.estimated_effort = self.effort self.effort = self.performed_effort #@-node:_check_completion #@+node:check def check(self): if self._constraint and self._is_compiled: globals_ = self._function.func_globals globals_["me"] = self globals_["up"] = self.up globals_["root"] = self.root globals_["assert_"] = self.__assert self._constraint() #@-node:check #@-node:Check Methods #@+node:Error Methods #@+node:__assert def __assert(self, value): if not value: warnings.warn('Assertion in scenario: "%s".' % self.scenario, RuntimeWarning, 2) #@-node:__assert #@+node:_warn def _warn(self, message, attrib=None, level=2): self.__compile_function([], True, _MeProxyWarn(self, attrib, message)) #@-node:_warn #@+node:_raise def _raise(self, exc, attrib=None): self.__compile_function([], True, _MeProxyError(self, attrib, exc)) raise exc #@-node:_raise #@-node:Error Methods #@-others #@nonl #@-node:class Task #@-node:Task #@+node:Projects #@+node:class _ProjectBase class _ProjectBase(Task): """ Base class for all projects. """ #@ << class _ProjectBase declarations >> #@+node:<< class _ProjectBase declarations >> __attrib_completions__ = { } __attrib_completions__.update(Task.__attrib_completions__) del __attrib_completions__["milestone"] #project cannot be milestones priority = 500 efficiency = 1.0 max_load = 1.0 balance = 0 resource = None copy_src = None has_actual_data = False is_snapshot = False #@-node:<< class _ProjectBase declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, top_task, scenario="_default", id=""): self.calendar = pcalendar.Calendar() Task.__init__(self, top_task, top_task.func_name) self.id = id or self.name self.scenario = scenario self.all_scenarios = set(("_default",)) self.path = "root" self._globals = top_task.func_globals.copy() self._generate() #@-node:__init__ #@+node:_idendity_ def _idendity_(self): return self.id #@-node:_idendity_ #@+node:_restore_globals def _restore_globals(self): self._function.func_globals.clear() self._function.func_globals.update(self._globals) del self._globals #@-node:_restore_globals #@+node:free def free(self): all_resources = self.all_resources() for r in all_resources: r().unbook_tasks_of_project(self.id, self.scenario) for t in self: t.booked_resource = () return all_resources #@-node:free #@+node:_get_balancing_list def _get_balancing_list(self): try: cached_list = balancing_cache[self._function.org_code] if len(cached_list) != len(tuple(self)): # different scenarios can have different tasks raise KeyError() except KeyError: cached_list = _build_balancing_list(self) balancing_cache[self._function.org_code] = cached_list else: cached_list = [ self.get_task(t.path) for t in cached_list ] return cached_list #@-node:_get_balancing_list #@+node:snapshot def snapshot(self, indent="", name=None): text = Task.snapshot(self, indent, name) lines = text.splitlines(True) indent += " " def make_resource(r): return '%sclass %s(Resource): title = "%s"\n' \ % (indent, r.name, r.title) now = datetime.datetime.now().strftime("%x %H:%M") resource_text = map(lambda r: make_resource(r), self.all_resources()) lines.insert(1, "%sfrom faces import Resource\n" % indent) lines.insert(2, "".join(resource_text) + "\n") lines.insert(3, '%snow = "%s"\n' % (indent, now)) lines.insert(4, '%sis_snapshot = True\n' % indent) return "".join(lines) #@-node:snapshot #@-others #@-node:class _ProjectBase #@+node:class Project class Project(_ProjectBase): """ Generates a Project without allocating resources. @param top_task: Specifies the highest function of a project definiton. @param scenario: Specifies the name of the scenario which should be scheduled. @param id: Specifiess a unique idenfication name to distinguish the project from other projects in the resource database. The default value for id is the name of top_task. """ #@ << class Project declarations >> #@+node:<< class Project declarations >> __call_completion__ = 'Project(|top_task, scenario="_default", id=None)' #@-node:<< class Project declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, top_task, scenario="_default", id=None): _ProjectBase.__init__(self, top_task, scenario, id) no_snapshot = not self.is_snapshot for t in self: t._is_frozen = True t._recalc_properties() no_snapshot and t.check() self._restore_globals() #@-node:__init__ #@-others #@-node:class Project #@+node:class _AllocationPoject class _AllocationPoject(_ProjectBase): #@ @+others #@+node:unfreeze_parents def unfreeze_parents(self): if self.has_actual_data: for t in filter(lambda t: t.children, self): if not t._original_values.has_key("start"): t._unfreeze("start") if not t._original_values.has_key("end"): t._unfreeze("end") #@-node:unfreeze_parents #@-others #@-node:class _AllocationPoject #@+node:class BalancedProject class BalancedProject(_AllocationPoject): """ Generates a project with allocated resources. The tasks are balanced to fit the resources load conditions. """ #@ << class BalancedProject declarations >> #@+node:<< class BalancedProject declarations >> __call_completion__ = """BalancedProject(|top_task, scenario="_default", id=None, balance=SMART, performed=None)""" #@-node:<< class BalancedProject declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, top_task, scenario="_default", id=None, balance=SMART, performed=None): _AllocationPoject.__init__(self, top_task, scenario, id) self.balance = balance if performed: self._distribute_performed(performed) self.has_actual_data = True no_snapshot = not self.is_snapshot if no_snapshot: self.allocate() else: self.allocate_snapshot() for t in self: t._is_frozen = True t._recalc_properties() no_snapshot and t.check() self._restore_globals() #@nonl #@-node:__init__ #@+node:allocate_snapshot def allocate_snapshot(self): all_resources = self.free() scenario = self.scenario has_actual_data = True for t in self: if not t.resource or t.milestone or t.children: continue t._convert_performed(all_resources) t._allocate_performed(t._performed) #@-node:allocate_snapshot #@+node:allocate def allocate(self): all_resources = self.free() balancing_list = self._get_balancing_list() scenario = self.scenario #for t in balancing_list: # print t.path for t in balancing_list: t._compile([], True) if not t.resource or t.milestone or t.children: continue if t._convert_performed(all_resources): has_actual_data = True try: t._allocate_performed(t._performed) except AttributeError: pass allocator = _allocators[t.balance] min_val = None min_state = None for p in range(t.resource._permutation_count()): state = t._test_allocation(p, allocator) if not state: continue to_minimize = state[0] if not min_val or min_val > to_minimize: min_val = to_minimize min_state = state if min_state: t._allocate(min_state, allocator) elif t.performed_start: # t could not be allocated ==> # performance data holds all information t.start = t._to_start(t.performed_start) t.end = t._to_end(t.performed_end) self.unfreeze_parents() #@-node:allocate #@+node:_distribute_performed def _distribute_performed(self, performed): project_id = self._idendity_() plen = len(project_id) performed = filter(lambda item: item[0].startswith(project_id), performed) performed.sort() task = None for item in performed: path = item[0] rpath = "root" + path[plen:] task = self.get_task(rpath) if not task: #@ << extract task in activity path >> #@+node:<< extract task in activity path >> #@+at # A performed path can have sub activities appended to the # task path. # like: # # root.parent1.parent2.task.subactivity # # here rhe correct task path is: # # root.parent1.parent2.task # #@-at #@@code orpath = rpath while not task: #path can specify a sub module #find the correct path to the module try: last_dot = rpath.rindex(".", 0, len(rpath)) except ValueError: break rpath = rpath[:last_dot] task = self.get_task(rpath) item = list(item) item.append(orpath[len(rpath):]) #@nonl #@-node:<< extract task in activity path >> #@nl if not task or task.children: self._warn("The performance data contain " "a task with id '%s'. But such " "a task does not exist in your " "project." % path) continue if not isinstance(task.performed, list): task.performed = list(task.performed) task.performed.append(item[1:]) #@nonl #@-node:_distribute_performed #@-others #@-node:class BalancedProject #@+node:class AdjustedProject class AdjustedProject(_AllocationPoject): """ Generates a project with allocated resources. The tasks are adjusted to the actual tracking data and balanced to fit the resources load conditions. """ #@ << class AdjustedProject declarations >> #@+node:<< class AdjustedProject declarations >> __call_completion__ = 'AdjustedProject(|base_project)' #@-node:<< class AdjustedProject declarations >> #@nl #@ @+others #@+node:__init__ def __init__(self, base_project): _AllocationPoject.__init__(self, base_project._function, base_project.scenario, base_project.id) self.balance = base_project.balance self.has_actual_data = base_project.has_actual_data self.allocate(base_project) for t in self: t._is_frozen = True t._recalc_properties() t.check() self._restore_globals() #@-node:__init__ #@+node:allocate def allocate(self, base): balancing_list = self._get_balancing_list() scenario = self.scenario cal = self.calendar now = cal.now #for t in balancing_list: # print t.path #@ << free the resources, we have to rebook >> #@+node:<< free the resources, we have to rebook >> for t in balancing_list: src = base.get_task(t.path) if src.end > now or src.complete < 100: for r in src._iter_booked_resources(): r.unbook_task(src) #@nonl #@-node:<< free the resources, we have to rebook >> #@nl for t in balancing_list: src = base.get_task(t.path) if src.end <= now and src.complete == 100: #@ << copy the attribs of complete tasks >> #@+node:<< copy the attribs of complete tasks >> t.effort = src.effort t.load = src.load t.start = src.start t.end = src.end t.done = src.done t.todo = src.todo t.booked_resource = src.booked_resource t.performed_resource = src.performed_resource t._unfreeze("length") t._unfreeze("duration") #@nonl #@-node:<< copy the attribs of complete tasks >> #@nl continue t._compile([], True) if not t.resource or t.milestone or t.children: continue # now allocate the uncomplete tasks #@ << allocate performed data >> #@+node:<< allocate performed data >> try: t._performed = src._performed t._allocate_performed(t._performed) except AttributeError: pass #@nonl #@-node:<< allocate performed data >> #@nl allocator = _allocators[t.balance] if src.start >= now: #@ << allocate tasks, that have not begun yet >> #@+node:<< allocate tasks, that have not begun yet >> min_val = None min_state = None for p in range(t.resource._permutation_count()): state = t._test_allocation(p, allocator) if not state: continue to_minimize = state[0] if not min_val or min_val > to_minimize: min_val = to_minimize min_state = state if min_state: t._allocate(min_state, allocator) elif t.performed_start: t.start = t._to_start(t.performed_start) t.end = t._to_end(t.performed_end) #@-node:<< allocate tasks, that have not begun yet >> #@nl else: #@ << allocate tasks, that are allready at work >> #@+node:<< allocate tasks, that are allready at work >> if t.__dict__.has_key("effort"): t.effort = t._to_delta(src.done + src.todo).round() resource = src.booked_resource or src.performed_resource state = allocator.test_allocation(t, resource) if state: t._allocate(state, allocator) #@nonl #@-node:<< allocate tasks, that are allready at work >> #@nl self.unfreeze_parents() #@nonl #@-node:allocate #@-others #@-node:class AdjustedProject #@-node:Projects #@-others """ Atttribute mit Bedeutung: calendar -------- minimum_time_unit |int in minutes| working_days_per_week |int in days | working_days_per_month|int in days | working_days_per_year |int in days | working_hours_per_day |int in hours | vacation | [ one_day, (from, to), .. ] | working_days now Task ----- load start end length effort duration resource booked_resource milestone complete done todo priority efficiency buffer children depth index path dont_inherit performed_effort performed_end performed_start sum() min() max() costs() indent_name() max_load copy_src (set: copy all attributes of another task get: reference of copy) balance for gantt ----- line accumulate Resource ---------- efficiency load vacation max_load """ #@-node:@file task.py #@-leo # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: