Example #1
0
def read_fraction(s, loc):
    if M.search(r'^[0-9.]+$', s):
        return float(s)
    if M.search(r'^([0-9]+)%$', s):
        return int(M.group(1)) / 100
    error(loc, f"unexpected fraction syntax: {s}")
    raise ValueError("silly Pylint fails to understand NoReturn")
Example #2
0
def export(net, dump=False):
  """Generate PERT chart in Graphviz from declarative plan."""

  p = PR.SourcePrinter(io.StringIO())

  p.writeln('digraph G {')
  with p:
    p.write('''\
#graph [rankdir = LR, splines = ortho]
#graph [rankdir = LR, concentrate = true]
graph [rankdir = LR]
''')
    net.visit_goals(callback=lambda g: _print_node(g, p))
    p.writeln('')
    net.visit_goals(callback=lambda g: _print_node_edges(g, p))
  p.writeln('}')

  if dump:
    print(p.out.getvalue())
  else:
    gv_file = 'pert.gv'
    pdf_file = 'pert.pdf'

    with open(gv_file, 'wt') as out:
      out.write(p.out.getvalue())

    try:
      with open(pdf_file, 'wb') as f:
        if subprocess.call(['dot', '-Tpdf', gv_file], stdout=f) != 0:
          error("failed to run dot(1); do you have Graphviz installed?")
    except FileNotFoundError:
      error("dot program not found; do you have Graphviz installed?")

    platform.open_file(pdf_file)
Example #3
0
 def add_attrs(self, attrs, loc):
   for a in attrs:
     if a[0].isdigit():
       self.efficiency = P.read_fraction(a, loc)
     elif M.search(r'vacations?\s+(.*)', a):
       duration = P.read_date2(M.group(1), loc)
       self.vacations.append(duration)
     else:
       error(loc, f"unexpected resource attribute: {a}")
Example #4
0
 def expect(self, type):
     """Return current lexeme and advance if type matches."""
     l = self.next()
     if isinstance(type, list):
         if l.type not in type:
             type_str = ', '.join(map(lambda t: f"'{type}'"))
             error(l.loc, f"expecting '{type_str}', got '{l.type}'")
     elif l.type != type:
         error(l.loc, f"expecting '{type}', got '{l.type}'")
     return l
Example #5
0
 def next_internal(self):
     if self.line == '':
         # File exhausted
         type = LexemeType.EOF
         data = text = ''
     elif self.mode == LexerMode.ATTR:
         nest = 0
         self.line = self.line.lstrip()
         if self.line[0] == ',':
             type = ','
             i = 1
         else:
             type = LexemeType.LIST_ELT
             for i, c in enumerate(self.line):
                 if c == ',' and not nest:
                     break
                 if c == '(':
                     nest += 1
                 elif c == ')':
                     nest -= 1
             else:
                 i += 1
                 self.mode = LexerMode.NORMAL
         text = self.line[:i]
         data = text.rstrip()
         self.line = self.line[i:]
     else:
         data = None
         if M.match(r'( *)\|([<>])-', self.line):
             type = LexemeType.LARROW if M.group(
                 2) == '<' else LexemeType.RARROW
             data = len(M.group(1))
         elif M.match(r'( *)\|\[([^\]]*)\]\s*(.*?)(?=(//|$))', self.line):
             type = LexemeType.CHECK
             data = len(M.group(1)), M.group(2), M.group(3).strip()
         elif M.match(r'^(\s*)(--|\|\|)\s*', self.line):
             type = LexemeType.SCHED
             data = len(M.group(1)), M.group(2)
         elif M.match(r'( *)\|(.*?)(?=//|$)', self.line):
             type = LexemeType.GOAL
             data = len(M.group(1)), M.group(2).strip()
         elif M.match(r'\s*//', self.line):
             type = LexemeType.ATTR_START
             self.mode = LexerMode.ATTR
         elif M.match(r'([A-Za-z][A-Za-z0-9_]*)(?=\s*=)', self.line):
             type = LexemeType.PRJ_ATTR
             data = M.group(1)
         elif M.match(r'\s*=\s*', self.line):
             type = LexemeType.ASSIGN
             self.mode = LexerMode.ATTR
         else:
             error(self._loc(), f"unexpected syntax: {self.line}")
         self.line = self.line[len(M.group(0)):]
         text = M.group(0)
     self.lexemes.append(PA.Lexeme(type, data, text, self._loc()))
