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
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
,
20 parser
= argparse
.ArgumentParser(
21 description
="Check for errors in "
22 "Libre-SOC's style of budget tracking in Bugzilla.")
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>")
28 "-o", "--output-dir", type=Path
, default
=None,
29 help="The path to the output directory, will be created if it "
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",
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()
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
)
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
)
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
:
63 logging
.fatal("must use --subset-person with --subset option")
65 print_markdown_for_person(budget_graph
, config
,
66 args
.person
, args
.subset
)
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
)
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
)
79 logging
.fatal("--subset-person: unknown person: %s", person_str
)
83 nodes_subset
= OrderedSet()
84 for bug_id
in re
.split(r
"[\s,]+", subset_str
):
86 node
= budget_graph
.nodes
[int(bug_id
)]
87 except (ValueError, KeyError):
88 logging
.fatal("--subset: unknown bug: %s", bug_id
)
90 nodes_subset
.add(node
)
91 print(markdown_for_person(budget_graph
, person
, nodes_subset
))
94 def print_budget_then_children(indent
, nodes
, bug_id
):
95 """recursive indented printout of budgets
99 print("bug #%5d %s budget %6s excltasks %6s s %s p %s" %
100 (bug
.bug
.id, ' | ' * indent
,
101 str(bug
.fixed_budget_including_subtasks
),
102 str(bug
.fixed_budget_excluding_subtasks
),
103 str(bug
.submitted_including_subtasks
),
104 str(bug
.paid_including_subtasks
)))
107 for child
in bug
.immediate_children
:
108 if (str(child
.budget_including_subtasks
) == "0" and
109 str(child
.budget_excluding_subtasks
) == "0"):
111 print_budget_then_children(indent
+1, nodes
, child
.bug
.id)
114 def summarize_milestones(budget_graph
: BudgetGraph
):
115 for milestone
, payments
in budget_graph
.milestone_payments
.items():
116 summary
= PaymentSummary(payments
)
117 print(f
"{milestone.identifier}")
118 print(f
"\t{summary.total} submitted: "
119 f
"{summary.total_submitted} paid: {summary.total_paid}")
120 not_submitted
= summary
.get_not_submitted()
122 print("not submitted", not_submitted
)
124 # and one to display people
125 for person
in budget_graph
.milestone_people
[milestone
]:
126 print(f
"\t%-30s - %s" % (person
.identifier
, person
.full_name
))
130 for milestone
, payments
in budget_graph
.milestone_payments
.items():
131 print("%s %d" % (milestone
.identifier
, milestone
.canonical_bug_id
))
132 print_budget_then_children(0, budget_graph
.nodes
,
133 milestone
.canonical_bug_id
)
136 print ("```") # for using the output as markdown
139 def json_milestones(budget_graph
, add_comments
):
140 """reports milestones as json format
142 for milestone
, payments
in budget_graph
.milestone_payments
.items():
143 summary
= PaymentSummary(payments
)
144 # and one to display people
146 for person
in budget_graph
.milestone_people
[milestone
]:
147 p
= {'name': person
.full_name
, 'email': person
.email
}
151 canonical
= budget_graph
.nodes
[milestone
.canonical_bug_id
]
152 for child
in canonical
.immediate_children
:
154 # include the task itself as a milestone
155 for st
in list(child
.children()) + [child
]:
156 amount
= st
.fixed_budget_excluding_subtasks
.int()
157 if amount
== 0: # skip anything at zero
159 # if "task itself" then put the milestone as "wrapup"
160 if st
.bug
== child
.bug
:
161 description
= 'wrapup'
164 # otherwise create a description and get comment #0
165 description
= "%d %s" % (st
.bug
.id, st
.bug
.summary
)
166 # add parent and MoU top-level
167 parent_id
= st
.parent
.bug
.id
168 if parent_id
!= child
.bug
.id:
169 description
+= "\n(Sub-sub-task of %d)" % parent_id
170 task
= {'description': description
,
173 #mou_bug = st.closest_bug_in_mou
174 #if mou_bug is not None:
175 # task['mou_task'] = mou_bug.bug.id
176 milestones
.append(task
)
177 # create MoU task: get comment #0
179 comment
= "%s\n " % child
.bug_url
181 comments
= child
.bug
.getcomments()
182 comment
+= "\n%s" % comments
[0]['text']
183 intro
.append(comment
)
184 #print (description, intro)
186 task
= {'title': "%d %s" % (child
.bug
.id, child
.bug
.summary
),
188 'amount': child
.fixed_budget_including_subtasks
.int(),
189 'url': "{{ %s }} " % child
.bug_url
,
190 'milestones': milestones
194 d
= {'participants': ppl
,
197 'url': canonical
.bug_url
,
198 'plan': { 'intro': [''],
204 with
open("report.%s.json" % milestone
.identifier
, "w") as f
:
205 json
.dump(d
, f
, indent
=2)
209 if __name__
== "__main__":