998bdba189370989c7545a41290ee4508f108636
[utils.git] / src / budget_sync / main.py
1 from bugzilla import Bugzilla
2 import logging
3 import argparse
4 import csv
5 from pathlib import Path
6 from budget_sync.util import all_bugs
7 from budget_sync.config import Config, ConfigParseError
8 from budget_sync.budget_graph import BudgetGraph, BudgetGraphBaseError
9 from budget_sync.write_budget_markdown import write_budget_markdown
10 from collections import OrderedDict
11
12
13 def write_csv(name, items, headers):
14 """ Write an array of dictionaries to the CSV file name """
15 with open(name, 'w') as csvfile:
16 writer = csv.DictWriter(csvfile, headers, lineterminator="\n")
17 writer.writeheader()
18 writer.writerows(items)
19
20
21 mdwn_csv_template = """\
22 # %s
23
24 [[!table format=csv file="%s"]]
25 """
26
27 mdwn_people_template = """\
28 * [%s](%s)
29 """
30
31
32 def write_budget_csv(budget_graph: BudgetGraph,
33 output_dir: Path):
34 # quick hack to display total payment amounts per-milestone
35 for milestone, payments in budget_graph.milestone_payments.items():
36 print(milestone)
37 total = 0
38 total_requested = 0
39 total_req_or_paid = 0
40 total_paid = 0
41 for payment in payments:
42 # print("\t", payment)
43 total += payment.amount
44 if payment.submitted is not None:
45 total_requested += payment.amount
46 if payment.paid is not None:
47 total_paid += payment.amount
48 if payment.submitted or payment.paid is not None:
49 total_req_or_paid += payment.amount
50
51 print("\t %-9s" % total,
52 "submitted %-9s" % total_requested,
53 "paid %-9s" % total_paid,
54 "submitted or paid %-9s" % total_req_or_paid)
55 print()
56
57 # and one to display peole
58 milestones_people = budget_graph.get_milestone_people()
59 for milestone, people in milestones_people.items():
60 print(milestone)
61 for person in people:
62 print("\t", person)
63
64 # even quicker hack to create something vaguely resembling a CSV file
65 milestone_csvs = {}
66 milestone_headings = {}
67 all_people = OrderedDict()
68 for milestone, nodes in budget_graph.assigned_nodes_for_milestones.items():
69 milestone_csvs[milestone] = {} # rows in the CSV file
70 people = milestones_people[milestone]
71 headings = ['bug_id',
72 'budget_excluding_subtasks',
73 'budget_including_subtasks',
74 'fixed_budget_excluding_subtasks',
75 'fixed_budget_including_subtasks',
76 'submitted_excluding_subtasks',
77 'paid_excluding_subtasks']
78 for person in people:
79 name = str(person).replace(" ", "_")
80 all_people[person] = person
81 # name, amount, requested (submitted), paid
82 headings.append(name)
83 headings.append(name+"_req")
84 headings.append(name+"_paid")
85 milestone_headings[milestone] = headings
86 for node in nodes:
87 # skip uninteresting nodes
88 if len(node.payments) == 0 \
89 and node.budget_excluding_subtasks == 0 \
90 and node.budget_including_subtasks == 0:
91 continue
92 row = {'bug_id': node.bug.id,
93 'budget_excluding_subtasks': str(node.budget_excluding_subtasks),
94 'budget_including_subtasks': str(node.budget_including_subtasks),
95 'fixed_budget_excluding_subtasks': str(node.fixed_budget_excluding_subtasks),
96 'fixed_budget_including_subtasks': str(node.fixed_budget_including_subtasks),
97 'submitted_excluding_subtasks': str(node.submitted_excluding_subtasks),
98 'paid_excluding_subtasks': str(node.paid_excluding_subtasks)}
99 for payment in node.payments.values():
100 short_name = str(payment.payee.output_markdown_file)
101 name = short_name.replace(".mdwn", "")
102
103 row[name] = str(payment.amount)
104 if payment.submitted is not None:
105 requested = str(payment.submitted)
106 else:
107 requested = ""
108 if payment.paid is not None:
109 paid = str(payment.paid)
110 else:
111 paid = ""
112 row[name+"_req"] = requested
113 row[name+"_paid"] = paid
114
115 # print(row)
116 milestone_csvs[milestone][node.bug.id] = row
117
118 with open(output_dir.joinpath("csvs.mdwn"), "w") as f:
119 # write out the people pages
120 # TODO, has to be done by the markdown page name
121 # f.write("# People\n\n")
122 # for name, person in all_people.items():
123 # fname = output_dir.joinpath(f"{name}.csv")
124 # f.write(mdwn_people_template % (person, fname))
125 # and the CSV files
126 for milestone, rows in milestone_csvs.items():
127 ident = milestone.identifier
128 header = milestone_headings[milestone]
129 fname = output_dir.joinpath(f"{ident}.csv")
130 rows = rows.values() # turn into list
131 write_csv(fname, rows, header)
132 f.write(mdwn_csv_template % (ident, fname))
133
134
135 def main():
136 parser = argparse.ArgumentParser(
137 description="Check for errors in "
138 "Libre-SOC's style of budget tracking in Bugzilla.")
139 parser.add_argument(
140 "-c", "--config", type=argparse.FileType('r'),
141 required=True, help="The path to the configuration TOML file",
142 dest="config", metavar="<path/to/budget-sync-config.toml>")
143 parser.add_argument(
144 "-o", "--output-dir", type=Path, default=None,
145 help="The path to the output directory, will be created if it "
146 "doesn't exist",
147 dest="output_dir", metavar="<path/to/output/dir>")
148 args = parser.parse_args()
149 try:
150 with args.config as config_file:
151 config = Config.from_file(config_file)
152 except (IOError, ConfigParseError) as e:
153 logging.error("Failed to parse config file: %s", e)
154 return
155 logging.info("Using Bugzilla instance at %s", config.bugzilla_url)
156 bz = Bugzilla(config.bugzilla_url)
157 logging.debug("Connected to Bugzilla")
158 budget_graph = BudgetGraph(all_bugs(bz), config)
159 for error in budget_graph.get_errors():
160 logging.error("%s", error)
161 if args.output_dir is not None:
162 write_budget_markdown(budget_graph, args.output_dir)
163 write_budget_csv(budget_graph, args.output_dir)
164
165
166 if __name__ == "__main__":
167 main()