Example #6
0
def read_alloc(a, loc):
    """Parse allocation directive e.g. "@dev1/dev2 (dev3)"."""
    aa = a.split('(')
    if len(aa) > 2 or not M.search(r'^@\s*(.*)', aa[0]):
        error(loc, f"unexpected allocation syntax: {a}")
    alloc = M.group(1).strip().split('/')
    if len(aa) <= 1:
        real_alloc = []
    else:
        if not M.search(r'^([^)]*)\)', a):
            error(loc, f"unexpected allocation syntax: {a}")
        real_alloc = M.group(1).strip().split('/')
    return alloc, real_alloc
Example #7
0
 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
Example #8
0
  def add_attrs(self, attrs, loc):
    attrs = add_common_attrs(loc, self, attrs)

    for a in attrs:
      if re.search(r'^[0-9.]+[hdwmy]', a):
        # Parse estimate
        self.effort = PA.read_eta(a, loc)
        continue

      if a.startswith('@'):
        self.alloc, self.real_alloc = PA.read_alloc(a, loc)
        continue

      # TODO: specify in effort attribute?
      if M.search(r'^[0-9]{4}-', a):
        self.duration = PA.read_date2(a, loc)
        continue

      if M.match(r'^id\s+(.*)', a):
        self.id = M.group(1)

      if M.match(r'over\s+(\S+)\s+(.*)', a):
        other_id = M.group(1)
        overlap, a = PA.read_float(M.group(2), loc)
        if a == '%':
          overlap /= 100
        self.overlaps[other_id] = overlap

      if a.startswith('||'):
        self.parallel = PA.read_par(a)
        continue

      if not M.search(r'^([a-z_0-9]+)\s*(.*)', a):
        error(loc, f"failed to parse attribute: {a}")
      k = M.group(1).strip()
      #v = M.group(2).strip()

      if k == 'global':
        self.globl = True
        continue

      error(loc, f"unknown activity attribute: '{k}'")
Example #9
0
    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
Example #10
0
def read_eta(s, loc):
    """Parse effort estimate e.g. "1h", "1h-3d" or "1h-3d (1d)"."""

    min, rest = read_effort(s, loc)

    max = min
    if rest and rest[0] == '-':
        max, rest = read_effort(rest[1:], loc)

    real = None
    completion = 0
    if M.search(r'^\s*\((.*)\)\s*$', rest):
        for a in re.split(r'\s*,\s*', M.group(1)):
            if re.search(r'^[0-9.]+[hdwmy]', a):
                real, _ = read_effort(a, loc)
            elif M.search(r'^([0-9]+)%', a):
                completion = float(M.group(1)) / 100
            else:
                error(loc, f"unknown ETA attribute: {a}")

    return ETA(min, max, real, completion)
Example #11
0
  def check(self, W):
    """Verify invariants."""

    if W and not self.defined and not self.dummy:
      warn(self.loc, f"goal '{self.name}' is undefined")

    pending_conds = [c.name for c in self.checks if not c.done()]
    if W and self.completion_date is not None \
        and self.completion_date <= datetime.date.today() \
        and pending_conds:
      warn(self.loc,
           "goal '%s' marked as completed but some checks are still pending:\n  %s"
           % (self.name, '\n  '.join(pending_conds)))

    if W and self.is_completed() and not self.completion_date:
      warn(self.loc, f"goal '{self.name}' marked as completed but is missing tracking data")

    for act in self.global_preds:
      if not act.is_instant():
        error(act.loc, "global dependencies must be instant")

      if W and not act.head:
        warn(act.loc, f"goal '{self.name}' has empty global dependency")

    for act in self.preds:
      act.check(W)

      if act.head and self.iter is not None and act.head.iter is None:
        warn(self.loc,
             f"goal has been scheduled but one of it's dependents is not: "
             f"'{act.head.name}'")

      if self.is_completed() and (not act.duration or not act.effort.real):
        warn(self.loc,
             f"goal '{self.name}' is achieved but "
             f"one of it's actions is missing tracking data")
Example #12
0
    def add_attrs(self, attrs, loc):
        for a in attrs:
            if a.startswith('@'):
                self.alloc, _ = PA.read_alloc(a, loc)
                continue

            if M.search(r'^[0-9]{4}-', a):
                self.duration = PA.read_date2(a, loc)
                continue

            if a.startswith('||'):
                self.parallel = PA.read_par(a)
                continue

            if not M.search(r'^([a-z_0-9]+)\s*(.*)', a):
                error(loc, f"failed to parse block attribute: {a}")
            k = M.group(1).strip()
            v = M.group(2).strip()

            if k == 'deadline':
                self.deadline, _ = PA.read_date(v, loc)
                continue

            error(loc, f"unknown block attribute '{k}'")
