12ce493cfac90a0b9eebba9ca4d0d6d5e58e9592
[utils.git] / src / budget_sync / main.py
1 import os
2 import re
3 import sys
4 import json
5 from typing import Optional
6 from budget_sync.ordered_set import OrderedSet
7 from budget_sync.write_budget_csv import write_budget_csv
8 from bugzilla import Bugzilla
9 import logging
10 import argparse
11 from pathlib import Path
12 from budget_sync.util import all_bugs
13 from budget_sync.config import Config, ConfigParseError
14 from budget_sync.budget_graph import BudgetGraph, PaymentSummary
15 from budget_sync.write_budget_markdown import (write_budget_markdown,
16 markdown_for_person)
17
18
19 def main():
20 parser = argparse.ArgumentParser(
21 description="Check for errors in "
22 "Libre-SOC's style of budget tracking in Bugzilla.")
23 parser.add_argument(
24 "-c", "--config", type=argparse.FileType('r'),
25 required=True, help="The path to the configuration TOML file",
26 dest="config", metavar="<path/to/budget-sync-config.toml>")
27 parser.add_argument(
28 "-o", "--output-dir", type=Path, default=None,
29 help="The path to the output directory, will be created if it "
30 "doesn't exist",
31 dest="output_dir", metavar="<path/to/output/dir>")
32 parser.add_argument('--subset',
33 help="write the output for this subset of bugs",
34 metavar="<bug-id>,<bug-id>,...")
35 parser.add_argument('--subset-person',
36 help="write the output for this person",
37 dest="person")
38 parser.add_argument('--username',
39 help="Log in with this Bugzilla username")
40 parser.add_argument('--password',
41 help="Log in with this Bugzilla password")
42 args = parser.parse_args()
43 try:
44 with args.config as config_file:
45 config = Config.from_file(config_file)
46 except (IOError, ConfigParseError) as e:
47 logging.error("Failed to parse config file: %s", e)
48 return
49 print ("```") # for using the output as markdown
50 logging.info("Using Bugzilla instance at %s", config.bugzilla_url)
51 bz = Bugzilla(config.bugzilla_url)
52 if args.username:
53 logging.debug("logging in...")
54 bz.interactive_login(args.username, args.password)
55 logging.debug("Connected to Bugzilla")
56 budget_graph = BudgetGraph(all_bugs(bz), config)
57 for error in budget_graph.get_errors():
58 logging.error("%s", error)
59 if args.person or args.subset:
60 if not args.person:
61 logging.fatal("must use --subset-person with --subset option")
62 sys.exit(1)
63 print_markdown_for_person(budget_graph, config,
64 args.person, args.subset)
65 return
66 if args.output_dir is not None:
67 write_budget_markdown(budget_graph, args.output_dir)
68 write_budget_csv(budget_graph, args.output_dir)
69 summarize_milestones(budget_graph)
70 json_milestones(budget_graph)
71
72
73 def print_markdown_for_person(budget_graph: BudgetGraph, config: Config,
74 person_str: str, subset_str: Optional[str]):
75 person = config.all_names.get(person_str)
76 if person is None:
77 logging.fatal("--subset-person: unknown person: %s", person_str)
78 sys.exit(1)
79 nodes_subset = None
80 if subset_str:
81 nodes_subset = OrderedSet()
82 for bug_id in re.split(r"[\s,]+", subset_str):
83 try:
84 node = budget_graph.nodes[int(bug_id)]
85 except (ValueError, KeyError):
86 logging.fatal("--subset: unknown bug: %s", bug_id)
87 sys.exit(1)
88 nodes_subset.add(node)
89 print(markdown_for_person(budget_graph, person, nodes_subset))
90
91
92 def print_budget_then_children(indent, nodes, bug_id):
93 """recursive indented printout of budgets
94 """
95
96 bug = nodes[bug_id]
97 print("bug #%5d %s budget %6s excltasks %6s s %s p %s" %
98 (bug.bug.id, ' | ' * indent,
99 str(bug.fixed_budget_including_subtasks),
100 str(bug.fixed_budget_excluding_subtasks),
101 str(bug.submitted_including_subtasks),
102 str(bug.paid_including_subtasks)))
103 # print(repr(bug))
104
105 for child in bug.immediate_children:
106 if (str(child.budget_including_subtasks) == "0" and
107 str(child.budget_excluding_subtasks) == "0"):
108 continue
109 print_budget_then_children(indent+1, nodes, child.bug.id)
110
111
112 def summarize_milestones(budget_graph: BudgetGraph):
113 for milestone, payments in budget_graph.milestone_payments.items():
114 summary = PaymentSummary(payments)
115 print(f"{milestone.identifier}")
116 print(f"\t{summary.total} submitted: "
117 f"{summary.total_submitted} paid: {summary.total_paid}")
118 not_submitted = summary.get_not_submitted()
119 if not_submitted:
120 print("not submitted", not_submitted)
121
122 # and one to display people
123 for person in budget_graph.milestone_people[milestone]:
124 print(f"\t%-30s - %s" % (person.identifier, person.full_name))
125 print()
126
127 # now do trees
128 for milestone, payments in budget_graph.milestone_payments.items():
129 print("%s %d" % (milestone.identifier, milestone.canonical_bug_id))
130 print_budget_then_children(0, budget_graph.nodes,
131 milestone.canonical_bug_id)
132 print()
133
134 print ("```") # for using the output as markdown
135
136
137 def json_milestones(budget_graph):
138 """reports milestones as json format
139 """
140 for milestone, payments in budget_graph.milestone_payments.items():
141 summary = PaymentSummary(payments)
142 # and one to display people
143 ppl = []
144 for person in budget_graph.milestone_people[milestone]:
145 p = {'name': person.full_name, 'email': person.email}
146 ppl.append(p)
147
148 tasks = []
149 canonical = budget_graph.nodes[milestone.canonical_bug_id]
150 for child in canonical.immediate_children:
151 milestones = []
152 # include the task itself as a milestone
153 for st in [child] + list(child.children()):
154 amount = st.fixed_budget_including_subtasks.int()
155 if amount == 0: # skip anything at zero
156 continue
157 task = {'description': "%d %s" % (st.bug.id, st.bug.summary),
158 'intro': [],
159 'amount': amount,
160 'url': st.bug_url,
161 }
162 # add parent and MoU top-level
163 parent_id = st.parent.bug.id,
164 if parent_id != milestone.canonical_bug_id:
165 task['parent'] = parent_id
166 mou_bug = st.closest_bug_in_mou
167 if mou_bug is not None:
168 task['mou_task'] = mou_bug.bug.id
169 milestones.append(task)
170 # create MoU task
171 task = {'title': "%d %s" % (child.bug.id, child.bug.summary),
172 'intro': [],
173 'amount': child.fixed_budget_including_subtasks.int(),
174 'url': child.bug_url,
175 'milestones': milestones
176 }
177 tasks.append(task)
178
179 d = {'participants': ppl,
180 'preamble': '',
181 'type': 'Group',
182 'plan': { 'intro': [''],
183 'tasks': tasks,
184 'rfp_secret': '',
185 }
186 }
187
188 with open("report.%s.json" % milestone.identifier, "w") as f:
189 json.dump(d, f, indent=2)
190
191
192
193 if __name__ == "__main__":
194 main()