working on implementing write_budget_markdown
authorJacob Lifshay <programmerjake@gmail.com>
Sat, 12 Sep 2020 01:29:01 +0000 (18:29 -0700)
committerJacob Lifshay <programmerjake@gmail.com>
Sat, 12 Sep 2020 01:29:01 +0000 (18:29 -0700)
.gitignore
budget-sync-config.toml
src/budget_sync/budget_graph.py
src/budget_sync/config.py
src/budget_sync/main.py
src/budget_sync/test/test_budget_graph.py
src/budget_sync/test/test_config.py
src/budget_sync/write_budget_markdown.py [new file with mode: 0644]

index ae15ee2d03a6a119be463b31bb6c0ff44edf5bed..3336860f3024e859bf25dba0f6ca58f06565724a 100644 (file)
@@ -1,4 +1,5 @@
 __pycache__
 *.egg-info
 /.vscode
-*.pyc
\ No newline at end of file
+*.pyc
+/output
\ No newline at end of file
index 60ae440353f8e41234728bb3c7d2c6f9d2fdf0d4..9366f2f263b35f195aa96e9e11e7a5eecc1c6269 100644 (file)
@@ -3,22 +3,27 @@ bugzilla_url = "https://bugs.libre-soc.org"
 [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 }
index c6b527245104aebaae046f37be19a8ffdddd9df6..463c3e1b397b8599fba4e96fd6e6dae380868972 100644 (file)
@@ -236,6 +236,11 @@ class Node:
         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:
index 1bebca1211b754f512b9e20639c0e066053362ac..8a490b5ea61f4df3e50a3d6d3cf9a62275b09c04 100644 (file)
@@ -13,10 +13,12 @@ class Person:
     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
@@ -35,8 +37,9 @@ class Person:
         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:
@@ -70,6 +73,10 @@ class Config:
             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
@@ -124,6 +131,7 @@ class Config:
                 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":
@@ -144,10 +152,20 @@ class Config:
                         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):
index d762f08e473f8d6880a3a173a59cd237a9d99c26..2726dbf0457bcf289b056213431c21a625a862f5 100644 (file)
@@ -1,12 +1,11 @@
 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():
@@ -17,6 +16,11 @@ 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:
@@ -30,6 +34,8 @@ def main():
     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__":
index 21786a18b744954e47c992a3617f6d16ca53bf29..a8857cb8793e8b80134740ffba121d90052354f0 100644 (file)
@@ -127,12 +127,15 @@ EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
 
 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 }
@@ -176,6 +179,8 @@ class TestBudgetGraph(unittest.TestCase):
         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)
@@ -212,6 +217,8 @@ class TestBudgetGraph(unittest.TestCase):
         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, {})
@@ -225,6 +232,8 @@ class TestBudgetGraph(unittest.TestCase):
         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([
index 3b203a1b8b292f727a4c31416102bce8f63d069d..6a40c18c30237abb11bd2a9e1c4114120b7d2646 100644 (file)
@@ -78,25 +78,45 @@ class TestConfig(unittest.TestCase):
             [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(
@@ -112,15 +132,18 @@ class TestConfig(unittest.TestCase):
             [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(
@@ -128,8 +151,10 @@ class TestConfig(unittest.TestCase):
             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 "
@@ -139,9 +164,11 @@ class TestConfig(unittest.TestCase):
             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 "
@@ -205,8 +232,10 @@ class TestConfig(unittest.TestCase):
             [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']
@@ -237,6 +266,35 @@ class TestConfig(unittest.TestCase):
                              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:
@@ -257,11 +315,13 @@ class TestConfig(unittest.TestCase):
             [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)})")
diff --git a/src/budget_sync/write_budget_markdown.py b/src/budget_sync/write_budget_markdown.py
new file mode 100644 (file)
index 0000000..efe3598
--- /dev/null
@@ -0,0 +1,48 @@
+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 "&lt;"
+    if char == "&":
+        return "&amp;"
+    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")