Example #13
0
    def parse_project_attr(self):
        l = self.lex.next()
        name = l.data
        attr_loc = l.loc

        self.lex.expect('=')

        rhs = []
        while True:
            l = self.lex.expect('LIST_ELT')
            rhs.append(l.data)
            if not self.lex.next_if(','):
                break

        def expect_one_value(loc, name, vals):
            error_if(
                len(vals) != 1, loc,
                f"too many values for attribute '{name}': " + ', '.join(vals))

        if name in {'name', 'tracker_link', 'pr_link'}:
            expect_one_value(l.loc, name, rhs)
            val = rhs[0]
        elif name in {'start', 'finish'}:
            expect_one_value(l.loc, name, rhs)
            val, _ = PA.read_date(rhs[0], attr_loc)
        elif name == 'members':
            val = []
            for rc_info in rhs:
                if not M.match(r'([A-Za-z][A-Za-z0-9_]*)\s*(?:\(([^\)]*)\))?',
                               rc_info):
                    error(attr_loc,
                          f"failed to parse resource declaration: {rc_info}")
                rc_name, attrs = M.groups()
                rc = project.Resource(rc_name, attr_loc)
                if attrs:
                    rc.add_attrs(re.split(r'\s*,\s*', attrs), attr_loc)
                val.append(rc)
        elif name == 'teams':
            val = []
            for team_info in rhs:
                if not M.match(r'\s*([A-Za-z][A-Za-z0-9_]*)\s*\(([^)]*)\)$',
                               team_info):
                    error(attr_loc, f"invalid team declaration: {team_info}")
                team_name = M.group(1)
                rc_names = re.split(r'\s*,\s*', M.group(2).strip())
                val.append(project.Team(team_name, rc_names, attr_loc))
        elif name == 'holidays':
            val = []
            for s in rhs:
                iv = PA.read_date2(s, attr_loc)
                val.append(iv)
        else:
            error(attr_loc, f"unknown project attribute: {name}")

        self.project_attrs[name] = val
Example #14
0
  def add_attrs(self, attrs, loc):
    attrs = add_common_attrs(loc, self, attrs)

    for a in attrs:
      if a.find('!') == 0:
        try:
          self.prio = Priority(int(a[1:]))
        except ValueError:
          error(loc, f"invalid priority value: {a}")
        continue

      if a.find('?') == 0:
        try:
          self.risk = Risk(int(a[1:]))
        except ValueError:
          error(loc, f"invalid risk value: {a}")
        continue

      if M.search(r'^I[0-9]+$', a):
        self.iter = int(a[1:])
        continue

      if M.search(r'^[0-9]{4}-', a):
        self.completion_date, _ = PA.read_date(a, loc)
        continue

      if not M.search(r'^([a-z_0-9]+)\s*(.*)', a):
        error(loc, f"failed to parse goal attribute: {a}")
      k = M.group(1).strip()
      v = M.group(2).strip()

      if k == 'deadline':
        self.deadline, _ = PA.read_date(v, loc)
        continue

      if k == 'id':
        self.id = v
        continue

      error(loc, f"unknown goal attribute '{k}'")
Example #15
0
def export(net, goal, duration, dump):
    """Generate gnuplot chart for all children of goal within time interval."""

    counts = defaultdict(int)
    counts[duration.start] = 0
    partial = [0]
    total_children = [0]

    def scan_completed(g):
        total_children[0] += 1  # TODO: skip milestones?
        if g.is_completed() and g.is_scheduled():
            counts[g.completion_date] += 1
        else:
            partial[0] += g.complete() / 100.0

    net.visit_goals([goal], callback=scan_completed, hierarchical=True)

    # Also count partially completed tasks
    today = datetime.date.today()
    if today < duration.finish:
        counts[today] = int(partial[0])

    sorted_dates = sorted(counts.keys())

    prev = 0
    for date in sorted_dates:
        counts[date] += prev
        prev = counts[date]

    p = PR.SourcePrinter(io.StringIO())

    p.writeln('set terminal png')

    p.write(f'''
reset

set title "{goal.name} (burndown chart)"
set timefmt "%Y-%m-%d"

set xlabel "Days"
set format x "%b-%d"
set xdata time
set xrange ["{duration.start}":"{duration.finish}"]
set xtics nomirror

set ylabel "#Goals"
set yrange [0:{total_children[0]}]
set ytics mirror

plot "-" using 1:2 title 'Real' with lines, "-" using 1:2 title "Planned" with lines
''')

    for date in sorted_dates:
        n = counts[date]
        left = total_children[0] - n
        p.writeln(f'  {date} {left}')
    p.writeln('e')

    p.writeln(f'  {duration.start} {total_children[0]}')
    p.writeln(f'  {duration.finish} {0}')
    p.writeln('e')

    if dump:
        print(p.out.getvalue())
    else:
        png_file = 'burndown.png'

        try:
            with open(png_file, 'w') as f:
                if subprocess.call(['gnuplot', '-'], stdin=p.out,
                                   stdout=f) != 0:
                    error(
                        "failed to run gnuplot(1); do you have Gnuplot installed?"
                    )
        except FileNotFoundError:
            error("gnuplot program not found; do you have Gnuplot installed?")

        platform.open_file(png_file)
