3f8c9963ae1b7add74dfbb7810950f830cd0cc5c
[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 parser.add_argument('--comments', action='store_true',
43 help="Put JSON into comments")
44 args = parser.parse_args()
45 try:
46 with args.config as config_file:
47 config = Config.from_file(config_file)
48 except (IOError, ConfigParseError) as e:
49 logging.error("Failed to parse config file: %s", e)
50 return
51 print("```") # for using the output as markdown
52 logging.info("Using Bugzilla instance at %s", config.bugzilla_url)
53 bz = Bugzilla(config.bugzilla_url)
54 if args.username:
55 logging.debug("logging in...")
56 bz.interactive_login(args.username, args.password)
57 logging.debug("Connected to Bugzilla")
58 budget_graph = BudgetGraph(all_bugs(bz), config)
59 for error in budget_graph.get_errors():
60 logging.error("%s", error)
61 if args.person or args.subset:
62 if not args.person:
63 logging.fatal("must use --subset-person with --subset option")
64 sys.exit(1)
65 print_markdown_for_person(budget_graph, config,
66 args.person, args.subset)
67 return
68 if args.output_dir is not None:
69 write_budget_markdown(budget_graph, args.output_dir)
70 write_budget_csv(budget_graph, args.output_dir)
71 summarize_milestones(budget_graph)
72 json_milestones(budget_graph, args.comments, args.output_dir)
73
74
75 def print_markdown_for_person(budget_graph: BudgetGraph, config: Config,
76 person_str: str, subset_str: Optional[str]):
77 person = config.all_names.get(person_str)
78 if person is None:
79 logging.fatal("--subset-person: unknown person: %s", person_str)
80 sys.exit(1)
81 nodes_subset = None
82 if subset_str:
83 nodes_subset = OrderedSet()
84 for bug_id in re.split(r"[\s,]+", subset_str):
85 try:
86 node = budget_graph.nodes[int(bug_id)]
87 except (ValueError, KeyError):
88 logging.fatal("--subset: unknown bug: %s", bug_id)
89 sys.exit(1)
90 nodes_subset.add(node)
91 print(markdown_for_person(budget_graph, person, nodes_subset))
92
93
94 def print_budget_then_children(indent, nodes, bug_id):
95 """recursive indented printout of budgets
96 """
97
98 bug = nodes[bug_id]
99 b_incl = str(bug.fixed_budget_including_subtasks)
100 b_excl = str(bug.fixed_budget_excluding_subtasks)
101 s_incl = str(bug.submitted_including_subtasks)
102 p_incl = str(bug.paid_including_subtasks)
103 if b_incl == s_incl and b_incl == p_incl:
104 descr = "(s+p)"
105 elif b_incl == s_incl:
106 descr = "(s) p %s" % p_incl
107 elif b_incl == p_incl:
108 descr = "(p) s %s" % s_incl
109 elif s_incl == p_incl:
110 descr = " s,p %s" % (p_incl)
111 else:
112 descr = "s %s p %s" % (s_incl, p_incl)
113 excl_desc = " "
114 if b_incl != b_excl:
115 excl_desc = "excltasks %6s" % b_excl
116 print("bug #%5d %s budget %6s %s %s" %
117 (bug.bug.id, ' | ' * indent,
118 b_incl,
119 excl_desc,
120 descr
121 ))
122 # print(repr(bug))
123
124 for child in bug.immediate_children:
125 if (str(child.budget_including_subtasks) == "0" and
126 str(child.budget_excluding_subtasks) == "0"):
127 continue
128 print_budget_then_children(indent+1, nodes, child.bug.id)
129
130
131 def summarize_milestones(budget_graph: BudgetGraph):
132 for milestone, payments in budget_graph.milestone_payments.items():
133 summary = PaymentSummary(payments)
134 print(f"{milestone.identifier}")
135 print(f"\t{summary.total} submitted: "
136 f"{summary.total_submitted} paid: {summary.total_paid}")
137 not_submitted = summary.get_not_submitted()
138 if not_submitted:
139 print("not submitted", not_submitted)
140
141 # and one to display people
142 for person in budget_graph.milestone_people[milestone]:
143 print(f"\t%-30s - %s" % (person.identifier, person.full_name))
144 print()
145
146 # now do trees
147 for milestone, payments in budget_graph.milestone_payments.items():
148 print("%s %d" % (milestone.identifier, milestone.canonical_bug_id))
149 print_budget_then_children(0, budget_graph.nodes,
150 milestone.canonical_bug_id)
151 print()
152
153 print("```") # for using the output as markdown
154
155
156 def json_milestones(budget_graph: BudgetGraph, add_comments: bool,
157 output_dir: Path):
158 """reports milestones as json format
159 """
160 for milestone, payments in budget_graph.milestone_payments.items():
161 summary = PaymentSummary(payments)
162 # and one to display people
163 ppl = []
164 for person in budget_graph.milestone_people[milestone]:
165 p = {'name': person.full_name, 'email': person.email}
166 ppl.append(p)
167
168 tasks = []
169 canonical = budget_graph.nodes[milestone.canonical_bug_id]
170 for child in canonical.immediate_children:
171 milestones = []
172 # include the task itself as a milestone
173 for st in list(child.children()) + [child]:
174 amount = st.fixed_budget_excluding_subtasks.int()
175 if amount == 0: # skip anything at zero
176 continue
177 # if "task itself" then put the milestone as "wrapup"
178 if st.bug == child.bug:
179 description = 'wrapup'
180 intro = []
181 else:
182 # otherwise create a description and get comment #0
183 description = "%d %s" % (st.bug.id, st.bug.summary)
184 # add parent and MoU top-level
185 parent_id = st.parent.bug.id
186 if parent_id != child.bug.id:
187 description += "\n(Sub-sub-task of %d)" % parent_id
188 task = {'description': description,
189 'amount': amount,
190 }
191 #mou_bug = st.closest_bug_in_mou
192 # if mou_bug is not None:
193 # task['mou_task'] = mou_bug.bug.id
194 milestones.append(task)
195 # create MoU task: get comment #0
196 intro = []
197 comment = "%s\n " % child.bug_url
198 if add_comments:
199 comments = child.bug.getcomments()
200 comment += "\n%s" % comments[0]['text']
201 intro.append(comment)
202 # print (description, intro)
203 # sys.stdout.flush()
204 task = {'title': "%d %s" % (child.bug.id, child.bug.summary),
205 'intro': intro,
206 'amount': child.fixed_budget_including_subtasks.int(),
207 'url': "{{ %s }} " % child.bug_url,
208 'milestones': milestones
209 }
210 tasks.append(task)
211
212 d = {'participants': ppl,
213 'preamble': '',
214 'type': 'Group',
215 'url': canonical.bug_url,
216 'plan': {'intro': [''],
217 'tasks': tasks,
218 'rfp_secret': '',
219 }
220 }
221
222 output_file = output_dir / f"report.{milestone.identifier}.json"
223 output_file.write_text(json.dumps(d, indent=2), encoding="utf-8")
224
225
226 if __name__ == "__main__":
227 main()