import collections
+
class OrderedSet(collections.MutableSet):
def __init__(self, iterable=None):
retval[key] = Payment._from_toml(self, key, value)
return retval
+ @cached_property
+ def submitted_excluding_subtasks(self) -> Money:
+ retval = Money()
+ for payment in self.payments.values():
+ if payment.submitted is not None or payment.paid is not None:
+ retval += payment.amount
+ return retval
+
+ @cached_property
+ def paid_excluding_subtasks(self) -> Money:
+ retval = Money()
+ for payment in self.payments.values():
+ if payment.paid is not None:
+ retval += payment.amount
+ return retval
+
@property
def parent(self) -> Optional["Node"]:
if self.parent_id is not None:
class BudgetGraph:
nodes: Dict[int, Node]
+ milestone_payments: Dict[Milestone, List[Payment]]
def __init__(self, bugs: Iterable[Bug], config: Config):
self.nodes = OrderedDict()
node.parent.immediate_children.add(node)
self.milestone_payments = OrderedDict()
# useful debug prints
- #for bug in bugs:
+ # for bug in bugs:
# node = self.nodes[bug.id]
# print ("bug added", bug.id, node, node.parent.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,
+ # print ("subtask total", node.bug.id, root.bug.id, subtasks_total,
# childlist)
payees_total = Money(0)
previous_payment = payee_payments.get(payment.payee)
if previous_payment is not None:
# NOT AN ERROR
- print ("NOT AN ERROR", BudgetGraphDuplicatePayeesForTask(
- node.bug.id, root.bug.id,
- previous_payment[-1].payee_key, payment.payee_key))
+ 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]
retval[node.assignee].append(node)
return retval
+ @cached_property
+ def assigned_nodes_for_milestones(self) -> Dict[Milestone, List[Node]]:
+ retval = {milestone: []
+ for milestone in self.config.milestones.values()}
+ for node in self.nodes.values():
+ if node.milestone is not None:
+ retval[node.milestone].append(node)
+ return retval
+
@cached_property
def payments(self) -> Dict[Person, Dict[Milestone, List[Payment]]]:
retval = OrderedDict()
milestone_payments = OrderedDict()
for milestone in self.config.milestones.values():
milestone_payments[milestone] = [] # per-person payments
- self.milestone_payments[milestone] = [] # global payments
+ self.milestone_payments[milestone] = [] # global payments
retval[person] = milestone_payments
for node in self.nodes.values():
if node.milestone is not None:
self.milestone_payments[node.milestone].append(payment)
return retval
- def get_milestone_people(self):
+ def get_milestone_people(self) -> Dict[Milestone, OrderedSet]:
"""get a list of people associated with each milestone
"""
- payments = list(self.payments) # just activate the payments
+ payments = list(self.payments) # just activate the payments
retval = OrderedDict()
for milestone in self.milestone_payments.keys():
retval[milestone] = OrderedSet()
"""
-def main():
- parser = argparse.ArgumentParser(
- description="Check for errors in "
- "Libre-SOC's style of budget tracking in Bugzilla.")
- parser.add_argument(
- "-c", "--config", type=argparse.FileType('r'),
- required=True, help="The path to the configuration TOML file",
- dest="config", metavar="<path/to/budget-sync-config.toml>")
- parser.add_argument(
- "-o", "--output-dir", type=Path, default=None,
- help="The path to the output directory, will be created if it "
- "doesn't exist",
- dest="output_dir", metavar="<path/to/output/dir>")
- args = parser.parse_args()
- try:
- with args.config as config_file:
- config = Config.from_file(config_file)
- except (IOError, ConfigParseError) as e:
- logging.error("Failed to parse config file: %s", e)
- return
- logging.info("Using Bugzilla instance at %s", config.bugzilla_url)
- bz = Bugzilla(config.bugzilla_url)
- logging.debug("Connected to Bugzilla")
- budget_graph = BudgetGraph(all_bugs(bz), config)
- for error in budget_graph.get_errors():
- logging.error("%s", error)
- if args.output_dir is not None:
- write_budget_markdown(budget_graph, args.output_dir)
-
+def write_budget_csv(budget_graph: BudgetGraph,
+ output_dir: Path):
# quick hack to display total payment amounts per-milestone
for milestone, payments in budget_graph.milestone_payments.items():
print(milestone)
milestone_csvs = {}
milestone_headings = {}
all_people = OrderedDict()
- # pre-initialise the CSV lists (to avoid overwrite)
- for milestone, payments in budget_graph.milestone_payments.items():
+ for milestone, nodes in budget_graph.assigned_nodes_for_milestones.items():
milestone_csvs[milestone] = {} # rows in the CSV file
-
- for milestone, payments in budget_graph.milestone_payments.items():
- # first get the list of people, then create some columns
people = milestones_people[milestone]
- headings = ['bug_id']
+ headings = ['bug_id',
+ 'budget_excluding_subtasks',
+ 'budget_including_subtasks',
+ 'fixed_budget_excluding_subtasks',
+ 'fixed_budget_including_subtasks',
+ 'submitted_excluding_subtasks',
+ 'paid_excluding_subtasks']
for person in people:
name = str(person).replace(" ", "_")
all_people[person] = person
headings.append(name+"_req")
headings.append(name+"_paid")
milestone_headings[milestone] = headings
- # now we go through the whole "payments" thing again...
- for payment in payments:
- row = milestone_csvs[milestone].get(payment.node.bug.id, None)
- if row is None:
- row = {'bug_id': payment.node.bug.id}
-
- short_name = str(payment.payee.output_markdown_file)
- name = short_name.replace(".mdwn", "")
-
- row[name] = str(payment.amount)
- if payment.submitted is not None:
- requested = str(payment.submitted)
- else:
- requested = ""
- if payment.paid is not None:
- paid = str(payment.paid)
- else:
- paid = ""
- row[name+"_req"] = requested
- row[name+"_paid"] = paid
+ for node in nodes:
+ # skip uninteresting nodes
+ if len(node.payments) == 0 \
+ and node.budget_excluding_subtasks == 0 \
+ and node.budget_including_subtasks == 0:
+ continue
+ row = {'bug_id': node.bug.id,
+ 'budget_excluding_subtasks': str(node.budget_excluding_subtasks),
+ 'budget_including_subtasks': str(node.budget_including_subtasks),
+ 'fixed_budget_excluding_subtasks': str(node.fixed_budget_excluding_subtasks),
+ 'fixed_budget_including_subtasks': str(node.fixed_budget_including_subtasks),
+ 'submitted_excluding_subtasks': str(node.submitted_excluding_subtasks),
+ 'paid_excluding_subtasks': str(node.paid_excluding_subtasks)}
+ for payment in node.payments.values():
+ short_name = str(payment.payee.output_markdown_file)
+ name = short_name.replace(".mdwn", "")
+
+ row[name] = str(payment.amount)
+ if payment.submitted is not None:
+ requested = str(payment.submitted)
+ else:
+ requested = ""
+ if payment.paid is not None:
+ paid = str(payment.paid)
+ else:
+ paid = ""
+ row[name+"_req"] = requested
+ row[name+"_paid"] = paid
print(row)
- milestone_csvs[milestone][payment.node.bug.id] = row
+ milestone_csvs[milestone][node.bug.id] = row
+
+ with open(output_dir.joinpath("csvs.mdwn"), "w") as f:
+ # write out the people pages
+ # TODO, has to be done by the markdown page name
+ # f.write("# People\n\n")
+ # for name, person in all_people.items():
+ # fname = output_dir.joinpath(f"{name}.csv")
+ # f.write(mdwn_people_template % (person, fname))
+ # and the CSV files
+ for milestone, rows in milestone_csvs.items():
+ ident = milestone.identifier
+ header = milestone_headings[milestone]
+ fname = output_dir.joinpath(f"{ident}.csv")
+ rows = rows.values() # turn into list
+ write_csv(fname, rows, header)
+ f.write(mdwn_csv_template % (ident, fname))
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Check for errors in "
+ "Libre-SOC's style of budget tracking in Bugzilla.")
+ parser.add_argument(
+ "-c", "--config", type=argparse.FileType('r'),
+ required=True, help="The path to the configuration TOML file",
+ dest="config", metavar="<path/to/budget-sync-config.toml>")
+ parser.add_argument(
+ "-o", "--output-dir", type=Path, default=None,
+ help="The path to the output directory, will be created if it "
+ "doesn't exist",
+ dest="output_dir", metavar="<path/to/output/dir>")
+ args = parser.parse_args()
+ try:
+ with args.config as config_file:
+ config = Config.from_file(config_file)
+ except (IOError, ConfigParseError) as e:
+ logging.error("Failed to parse config file: %s", e)
+ return
+ logging.info("Using Bugzilla instance at %s", config.bugzilla_url)
+ bz = Bugzilla(config.bugzilla_url)
+ logging.debug("Connected to Bugzilla")
+ budget_graph = BudgetGraph(all_bugs(bz), config)
+ for error in budget_graph.get_errors():
+ logging.error("%s", error)
if args.output_dir is not None:
- with open("%s/csvs.mdwn" % args.output_dir, "w") as f:
- # write out the people pages
- # TODO, has to be done by the markdown page name
- # f.write("# People\n\n")
- # for name, person in all_people.items():
- # fname = "%s/%s" % (args.output_dir, name)
- # f.write(mdwn_people_template % (person, fname))
- # and the CSV files
- for milestone, rows in milestone_csvs.items():
- ident = milestone.identifier
- header = milestone_headings[milestone]
- fname = "%s/%s.csv" % (args.output_dir, ident)
- rows = rows.values() # turn into list
- write_csv(fname, rows, header)
- f.write(mdwn_csv_template % (ident, fname))
+ write_budget_markdown(budget_graph, args.output_dir)
+ write_budget_csv(budget_graph, args.output_dir)
if __name__ == "__main__":