Example #16
0
def export(prj, wbs, est, dump=False):
  """Generate TaskJuggler plan from declarative plan."""

  today = datetime.date.today()

  p = PR.SourcePrinter(io.StringIO())

  # Print header
  # (based upon http://www.taskjuggler.org/tj3/manual/Tutorial.html)

  p.write(f'''\
project "{prj.name}" {prj.start} - {prj.finish} {{
  timeformat "{time_format}"
  now {today.strftime(time_format)}
  timezone "Europe/Moscow"
  currency "USD"
  extend task {{
    reference JiraLink "Tracker link"
  }}
}}

flags internal

''')

  # Print holidays
  # TODO: additional holidays in plan

  for y in range(prj.start.year, prj.finish.year + 1):
    for name, dates in [
        ('New year holidays', '01-01 + 8d'),
        ('Army day',          '02-23'),
        ('Womens day',        '03-08'),
        ('May holidays',      '05-01 + 2d'),
        ('Victory day',       '05-09'),
        ('Independence day',  '06-12'),
        ('Unity day',         '11-04')]:
      p.writeln(f'leaves holiday "{name} {y}" {y}-{dates}')
  p.writeln('')

  # Print resources

  p.writeln('resource dev "Developers" {')
  for dev in prj.members:
    p.writeln(f'  resource {dev.name} "{dev.name}" {{')
    p.writeln(f'    efficiency {dev.efficiency}')
    for iv in dev.vacations:
      p.writeln(f'    vacation {iv.start} - {iv.finish}')
    p.writeln('  }')
  p.writeln('}')

  # Print WBS

  abs_ids = {}
  def cache_abs_id(task):
    if task.parent is None:
      abs_ids[task.id] = task.id
    else:
      parent_id = abs_ids[task.parent.id]
      abs_ids[task.id] = f'{parent_id}.{task.id}'
  wbs.visit_tasks(cache_abs_id)

  for task in wbs.tasks:
    _print_task(p, task, abs_ids, prj, est)

  # A hack to prevent TJ from scheduling unfinished tasks in the past
  p.write('''\
task now "Now" {
  milestone
  flags internal
  start ${now}
}

''')

  # Print reports

  p.write(f'''\
taskreport gantt "GanttChart" {{
  headline "{prj.name} - Gantt Chart"
  timeformat "%Y-%m-%d"
  formats html
  columns bsi {{ title 'ID' }}, name, JiraLink, start, end, effort, resources, chart {{ width 5000 }}
  loadunit weeks
  sorttasks tree
  hidetask (internal)
}}

resourcereport resources "ResourceGraph" {{
  headline "{prj.name} - Resource Allocation Report"
  timeformat "%Y-%m-%d"
  formats html
  columns bsi, name, JiraLink, start, end, effort, chart {{ width 5000 }}
#  loadunit weeks
  sorttasks tree
  hidetask (internal | ~isleaf_())
  hideresource ~isleaf()
}}

tracereport trace "TraceReport" {{
  columns bsi, name, start, end
  timeformat "%Y-%m-%d"
  formats csv
}}

export msproject "{prj.name}" {{
  formats mspxml
}}
''')

  if dump:
    print(p.out.getvalue())
  else:
    tjp_file = 'plan.tjp'
    tj_dir = './tj'

    with open(tjp_file, 'w') as f:
      f.write(p.out.getvalue())

    if not os.path.exists(tj_dir):
      os.mkdir(tj_dir)

    if subprocess.call(['tj3', '-o', tj_dir, tjp_file]) != 0:
      error("failed to run tj3; do you have TaskJuggler installed?")

    platform.open_file(os.path.join(tj_dir, 'GanttChart.html'))
    platform.open_file(os.path.join(tj_dir, 'ResourceGraph.html'))
