From 59fed33efb20451aca6cbc8db8dd1db98aaf08b5 Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Tue, 18 Aug 2020 20:05:39 -0700 Subject: [PATCH] working on code --- setup.py | 2 +- src/budget_sync.py | 9 -- src/budget_sync/__init__.py | 0 src/budget_sync/budget_graph.py | 38 ++++++++ src/budget_sync/main.py | 22 +++++ src/budget_sync/money.py | 141 +++++++++++++++++++++++++++++ src/budget_sync/test/__init__.py | 0 src/budget_sync/test/test_money.py | 56 ++++++++++++ src/budget_sync/util.py | 14 +++ 9 files changed, 272 insertions(+), 10 deletions(-) delete mode 100644 src/budget_sync.py create mode 100644 src/budget_sync/__init__.py create mode 100644 src/budget_sync/budget_graph.py create mode 100644 src/budget_sync/main.py create mode 100644 src/budget_sync/money.py create mode 100644 src/budget_sync/test/__init__.py create mode 100644 src/budget_sync/test/test_money.py create mode 100644 src/budget_sync/util.py diff --git a/setup.py b/setup.py index 78dd77f..b863c1f 100644 --- 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 index 99aa868..0000000 --- a/src/budget_sync.py +++ /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 index 0000000..e69de29 diff --git a/src/budget_sync/budget_graph.py b/src/budget_sync/budget_graph.py new file mode 100644 index 0000000..3711385 --- /dev/null +++ b/src/budget_sync/budget_graph.py @@ -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 index 0000000..36b485f --- /dev/null +++ b/src/budget_sync/main.py @@ -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 index 0000000..ef16889 --- /dev/null +++ b/src/budget_sync/money.py @@ -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 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 index 0000000..77a22b2 --- /dev/null +++ b/src/budget_sync/test/test_money.py @@ -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 index 0000000..ed1c300 --- /dev/null +++ b/src/budget_sync/util.py @@ -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 -- 2.30.2