def _schedule_block(self, block, start, alloc, par): logger.debug(f"_schedule_block: scheduling block in {block.loc}: " f"start={start}, alloc={alloc}, par={par}") alloc = block.alloc or alloc par = block.parallel or par latest = start if block.goal_name is None: for b in block.blocks: last = self._schedule_block(b, start, alloc, par) latest = max(latest or last, last) if block.seq: start = last else: assert not block.blocks, "block with goals should have no subblocks" goal = self.net.name_to_goal.get(block.goal_name) error_if(goal is None, block.loc, f"goal '{block.goal_name}' not found in plan") goal_finish = self._schedule_goal(goal, start, alloc, par) latest = max(latest, goal_finish) if block.deadline is not None and latest > block.deadline: warn( "Failed to schedule block at {block.loc} before deadline {block.deadline}" ) return latest
def update_name_to_goal(g): self.name_to_goal[g.name] = g if g.id is not None: other_goal = self.name_to_goal.get(g.id, None) error_if(other_goal is not None and other_goal.name != g.name, f"goals '{other_goal.name}' and '{g.name}' use the same id '{g.id}'") self.name_to_goal[g.id] = g
def open_file(filename): """Open file with appropriate reader.""" if sys.platform == 'cygwin': rc = os.system(f'cygstart {filename}') elif sys.platform.startswith('win'): rc = os.system(f'explorer {filename}') else: rc = os.system(f'xdg-open {filename}') error_if(rc != 0, f"failed to open pdf file '{filename}'")
def read_date(s, loc): """Parse date duration e.g. "2020-01-10".""" # TODO: allow shorter formats (e.g. 'Jan 10') # but what if someone uses this in 2020?! m = re.search(r'^\s*([^-\s]*-[^-\s]*)(-[^-\s]*)?\s*(.*)', s) error_if(m is None, loc, f"failed to parse date: {s}") date_str = m.group(1) # If day is omitted, consider first day date_str += m.group(2) or '-01' return datetime.datetime.strptime(date_str, '%Y-%m-%d').date(), m.group(3)
def get_resources(self, names): """Returns resources that match a set of team/resource names.""" resources = [] for name in names: team = self.teams_map.get(name) if team is not None: for rc in team.members: if rc not in resources: resources.append(rc) else: rc = self.members_map.get(name) error_if(rc is None, f"resource '{name}' not defined") if rc not in resources: resources.append(rc) resources = sorted(resources, key=lambda rc: rc.name) return resources
def _recompute(self): self.members_map = {m.name : m for m in self.members} self.teams_map = {t.name : t for t in self.teams} if 'all' in self.teams_map: error(self.teams_map['all'].loc, "predefined goal 'all' overriden") self.teams_map['all'] = Team('all', self.members, self.loc) # Resolve resources for team in self.teams: error_if(team.name in self.members_map, team.loc, f"team '{team.name}' clashes with developer '{team.name}'") for i, name in enumerate(team.members): if not isinstance(name, str): continue m = self.members_map.get(name) error_if(m is None, team.loc, f"no member with name '{name}'") team.members[i] = m
def read_effort(s, loc): """Parse effort estimate e.g. "1h" or "3d".""" m = re.search(r'^\s*([0-9]+(?:\.[0-9]+)?)([hdwmy])\s*(.*)', s) error_if(m is None, f"failed to parse effort: {s}") d = float(m.group(1)) spec = m.group(2) rest = m.group(3) if spec == 'd': d *= 8 elif spec == 'w': d *= 5 * 8 # Work week elif spec == 'm': d *= 22 * 8 # Work month elif spec == 'y': d *= 12 * 22 * 8 # Work year d = int(round(d)) return d, rest
def parse_checks(self, g, goal_offset): while True: l = self.lex.next_if(LexemeType.CHECK) if l is None: return logger.debug(f"parse_checks: new check: {l}") check_offset, status, text = l.data error_if(check_offset != goal_offset, l.loc, "check is not properly nested") error_if(status not in {'X', 'F', ''}, l.loc, f"unexpected check status: '{status}'") check = G.Condition(text, status, l.loc) g.add_check(check) if self.lex.next_if(LexemeType.ATTR_START) is not None: a = self.parse_attrs() check.add_attrs(a, l.loc)
def parse(self, W): net_loc = project_loc = sched_loc = Location() root_goals = [] root_blocks = [] while True: l = self.lex.peek() if l is None: break logger.debug(f"parse: next lexeme: {l}") if l.type == LexemeType.GOAL: error_if(l.data[0] != 0, l.loc, f"root goal '{l.data[1]}' must be left-adjusted") goal = self.parse_goal(l.data[0], None, False) if not net_loc: net_loc = goal.loc root_goals.append(goal) elif l.type == LexemeType.PRJ_ATTR: if not project_loc: project_loc = l.loc self.parse_project_attr() elif l.type == LexemeType.SCHED: error_if(l.data[0] != 0, l.loc, "root block must be left-adjusted") block = self.parse_sched_block(l.data[0]) if not sched_loc: sched_loc = block.loc root_blocks.append(block) elif l.type == LexemeType.EOF: break else: # TODO: anonymous goals error(l.loc, f"unexpected lexeme: '{l.text}'") net = G.Net(root_goals, W, net_loc) prj = project.Project(project_loc) prj.add_attrs(self.project_attrs) sched = schedule.SchedPlan(root_blocks, sched_loc) return net, prj, sched
def parse_goal(self, offset, other_goal, is_pred, allow_empty=False): logger.debug(f"parse_goal: start lex: {self.lex.peek()}") loc = self.lex.loc() goal_name, goal_attrs = self.maybe_parse_goal_decl(offset) if goal_name is None: if not allow_empty: return None # The infamous PERT dummy goals goal = self._make_dummy_goal(loc) was_defined = False logger.debug("parse_goal: creating dummy goal") else: goal = self.names.get(goal_name) if goal is None: was_defined = False goal = self.names[goal_name] = G.Goal(goal_name, loc) logger.debug(f"parse_goal: parsed new goal: {goal.name}") else: was_defined = goal.defined logger.debug(f"parse_goal: parsed existing goal: {goal.name}") if goal_attrs: error_if( was_defined, loc, f"duplicate definition of goal '{goal.name}' " f"(previous definition was in {goal.loc})") goal.add_attrs(goal_attrs, loc) # TODO: Gaperton's examples contain interwined checks and deps self.parse_checks(goal, offset) self.parse_subgoals(goal, offset) if not was_defined and (goal.checks or goal_attrs or goal.children): goal.defined = True if other_goal is not None and is_pred: other_goal.add_child(goal) return goal
def _create_wbs_iterative(net, ids): user_iters = list(filter(lambda i: i is not None, net.iter_to_goals.keys())) error_if(not user_iters, "no iterations defined in plan") user_iters.sort() last_iter = (user_iters[-1] + 1) if user_iters else 0 tasks = [] for i in user_iters + [None]: i_num = last_iter if i is None else i task = Task(f'iter_{i_num}', f'Iteration {i_num}', None) task.depends.add(f'iter_{i_num - 1}') tasks.append(task) for g in net.iter_to_goals[i]: if not _is_goal_ignored(g): t = _create_goal_task(g, task, ids) task.subtasks.append(t) return WBS(tasks)
def read_float(s, loc): """Parse float number.""" m = re.search(r'^\s*([0-9]+(\.[0-9]+)?)(.*)', s) error_if(m is None, loc, f"failed to parse float: {s}") return float(m.group(1)), m.group(3)
def expect_one_value(loc, name, vals): error_if( len(vals) != 1, loc, f"too many values for attribute '{name}': " + ', '.join(vals))
def set_completion_date(self, goal, d): error_if(self.is_completed(goal), f"goal '{goal.name}' scheduled more than once") self.goals[goal.name] = GoalInfo(goal.name, d)
def add_attrs(self, attrs, loc): attrs = add_common_attrs(loc, self, attrs) error_if(attrs, loc, "unknown condition attribute(s): " + ', '.join(attrs))
def enter(g, path=path): # pylint: disable=dangerous-default-value error_if(g.name in path, "found a cycle: %s" % '\n '.join(path)) path.append(g.name)
def set_duration(self, act, iv, alloc): error_if(self.is_done(act), f"activity '{act.name}' scheduled more than once") self.acts[act.name] = ActivityInfo(act, iv, alloc)