Example #17
0
    def _schedule_goal(self, goal, start, alloc, par, warn_if_past=True):
        logger.debug(f"_schedule_goal: scheduling goal '{goal.name}': "
                     f"start={start}, alloc={alloc}, par={par}")

        if self.sched.is_completed(goal):
            return self.sched.get_completion_date(goal)

        if goal.completion_date is not None:
            logger.debug("_schedule_goal: goal already scheduled")
            if warn_if_past and goal.completion_date < start:
                warn(
                    goal.loc,
                    f"goal '{goal.name}' is completed on {goal.completion_date}, before {start}"
                )
            # TODO: warn if completion_date < start
            self.sched.set_completion_date(goal, goal.completion_date)
            return goal.completion_date

        if goal.is_completed():
            warn(
                goal.loc,
                f"unable to schedule completed goal '{goal.name}' with no completion date"
            )
            self.sched.set_completion_date(goal, datetime.date.today())
            return datetime.date.today()

        completion_date = start
        for act in goal.preds:
            logger.debug(
                f"_schedule_goal: scheduling activity '{act.name}' for goal '{goal.name}'"
            )
            if act.duration is not None:
                # TODO: register spent time for devs
                if warn_if_past and act.duration.start < start:
                    warn(
                        act.loc,
                        f"activity '{act.name}' started on {act.duration.start}, before {start}"
                    )
                completion_date = max(completion_date, act.duration.finish)
                continue

            act_start = start
            if act.head is not None:
                if self.sched.is_completed(act.head):
                    act_start = max(act_start,
                                    self.sched.get_completion_date(act.head))
                else:
                    # For goals that are not specified by schedule we use default settings
                    logger.debug(
                        f"_schedule_goal: scheduling predecessor '{act.head.name}'"
                    )
                    self._schedule_goal(act.head,
                                        datetime.date.today(), [],
                                        None,
                                        warn_if_past=False)
                    if not act.overlaps:
                        act_start = max(
                            act_start,
                            self.sched.get_completion_date(act.head))
                    else:
                        for pred in act.head.preds:
                            overlap = act.overlaps.get(pred.id)
                            if overlap is not None:
                                pred_iv = self.sched.get_duration(pred)
                                span = (pred_iv.finish -
                                        pred_iv.start) * (1 - overlap)
                                act_start = max(act_start,
                                                pred_iv.start + span)

            if act.is_instant():
                completion_date = max(completion_date, act_start)
                continue

            plan_rcs = self.prj.get_resources(act.alloc)
            if alloc:
                rcs = self.prj.get_resources(alloc)
                if any(rc for rc in rcs if rc not in plan_rcs):
                    allocs = '/'.join(alloc)
                    assignees = '/'.join(rc.name for rc in plan_rcs)
                    error(
                        f"allocations defined in schedule ({allocs}) do not match "
                        f"allocations defined in action ({assignees})")
            else:
                rcs = plan_rcs

            act_par = par
            if act_par is None:
                act_par = act.parallel

            act_effort, _ = self.est.estimate(act)
            act_effort *= 1 - act.effort.completion

            assignees = '/'.join(rc.name for rc in rcs)
            logger.debug(
                f"_schedule_goal: scheduling activity '{act.name}': "
                f"start={act_start}, effort={act_effort}, par={act_par}, rcs={assignees}"
            )

            iv, assigned_rcs = self.sched.assign_best_rcs(
                rcs, act_start, act_effort, act_par)
            assignees = '/'.join(rc.name for rc in assigned_rcs)
            logger.debug(
                f"_schedule_goal: assignment for activity '{act.name}': "
                f"@{assignees}, duration {iv}")

            self.sched.set_duration(act, iv, assigned_rcs)
            completion_date = max(completion_date, iv.finish)

        logger.debug(
            f"_schedule_goal: scheduled goal '{goal.name}' for completion_date"
        )
        self.sched.set_completion_date(goal, completion_date)

        if goal.deadline is not None and completion_date > goal.deadline:
            warn(
                f"failed to schedule goal '{goal.name}' before deadline goal.deadline"
            )

        return completion_date