zip_safe=False,
entry_points={
"console_scripts": [
- "budget-sync=budget_sync:main",
+ "budget-sync=budget_sync.main:main",
],
},
install_requires=install_requires,
+++ /dev/null
-from bugzilla import Bugzilla
-
-
-def main():
- print("Hello World!")
-
-
-if __name__ == "__main__":
- main()
--- /dev/null
+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
--- /dev/null
+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()
--- /dev/null
+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)
--- /dev/null
+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()
--- /dev/null
+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