a quick hack to display total payments per milestone
[utils.git] / src / budget_sync / budget_graph.py
index 463c3e1b397b8599fba4e96fd6e6dae380868972..5a9542b3edeb1d3b7026fa40075643a7609d9040 100644 (file)
@@ -1,7 +1,6 @@
 from bugzilla.bug import Bug
-from bugzilla import Bugzilla
 from typing import Set, Dict, Iterable, Optional, List, Union, Any
-from budget_sync.util import all_bugs
+from budget_sync.util import BugStatus
 from budget_sync.money import Money
 from budget_sync.config import Config, Person, Milestone
 from functools import cached_property
@@ -10,7 +9,7 @@ import sys
 import enum
 from collections import deque
 from datetime import date, time, datetime
-
+from collections import OrderedDict
 
 class BudgetGraphBaseError(Exception):
     pass
@@ -31,6 +30,16 @@ class BudgetGraphPayeesParseError(BudgetGraphParseError):
             f"bug #{self.bug_id}: {self.msg}"
 
 
+class BudgetGraphUnknownAssignee(BudgetGraphParseError):
+    def __init__(self, bug_id: int, assignee: str):
+        super().__init__(bug_id)
+        self.assignee = assignee
+
+    def __str__(self):
+        return f"Bug #{self.bug_id} is assigned to an unknown person: " \
+            f"{self.assignee!r}"
+
+
 class BudgetGraphLoopError(BudgetGraphBaseError):
     def __init__(self, bug_ids: List[int]):
         self.bug_ids = bug_ids
@@ -212,6 +221,16 @@ class BudgetGraphUnknownMilestone(BudgetGraphParseError):
             f"#{self.bug_id}: unknown milestone: {self.milestone_str!r}"
 
 
+class BudgetGraphUnknownStatus(BudgetGraphParseError):
+    def __init__(self, bug_id: int, status_str: str):
+        super().__init__(bug_id)
+        self.status_str = status_str
+
+    def __str__(self):
+        return f"failed to parse status field of bug " \
+            f"#{self.bug_id}: unknown status: {self.status_str!r}"
+
+
 class Node:
     graph: "BudgetGraph"
     bug: Bug
@@ -236,6 +255,23 @@ class Node:
         if self.milestone_str == "---":
             self.milestone_str = None
 
+    @property
+    def status(self) -> BugStatus:
+        try:
+            return BugStatus.cast(self.bug.status)
+        except ValueError:
+            new_err = BudgetGraphUnknownStatus(self.bug.id, self.bug.status)
+            raise new_err.with_traceback(sys.exc_info()[2])
+
+    @cached_property
+    def assignee(self) -> Person:
+        try:
+            return self.graph.config.all_names[self.bug.assigned_to]
+        except KeyError:
+            raise BudgetGraphUnknownAssignee(self.bug.id,
+                                             self.bug.assigned_to) \
+                .with_traceback(sys.exc_info()[2])
+
     @cached_property
     def bug_url(self) -> str:
         return f"{self.graph.config.bugzilla_url_stripped}/show_bug.cgi?" \
@@ -260,7 +296,7 @@ class Node:
             new_err = BudgetGraphPayeesParseError(
                 self.bug.id, f"TOML parse error: {e}")
             raise new_err.with_traceback(sys.exc_info()[2])
-        retval = {}
+        retval = OrderedDict()
         for key, value in parsed.items():
             if not isinstance(key, str):
                 raise BudgetGraphPayeesParseError(
@@ -330,6 +366,14 @@ class Node:
             milestone = repr(self.milestone)
         except BudgetGraphBaseError:
             milestone = "<unknown milestone>"
+        try:
+            status = repr(self.status)
+        except BudgetGraphBaseError:
+            status = f"<unknown status: {self.bug.status!r}>"
+        try:
+            assignee = f"Person<{self.assignee.identifier!r}>"
+        except BudgetGraphBaseError:
+            assignee = f"<unknown assignee: {self.bug.assigned_to!r}>"
         immediate_children = []
         for i in self.immediate_children:
             immediate_children.append(_NodeSimpleReprWrapper(i))
@@ -347,7 +391,9 @@ class Node:
                 f"milestone_str={self.milestone_str!r}, "
                 f"milestone={milestone}, "
                 f"immediate_children={immediate_children!r}, "
-                f"payments={payments!r}")
+                f"payments={payments!r}, "
+                f"status={status}, "
+                f"assignee={assignee})")
 
 
 class BudgetGraphError(BudgetGraphBaseError):
