working on code
authorJacob Lifshay <programmerjake@gmail.com>
Wed, 19 Aug 2020 03:05:39 +0000 (20:05 -0700)
committerJacob Lifshay <programmerjake@gmail.com>
Wed, 19 Aug 2020 03:05:39 +0000 (20:05 -0700)
setup.py
src/budget_sync.py [deleted file]
src/budget_sync/__init__.py [new file with mode: 0644]
src/budget_sync/budget_graph.py [new file with mode: 0644]
src/budget_sync/main.py [new file with mode: 0644]
src/budget_sync/money.py [new file with mode: 0644]
src/budget_sync/test/__init__.py [new file with mode: 0644]
src/budget_sync/test/test_money.py [new file with mode: 0644]
src/budget_sync/util.py [new file with mode: 0644]

index 78dd77f2788e4f8c3bda1c6a233f997432845f28..b863c1f350d461b7657312f70a6d729a1e290cf3 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ setup(
     zip_safe=False,
     entry_points={
         "console_scripts": [
-            "budget-sync=budget_sync:main",
+            "budget-sync=budget_sync.main:main",
         ],
     },
     install_requires=install_requires,
diff --git a/src/budget_sync.py b/src/budget_sync.py
deleted file mode 100644 (file)
index 99aa868..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-from bugzilla import Bugzilla
-
-
-def main():
-    print("Hello World!")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/src/budget_sync/__init__.py b/src/budget_sync/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/budget_sync/budget_graph.py b/src/budget_sync/budget_graph.py
new file mode 100644 (file)
index 0000000..3711385
--- /dev/null
@@ -0,0 +1,38 @@
+from bugzilla.bug import Bug
+from bugzilla import Bugzilla
+from typing import Set, Dict, Iterable, Optional
+from budget_sync.util import all_bugs
+from budget_sync.money import Money
+
+
+class Node:
+    parent: Optional["Node"]
+    parent_id: Optional[int]
+    budget_excluding_subtasks: Money
+    budget_including_subtasks: Money
+    nlnet_milestone: Optional[str]
+
+    def __init__(self, bug: Bug, bug_set: Set[Bug] = None):
+        self.bug = bug
+        if bug_set is None:
+            bug_set = {bug}
+        self.bug_set = bug_set
+        self.parent = None
+        self.parent_id = getattr(bug, "cf_budget_parent", None)
+        self.budget_excluding_subtasks = Money.from_str(bug.cf_budget)
+        self.budget_including_subtasks = Money.from_str(bug.cf_total_budget)
+        self.nlnet_milestone = bug.cf_nlnet_milestone
+        if self.nlnet_milestone == "---":
+            self.nlnet_milestone = None
+
+
+class BudgetGraph:
+    nodes: Dict[int, Node]
+
+    def __init__(self, bugs: Iterable[Bug]):
+        self.nodes = {}
+        for bug in bugs:
+            self.nodes[bug.id] = Node(bug)
+        for bug_id, node in self.nodes.items():
+            # if bug.
+            pass
diff --git a/src/budget_sync/main.py b/src/budget_sync/main.py
new file mode 100644 (file)
index 0000000..36b485f
--- /dev/null
@@ -0,0 +1,22 @@
+from bugzilla import Bugzilla
+import logging
+from budget_sync.util import all_bugs
+from budget_sync.budget_graph import BudgetGraph
+
+
+BUGZILLA_URL = "https://bugs.libre-soc.org"
+
+
+def main():
+    logging.info("Using Bugzilla instance at %s", BUGZILLA_URL)
+    bz = Bugzilla(BUGZILLA_URL)
+    logging.debug("Connected to Bugzilla")
+    print(bz.getbug(269).__dict__)
+    print(bz.getbug(1).__dict__)
+    return
+    budget_graph = BudgetGraph(all_bugs(bz))
+    print(budget_graph)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/budget_sync/money.py b/src/budget_sync/money.py
new file mode 100644 (file)
index 0000000..ef16889
--- /dev/null
@@ -0,0 +1,141 @@
+LOG10_CENTS_PER_EURO = 2
+CENTS_PER_EURO = 10 ** LOG10_CENTS_PER_EURO
+
+__all__ = ["Money", "LOG10_CENTS_PER_EURO", "CENTS_PER_EURO"]
+
+
+def _is_ascii_digits(s: str):
+    try:
+        s.encode("ascii")
+    except UnicodeEncodeError:
+        return False
+    return s.isdigit()
+
+
+class Money:
+    """class for handling money, stored as an integer number of cents.
+
+    Note: float is not appropriate for dealing with monetary values due to
+    loss of precision and round-off error. Decimal has similar issues, but to
+    a smaller extent."""
+
+    __slots__ = ["cents"]
+
+    def __init__(self, value=None, *, cents=None):
+        if value is None:
+            if cents is None:
+                cents = 0
+            assert isinstance(cents, int)
+        elif isinstance(value, Money):
+            cents = value.cents
+        elif isinstance(value, int):
+            cents = value * CENTS_PER_EURO
+        elif isinstance(value, float):
+            raise TypeError("float is not an appropriate type"
+                            " for dealing with money")
+        else:
+            cents = self.from_str(value).cents
+        self.cents = cents
+
+    @staticmethod
+    def from_str(text: str):
+        if not isinstance(text, str):
+            raise TypeError("Can't use Money.from_str to "
+                            "convert from non-str value")
+        parts = text.strip().split(".", maxsplit=2)
+        first_part = parts[0]
+        negative = first_part.startswith("-")
+        if first_part.startswith(("-", "+")):
+            first_part = first_part[1:]
+        if first_part == "":
+            euros = 0
+        elif _is_ascii_digits(first_part):
+            euros = int(first_part)
+        else:
+            raise ValueError("invalid Money string: characters after sign and"
+                             " before first `.` must be ascii digits")
+        if len(parts) > 2:
+            raise ValueError("invalid Money string: too many `.` characters")
+        elif len(parts) == 2:
+            if parts[1] == "":
+                if first_part == "":
+                    raise ValueError("invalid Money string: missing digits")
+                cents = 0
+            elif _is_ascii_digits(parts[1]):
+                shift_amount = LOG10_CENTS_PER_EURO - len(parts[1])
+                if shift_amount < 0:
+                    raise ValueError("invalid Money string: too many digits"
+                                     " after `.`")
+                cents = int(parts[1]) * (10 ** shift_amount)
+            else:
+                raise ValueError("invalid Money string: characters"
+                                 " after `.` must be ascii digits")
+        elif first_part == "":
+            raise ValueError("invalid Money string: missing digits")
+        else:
+            cents = 0
+        cents += CENTS_PER_EURO * euros
+        if negative:
+            cents = -cents
+        return Money(cents=cents)
+
+    def __str__(self):
+        retval = "-" if self.cents < 0 else ""
+        retval += str(abs(self.cents) // CENTS_PER_EURO)
+        cents = abs(self.cents) % CENTS_PER_EURO
+        if cents != 0:
+            retval += "."
+            retval += str(cents).zfill(LOG10_CENTS_PER_EURO)
+        return retval
+
+    def __lt__(self, other):
+        return self.cents < Money(other).cents
+
+    def __le__(self, other):
+        return self.cents <= Money(other).cents
+
+    def __eq__(self, other):
+        return self.cents == Money(other).cents
+
+    def __ne__(self, other):
+        return self.cents != Money(other).cents
+
+    def __gt__(self, other):
+        return self.cents > Money(other).cents
+
+    def __ge__(self, other):
+        return self.cents >= Money(other).cents
+
+    def __repr__(self):
+        return f"Money({repr(str(self))})"
+
+    def __bool__(self):
+        return bool(self.cents)
+
+    def __add__(self, other):
+        cents = self.cents + Money(other).cents
+        return Money(cents=cents)
+
+    def __radd__(self, other):
+        cents = Money(other).cents + self.cents
+        return Money(cents=cents)
+
+    def __sub__(self, other):
+        cents = self.cents - Money(other).cents
+        return Money(cents=cents)
+
+    def __rsub__(self, other):
+        cents = Money(other).cents - self.cents
+        return Money(cents=cents)
+
+    def __mul__(self, other):
+        if not isinstance(other, int):
+            raise TypeError("can't multiply by non-int")
+        cents = self.cents * other
+        return Money(cents=cents)
+
+    def __rmul__(self, other):
+        if not isinstance(other, int):
+            raise TypeError("can't multiply by non-int")
+        cents = other * self.cents
+        return Money(cents=cents)
diff --git a/src/budget_sync/test/__init__.py b/src/budget_sync/test/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/budget_sync/test/test_money.py b/src/budget_sync/test/test_money.py
new file mode 100644 (file)
index 0000000..77a22b2
--- /dev/null
@@ -0,0 +1,56 @@
+import unittest
+from budget_sync.money import Money
+
+
+class TestMoney(unittest.TestCase):
+    def test_str(self):
+        self.assertEqual("123.45", str(Money(cents=12345)))
+        self.assertEqual("123", str(Money(cents=12300)))
+        self.assertEqual("123.40", str(Money(cents=12340)))
+        self.assertEqual("120", str(Money(cents=12000)))
+        self.assertEqual("-123.45", str(Money(cents=-12345)))
+        self.assertEqual("-123", str(Money(cents=-12300)))
+        self.assertEqual("-123.40", str(Money(cents=-12340)))
+        self.assertEqual("-120", str(Money(cents=-12000)))
+        self.assertEqual("0", str(Money(cents=0)))
+
+    def test_from_str(self):
+        with self.assertRaisesRegex(TypeError, "^Can't use Money\\.from_str to convert from non-str value$"):
+            Money.from_str(123)
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: missing digits$"):
+            Money.from_str("")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: missing digits$"):
+            Money.from_str(".")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: missing digits$"):
+            Money.from_str("-")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: missing digits$"):
+            Money.from_str("-.")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: characters after sign and before first `\\.` must be ascii digits$"):
+            Money.from_str("+asdjkhfk")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: too many digits after `\\.`$"):
+            Money.from_str("12.345")
+        with self.assertRaisesRegex(ValueError, "^invalid Money string: too many `\\.` characters$"):
+            Money.from_str("12.3.4")
+        self.assertEqual(Money(cents=0), Money.from_str("0"))
+        self.assertEqual(Money(cents=0), Money.from_str("+0"))
+        self.assertEqual(Money(cents=0), Money.from_str("-0"))
+        self.assertEqual(Money(cents=-1000), Money.from_str("-010"))
+        self.assertEqual(Money(cents=1000), Money.from_str("+010"))
+        self.assertEqual(Money(cents=1000), Money.from_str("010"))
+        self.assertEqual(Money(cents=1000), Money.from_str("10"))
+        self.assertEqual(Money(cents=1234), Money.from_str("12.34"))
+        self.assertEqual(Money(cents=-1234), Money.from_str("-12.34"))
+        self.assertEqual(Money(cents=-1234), Money.from_str("-12.34"))
+        self.assertEqual(Money(cents=1230), Money.from_str("12.3"))
+        self.assertEqual(Money(cents=1200), Money.from_str("12."))
+        self.assertEqual(Money(cents=1200), Money.from_str("12"))
+        self.assertEqual(Money(cents=0), Money.from_str(".0"))
+        self.assertEqual(Money(cents=10), Money.from_str(".1"))
+        self.assertEqual(Money(cents=12), Money.from_str(".12"))
+        self.assertEqual(Money(cents=-12), Money.from_str("-.12"))
+
+    # FIXME(programmerjake): add other methods
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/src/budget_sync/util.py b/src/budget_sync/util.py
new file mode 100644 (file)
index 0000000..ed1c300
--- /dev/null
@@ -0,0 +1,14 @@
+from bugzilla import Bugzilla
+from bugzilla.bug import Bug
+from typing import Iterator
+
+
+def all_bugs(bz: Bugzilla) -> Iterator[Bug]:
+    chunk_start = 1
+    chunk_size = 100
+    while True:
+        bugs = bz.getbugs(list(range(chunk_start, chunk_start + chunk_size)))
+        chunk_start += chunk_size
+        yield from bugs
+        if len(bugs) < chunk_size:
+            return