__pycache__
*.egg-info
/.vscode
-*.pyc
\ No newline at end of file
+*.pyc
+/output
\ No newline at end of file
[people."Jacob R. Lifshay"]
email = "programmerjake@gmail.com"
aliases = ["programmerjake", "jacob", "Jacob", "Jacob Lifshay"]
+output_markdown_file = "programmerjake.mdwn"
[people."Luke Kenneth Casson Leighton"]
email = "lkcl@lkcl.net"
aliases = ["lkcl", "luke", "Luke", "Luke Leighton"]
+output_markdown_file = "lkcl.mdwn"
[people."Samuel A. Falvo II"]
email = "kc5tja@arrl.net"
aliases = ["kc5tja", "samuel", "Samuel", "Samuel Falvo II", "sam.falvo"]
+output_markdown_file = "samuel_falvo_ii.mdwn"
[people."Vivek Pandya"]
email = "vivekvpandya@gmail.com"
aliases = ["vivekvpandya", "vivek pandya", "vivek", "Vivek"]
+output_markdown_file = "vivek_pandya.mdwn"
[people."Florent Kermarrec"]
email = "florent@enjoy-digital.fr"
aliases = ["florent", "Florent"]
+output_markdown_file = "florent_kermarrec.mdwn"
[milestones]
"NLnet.2019.02" = { canonical_bug_id = 191 }
if self.milestone_str == "---":
self.milestone_str = None
+ @cached_property
+ def bug_url(self) -> str:
+ return f"{self.graph.config.bugzilla_url_stripped}/show_bug.cgi?" \
+ f"id={self.bug.id}"
+
@cached_property
def milestone(self) -> Optional[Milestone]:
try:
email: Optional[str]
def __init__(self, config: "Config", identifier: str,
+ output_markdown_file: str,
aliases: Optional[Set[str]] = None,
email: Optional[str] = None):
self.config = config
self.identifier = identifier
+ self.output_markdown_file = output_markdown_file
if aliases is None:
aliases = set()
self.aliases = aliases
return hash(self.identifier)
def __repr__(self):
- return f"Person(config=..., identifier={self.identifier!r}, " \
- f"aliases={self.aliases!r}, email={self.email!r})"
+ return (f"Person(config=..., identifier={self.identifier!r}, "
+ f"output_markdown_file={self.output_markdown_file!r}, "
+ f"aliases={self.aliases!r}, email={self.email!r})")
class Milestone:
f"people={self.people!r}, " \
f"milestones={self.milestones!r})"
+ @cached_property
+ def bugzilla_url_stripped(self):
+ return self.bugzilla_url.rstrip('/')
+
@cached_property
def all_names(self) -> Dict[str, Person]:
# also checks for any name clashes and raises
f"person entry for {identifier!r} must be a table")
aliases = set()
email = None
+ output_markdown_file = None
for k, v in value.items():
assert isinstance(k, str)
if k == "aliases":
f"`email` field in person entry for {identifier!r} "
f"must be a string")
email = v
+ elif k == "output_markdown_file":
+ if not isinstance(v, str):
+ raise ConfigParseError(
+ f"`output_markdown_file` field in person entry for "
+ f"{identifier!r} must be a string")
+ output_markdown_file = v
else:
raise ConfigParseError(
f"unknown field in person entry for {identifier!r}: `{k}`")
+ if output_markdown_file is None:
+ raise ConfigParseError(f"`output_markdown_file` field is missing in "
+ f"person entry for {identifier!r}")
return Person(config=self, identifier=identifier,
+ output_markdown_file=output_markdown_file,
aliases=aliases, email=email)
def _parse_people(self, people: Any):
from bugzilla import Bugzilla
import logging
import argparse
+from pathlib import Path
from budget_sync.util import all_bugs
from budget_sync.config import Config, ConfigParseError
from budget_sync.budget_graph import BudgetGraph, BudgetGraphBaseError
-
-
-BUGZILLA_URL = "https://bugs.libre-soc.org"
+from budget_sync.write_budget_markdown import write_budget_markdown
def main():
"-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:
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)
if __name__ == "__main__":
EXAMPLE_CONFIG = Config.from_str(
"""
- bugzilla_url = ""
+ bugzilla_url = "https://bugzilla.example.com/"
[people."person1"]
aliases = ["person1_alias1", "alias1"]
+ output_markdown_file = "person1.mdwn"
[people."person2"]
aliases = ["person1_alias2", "alias2", "person 2"]
+ output_markdown_file = "person2.mdwn"
[people."person3"]
+ output_markdown_file = "person3.mdwn"
[milestones]
"milestone 1" = { canonical_bug_id = 1 }
"milestone 2" = { canonical_bug_id = 2 }
self.assertIs(node.root, node)
self.assertIsNone(node.parent_id)
self.assertEqual(node.immediate_children, set())
+ self.assertEqual(node.bug_url,
+ "https://bugzilla.example.com/show_bug.cgi?id=1")
self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
self.assertEqual(node.budget_including_subtasks, Money(cents=0))
self.assertIsNone(node.milestone)
self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
self.assertEqual(node1.milestone_str, "milestone 1")
+ self.assertEqual(node1.bug_url,
+ "https://bugzilla.example.com/show_bug.cgi?id=1")
self.assertEqual(list(node1.children()), [node2])
self.assertEqual(list(node1.children_breadth_first()), [node2])
self.assertEqual(node1.payments, {})
self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
self.assertEqual(node2.milestone_str, "milestone 1")
self.assertEqual(node2.payments, {})
+ self.assertEqual(node2.bug_url,
+ "https://bugzilla.example.com/show_bug.cgi?id=2")
def test_children(self):
bg = BudgetGraph([
[people]
""",
"`milestones` table is missing")
+ check_error(
+ """
+ bugzilla_url = ""
+ [people."person1"]
+ """,
+ "`output_markdown_file` field is missing in person entry for "
+ "'person1'")
+ check_error(
+ """
+ bugzilla_url = ""
+ [people."person1"]
+ output_markdown_file = 1
+ """,
+ "`output_markdown_file` field in person entry for 'person1' must "
+ "be a string")
check(
"""
bugzilla_url = ""
[milestones]
[people."person1"]
aliases = ["a"]
+ output_markdown_file = "person1.mdwn"
[people."person2"]
aliases = ["b"]
+ output_markdown_file = "person2.mdwn"
""",
"Config(bugzilla_url='', people={"
"'person1': Person(config=..., identifier='person1', "
+ "output_markdown_file='person1.mdwn', "
"aliases={'a'}, email=None), "
"'person2': Person(config=..., identifier='person2', "
+ "output_markdown_file='person2.mdwn', "
"aliases={'b'}, email=None)}, milestones={})")
check_error(
"""
bugzilla_url = ""
[people."person1"]
email = 123
+ output_markdown_file = "person1.mdwn"
""",
"`email` field in person entry for 'person1' must be a string")
check(
[milestones]
[people."person1"]
email = "email@example.com"
+ output_markdown_file = "person1.mdwn"
""",
"Config(bugzilla_url='', people={"
"'person1': Person(config=..., identifier='person1', "
+ "output_markdown_file='person1.mdwn', "
"aliases=set(), email='email@example.com')}, milestones={})")
check_error(
"""
bugzilla_url = ""
[people."person1"]
blah = 123
+ output_markdown_file = "person1.mdwn"
""",
"unknown field in person entry for 'person1': `blah`")
check_error(
bugzilla_url = ""
[milestones]
[people."person1"]
+ output_markdown_file = "person1.mdwn"
[people."person2"]
aliases = ["person1"]
+ output_markdown_file = "person2.mdwn"
""",
"alias is not allowed to be the same as any person's identifier: "
"in person entry for 'person2': 'person1' is also the identifier "
bugzilla_url = ""
[milestones]
[people."person1"]
+ output_markdown_file = "person1.mdwn"
aliases = ["a"]
[people."person2"]
aliases = ["a"]
+ output_markdown_file = "person2.mdwn"
""",
"alias is not allowed to be the same as another person's alias: "
"in person entry for 'person2': 'a' is also an alias for person "
[milestones]
[people."person1"]
aliases = ["person1_alias1", "alias1"]
+ output_markdown_file = "person1.mdwn"
[people."person2"]
aliases = ["person2_alias2", "alias2"]
+ output_markdown_file = "person2.mdwn"
""")
person1 = config.people['person1']
person2 = config.people['person2']
2: milestone2,
})
+ def test_bugzilla_url_stripped(self):
+ c = Config.from_str(
+ """
+ bugzilla_url = "https://bugzilla.example.com/prefix"
+ [people]
+ [milestones]
+ """
+ )
+ self.assertEqual(c.bugzilla_url_stripped,
+ "https://bugzilla.example.com/prefix")
+ c = Config.from_str(
+ """
+ bugzilla_url = "https://bugzilla.example.com/prefix/"
+ [people]
+ [milestones]
+ """
+ )
+ self.assertEqual(c.bugzilla_url_stripped,
+ "https://bugzilla.example.com/prefix")
+ c = Config.from_str(
+ """
+ bugzilla_url = "https://bugzilla.example.com/"
+ [people]
+ [milestones]
+ """
+ )
+ self.assertEqual(c.bugzilla_url_stripped,
+ "https://bugzilla.example.com")
+
def test_from_file(self):
def load(text):
with io.StringIO(text) as file:
[people."person1"]
email = "person1@example.com"
aliases = ["alias1"]
+ output_markdown_file = "person1.mdwn"
[milestones]
"Milestone 1" = { canonical_bug_id = 123 }
""")),
"Config(bugzilla_url='https://bugzilla.example.com/', "
"people={'person1': Person(config=..., identifier='person1', "
+ "output_markdown_file='person1.mdwn', "
"aliases={'alias1'}, email='person1@example.com')}, "
"milestones={'Milestone 1': Milestone(config=..., "
"identifier='Milestone 1', canonical_bug_id=123)})")
--- /dev/null
+from pathlib import Path
+from typing import Dict, List, Any
+from io import StringIO
+from budget_sync.budget_graph import BudgetGraph, Node, Payment, PayeeState
+from budget_sync.config import Person, Milestone, Config
+
+
+def _markdown_escape_char(char: str) -> str:
+ if char == "<":
+ return "<"
+ if char == "&":
+ return "&"
+ if char in "\\`*_{}[]()#+-.!":
+ return "\\" + char
+ return char
+
+
+def _markdown_escape(v: Any) -> str:
+ return "".join([char for char in str(v)])
+
+
+def _markdown_for_person(person: Person,
+ payments_dict: Dict[Milestone, List[Payment]]) -> str:
+ buffer = StringIO()
+ print(f"<!-- autogenerated by budget-sync -->", file=buffer)
+ print(f"# {person.identifier}", file=buffer)
+ print(file=buffer)
+ print(f"# Status Tracking", file=buffer)
+ for milestone, payments_list in payments_dict.items():
+ if len(payments_list) == 0:
+ continue
+ print(f"## {milestone.identifier}", file=buffer)
+ for payment in payments_list:
+ # TODO: finish
+ summary = _markdown_escape(payment.node.bug.summary)
+ print(f"* [Bug #{payment.node.bug.id}: "
+ f"{summary}]({payment.node.bug_url})",
+ file=buffer)
+ return buffer.getvalue()
+
+
+def write_budget_markdown(budget_graph: BudgetGraph,
+ output_dir: Path):
+ output_dir.mkdir(parents=True, exist_ok=True)
+ for person, payments_dict in budget_graph.payments.items():
+ output_dir.joinpath(person.output_markdown_file) \
+ .write_text(_markdown_for_person(person, payments_dict),
+ encoding="utf-8")