@@ -457,7 +503,7 @@ class BudgetGraph:
     nodes: Dict[int, Node]
 
     def __init__(self, bugs: Iterable[Bug], config: Config):
-        self.nodes = {}
+        self.nodes = OrderedDict()
         self.config = config
         for bug in bugs:
             self.nodes[bug.id] = Node(self, bug)
@@ -465,6 +511,11 @@ class BudgetGraph:
             if node.parent is None:
                 continue
             node.parent.immediate_children.add(node)
+        self.milestone_payments = OrderedDict()
+        # useful debug prints
+        #for bug in bugs:
+        #    node = self.nodes[bug.id]
+        #    print ("bug added", bug.id, node, node.parent.immediate_children)
 
     @cached_property
     def roots(self) -> Set[Node]:
@@ -497,6 +548,18 @@ class BudgetGraph:
         except BudgetGraphBaseError as e:
             errors.append(e)
 
+        try:
+            # check for status errors
+            node.status
+        except BudgetGraphBaseError as e:
+            errors.append(e)
+
+        try:
+            # check for assignee errors
+            node.assignee
+        except BudgetGraphBaseError as e:
+            errors.append(e)
+
         if node.milestone_str != root.milestone_str:
             errors.append(BudgetGraphMilestoneMismatch(
                 node.bug.id, root.bug.id))
@@ -506,12 +569,17 @@ class BudgetGraph:
             errors.append(BudgetGraphNegativeMoney(
                 node.bug.id, root.bug.id))
 
+        childlist = []
         subtasks_total = Money(0)
         for child in node.immediate_children:
             subtasks_total += child.fixed_budget_including_subtasks
+            childlist.append(child.bug.id)
+        # useful debug prints
+        #print ("subtask total", node.bug.id, root.bug.id, subtasks_total,
+        #                        childlist)
 
         payees_total = Money(0)
-        payee_payments = {}
+        payee_payments = OrderedDict()
         for payment in node.payments.values():
             if payment.amount < 0:
                 errors.append(BudgetGraphNegativePayeeMoney(
@@ -522,11 +590,13 @@ class BudgetGraph:
                 payment.payee
                 previous_payment = payee_payments.get(payment.payee)
                 if previous_payment is not None:
-                    errors.append(BudgetGraphDuplicatePayeesForTask(
-                        node.bug.id, root.bug.id,
-                        previous_payment.payee_key, payment.payee_key
-                    ))
-                payee_payments[payment.payee] = payment
+                    # NOT AN ERROR
+                    print ("NOT AN ERROR", BudgetGraphDuplicatePayeesForTask(
+                           node.bug.id, root.bug.id,
+                           previous_payment[-1].payee_key, payment.payee_key))
+                    payee_payments[payment.payee].append(payment)
+                else:
+                    payee_payments[payment.payee] = [payment]
             except BudgetGraphBaseError as e:
                 errors.append(e)
 
@@ -659,16 +729,36 @@ class BudgetGraph:
                 errors.append(e)
         return errors
 
+    @cached_property
+    def assigned_nodes(self) -> Dict[Person, List[Node]]:
+        retval = {person: [] for person in self.config.people.values()}
+        for node in self.nodes.values():
+            retval[node.assignee].append(node)
+        return retval
+
     @cached_property
     def payments(self) -> Dict[Person, Dict[Milestone, List[Payment]]]:
-        retval = {}
+        retval = OrderedDict()
         for person in self.config.people.values():
-            milestone_payments = {}
+            milestone_payments = OrderedDict()
             for milestone in self.config.milestones.values():
-                milestone_payments[milestone] = []
+                milestone_payments[milestone] = []      # per-person payments
+                self.milestone_payments[milestone] = [] # global payments
             retval[person] = milestone_payments
         for node in self.nodes.values():
             if node.milestone is not None:
                 for payment in node.payments.values():
                     retval[payment.payee][node.milestone].append(payment)
+                    # add to global payments as well
+                    self.milestone_payments[node.milestone].append(payment)
         return retval
+
+    def __repr__(self):
+        nodes = [*self.nodes.values()]
+        try:
+            roots = [_NodeSimpleReprWrapper(i) for i in self.roots]
+            roots.sort()
+            roots_str = repr(roots)
+        except BudgetGraphBaseError:
+            roots_str = "<failed>"
+        return f"BudgetGraph{{nodes={nodes!r}, roots={roots}}}"