Digitale bierlijst

model.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. """
  2. Provides access to the models stored in the database, via the server.
  3. """
  4. import datetime
  5. import logging
  6. from typing import NamedTuple, Sequence
  7. from urllib.parse import urljoin
  8. import requests
  9. LOG = logging.getLogger(__name__)
  10. SERVER_URL = "http://127.0.0.1:5000"
  11. DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
  12. class ServerStatus:
  13. """ Provides helper classes to check whether the server is up. """
  14. @classmethod
  15. def is_server_running(cls) -> bool:
  16. try:
  17. req = requests.get(urljoin(SERVER_URL, "ping"))
  18. if req.status_code == 200:
  19. return True, req.content
  20. return False, req.content
  21. except requests.ConnectionError as ex:
  22. return False, ex
  23. @classmethod
  24. def unsettled_consumptions(cls) -> dict:
  25. req = requests.get(urljoin(SERVER_URL, "status"))
  26. data = req.json()
  27. if data["unsettled"]["amount"]:
  28. data["unsettled"]["first"] = datetime.datetime.strptime(
  29. data["unsettled"]["first"], DATETIME_FORMAT
  30. )
  31. data["unsettled"]["last"] = datetime.datetime.strptime(
  32. data["unsettled"]["last"], DATETIME_FORMAT
  33. )
  34. return data
  35. class Person(NamedTuple):
  36. """ Represents a Person, as retrieved from the database. """
  37. name: str
  38. active: bool = True
  39. person_id: int = None
  40. consumptions: dict = {}
  41. def add_consumption(self, type_id: str) -> bool:
  42. """ Register a consumption for this Person. """
  43. req = requests.post(
  44. urljoin(SERVER_URL, f"people/{self.person_id}/add_consumption/{type_id}")
  45. )
  46. try:
  47. data = req.json()
  48. if "error" in data:
  49. LOG.error(
  50. "Could not add consumption for %s (%s): %s",
  51. self.person_id,
  52. req.status_code,
  53. data,
  54. )
  55. return False
  56. self.consumptions.update(data["person"]["consumptions"])
  57. return Consumption.from_dict(data["consumption"])
  58. except ValueError:
  59. LOG.error(
  60. "Did not get JSON on adding Consumption (%s): %s",
  61. req.status_code,
  62. req.content,
  63. )
  64. return False
  65. def create(self) -> "Person":
  66. """ Create a new Person from the current attributes. As tuples are
  67. immutable, a new Person with the correct id is returned. """
  68. req = requests.post(
  69. urljoin(SERVER_URL, "people"),
  70. json={"person": {"name": self.name, "active": True}},
  71. )
  72. try:
  73. data = req.json()
  74. except ValueError:
  75. LOG.error(
  76. "Did not get JSON on adding Person (%s): %s",
  77. req.status_code,
  78. req.content,
  79. )
  80. return None
  81. if "error" in data or req.status_code != 201:
  82. LOG.error("Could not create Person (%s): %s", req.status_code, data)
  83. return None
  84. return Person.from_dict(data["person"])
  85. def set_active(self, new_state=True) -> "Person":
  86. req = requests.patch(
  87. urljoin(SERVER_URL, f"people/{self.person_id}"),
  88. json={"person": {"active": new_state}},
  89. )
  90. try:
  91. data = req.json()
  92. except ValueError:
  93. LOG.error(
  94. "Did not get JSON on updating Person (%s): %s",
  95. req.status_code,
  96. req.content,
  97. )
  98. return None
  99. if "error" in data or req.status_code != 200:
  100. LOG.error("Could not update Person (%s): %s", req.status_code, data)
  101. return None
  102. return Person.from_dict(data["person"])
  103. @classmethod
  104. def get(cls, person_id: int) -> "Person":
  105. """ Retrieve a Person by id. """
  106. req = requests.get(urljoin(SERVER_URL, f"/people/{person_id}"))
  107. try:
  108. data = req.json()
  109. if "error" in data:
  110. LOG.warning(
  111. "Could not get person %s (%s): %s", person_id, req.status_code, data
  112. )
  113. return None
  114. return Person.from_dict(data["person"])
  115. except ValueError:
  116. LOG.error(
  117. "Did not get JSON from server on getting Person (%s): %s",
  118. req.status_code,
  119. req.content,
  120. )
  121. return None
  122. @classmethod
  123. def get_all(cls, active=None) -> ["Person"]:
  124. """ Get all active People. """
  125. params = {}
  126. if active is not None:
  127. params["active"] = int(active)
  128. req = requests.get(urljoin(SERVER_URL, "/people"), params=params)
  129. try:
  130. data = req.json()
  131. if "error" in data:
  132. LOG.warning("Could not get people (%s): %s", req.status_code, data)
  133. return [Person.from_dict(item) for item in data["people"]]
  134. except ValueError:
  135. LOG.error(
  136. "Did not get JSON from server on getting People (%s): %s",
  137. req.status_code,
  138. req.content,
  139. )
  140. return None
  141. @classmethod
  142. def from_dict(cls, data: dict) -> "Person":
  143. """ Reconstruct a Person object from a dict. """
  144. return Person(
  145. name=data["name"],
  146. active=data["active"],
  147. person_id=data["person_id"],
  148. consumptions=data["consumptions"],
  149. )
  150. class Export(NamedTuple):
  151. created_at: datetime.datetime
  152. settlement_ids: Sequence[int]
  153. export_id: int
  154. settlements: Sequence["Settlement"] = []
  155. @classmethod
  156. def from_dict(cls, data: dict) -> "Export":
  157. """ Reconstruct an Export from a dict. """
  158. return cls(
  159. export_id=data["export_id"],
  160. created_at=datetime.datetime.strptime(data["created_at"], DATETIME_FORMAT),
  161. settlement_ids=data["settlement_ids"],
  162. settlements=data.get("settlements", []),
  163. )
  164. @classmethod
  165. def get_all(cls) -> ["Export"]:
  166. """ Get a list of all existing Exports. """
  167. req = requests.get(urljoin(SERVER_URL, "exports"))
  168. try:
  169. data = req.json()
  170. except ValueError:
  171. LOG.error(
  172. "Did not get JSON on listing Exports (%s): %s",
  173. req.status_code,
  174. req.content,
  175. )
  176. return None
  177. if "error" in data or req.status_code != 200:
  178. LOG.error("Could not list Exports (%s): %s", req.status_code, data)
  179. return None
  180. return [cls.from_dict(e) for e in data["exports"]]
  181. @classmethod
  182. def get(cls, export_id: int) -> "Export":
  183. """ Retrieve one Export. """
  184. req = requests.get(urljoin(SERVER_URL, f"exports/{export_id}"))
  185. try:
  186. data = req.json()
  187. except ValueError:
  188. LOG.error(
  189. "Did not get JSON on getting Export (%s): %s",
  190. req.status_code,
  191. req.content,
  192. )
  193. return None
  194. if "error" in data or req.status_code != 200:
  195. LOG.error("Could not get Export (%s): %s", req.status_code, data)
  196. return None
  197. data["export"]["settlements"] = data["settlements"]
  198. return cls.from_dict(data["export"])
  199. @classmethod
  200. def create(cls) -> "Export":
  201. """ Create a new Export, containing all un-exported Settlements. """
  202. req = requests.post(urljoin(SERVER_URL, "exports"))
  203. try:
  204. data = req.json()
  205. except ValueError:
  206. LOG.error(
  207. "Did not get JSON on adding Export (%s): %s",
  208. req.status_code,
  209. req.content,
  210. )
  211. return None
  212. if "error" in data or req.status_code != 201:
  213. LOG.error("Could not create Export (%s): %s", req.status_code, data)
  214. return None
  215. data["export"]["settlements"] = data["settlements"]
  216. return cls.from_dict(data["export"])
  217. class ConsumptionType(NamedTuple):
  218. """ Represents a stored ConsumptionType. """
  219. name: str
  220. consumption_type_id: int = None
  221. icon: str = None
  222. def create(self) -> "ConsumptionType":
  223. """ Create a new ConsumptionType from the current attributes. As tuples
  224. are immutable, a new ConsumptionType with the correct id is returned.
  225. """
  226. req = requests.post(
  227. urljoin(SERVER_URL, "consumption_types"),
  228. json={"consumption_type": {"name": self.name, "icon": self.icon}},
  229. )
  230. try:
  231. data = req.json()
  232. except ValueError:
  233. LOG.error(
  234. "Did not get JSON on adding ConsumptionType (%s): %s",
  235. req.status_code,
  236. req.content,
  237. )
  238. return None
  239. if "error" in data or req.status_code != 201:
  240. LOG.error(
  241. "Could not create ConsumptionType (%s): %s", req.status_code, data
  242. )
  243. return None
  244. return ConsumptionType.from_dict(data["consumption_type"])
  245. @classmethod
  246. def get(cls, consumption_type_id: int) -> "ConsumptionType":
  247. """ Retrieve a ConsumptionType by id. """
  248. req = requests.get(
  249. urljoin(SERVER_URL, f"/consumption_types/{consumption_type_id}")
  250. )
  251. try:
  252. data = req.json()
  253. if "error" in data:
  254. LOG.warning(
  255. "Could not get consumption type %s (%s): %s",
  256. consumption_type_id,
  257. req.status_code,
  258. data,
  259. )
  260. return None
  261. return cls.from_dict(data["consumption_type"])
  262. except ValueError:
  263. LOG.error(
  264. "Did not get JSON from server on getting consumption type (%s): %s",
  265. req.status_code,
  266. req.content,
  267. )
  268. return None
  269. @classmethod
  270. def get_all(cls) -> ["ConsumptionType"]:
  271. """ Get all active ConsumptionTypes. """
  272. req = requests.get(urljoin(SERVER_URL, "/consumption_types"))
  273. try:
  274. data = req.json()
  275. if "error" in data:
  276. LOG.warning(
  277. "Could not get consumption types (%s): %s", req.status_code, data
  278. )
  279. return [cls.from_dict(item) for item in data["consumption_types"]]
  280. except ValueError:
  281. LOG.error(
  282. "Did not get JSON from server on getting ConsumptionTypes (%s): %s",
  283. req.status_code,
  284. req.content,
  285. )
  286. return None
  287. @classmethod
  288. def from_dict(cls, data: dict) -> "ConsumptionType":
  289. """ Reconstruct a ConsumptionType from a dict. """
  290. return cls(
  291. name=data["name"],
  292. consumption_type_id=data["consumption_type_id"],
  293. icon=data.get("icon"),
  294. )
  295. class Consumption(NamedTuple):
  296. """ Represents a stored Consumption. """
  297. consumption_id: int
  298. person_id: int
  299. consumption_type_id: int
  300. created_at: datetime.datetime
  301. reversed: bool = False
  302. settlement_id: int = None
  303. @classmethod
  304. def from_dict(cls, data: dict) -> "Consumption":
  305. """ Reconstruct a Consumption from a dict. """
  306. return cls(
  307. consumption_id=data["consumption_id"],
  308. person_id=data["person_id"],
  309. consumption_type_id=data["consumption_type_id"],
  310. settlement_id=data["settlement_id"],
  311. created_at=datetime.datetime.strptime(data["created_at"], DATETIME_FORMAT),
  312. reversed=data["reversed"],
  313. )
  314. def reverse(self) -> "Consumption":
  315. """ Reverse this consumption. """
  316. req = requests.delete(
  317. urljoin(SERVER_URL, f"/consumptions/{self.consumption_id}")
  318. )
  319. try:
  320. data = req.json()
  321. if "error" in data:
  322. LOG.error(
  323. "Could not reverse consumption %s (%s): %s",
  324. self.consumption_id,
  325. req.status_code,
  326. data,
  327. )
  328. return False
  329. return Consumption.from_dict(data["consumption"])
  330. except ValueError:
  331. LOG.error(
  332. "Did not get JSON on reversing Consumption (%s): %s",
  333. req.status_code,
  334. req.content,
  335. )
  336. return False
  337. class Settlement(NamedTuple):
  338. """ Represents a stored Settlement. """
  339. settlement_id: int
  340. name: str
  341. consumption_summary: dict
  342. count_info: dict = {}
  343. @classmethod
  344. def from_dict(cls, data: dict) -> "Settlement":
  345. return Settlement(
  346. settlement_id=data["settlement_id"],
  347. name=data["name"],
  348. consumption_summary=data["consumption_summary"],
  349. count_info=data.get("count_info", {}),
  350. )
  351. @classmethod
  352. def create(cls, name: str) -> "Settlement":
  353. req = requests.post(
  354. urljoin(SERVER_URL, "/settlements"), json={"settlement": {"name": name}}
  355. )
  356. return cls.from_dict(req.json()["settlement"])
  357. @classmethod
  358. def get(cls, settlement_id: int) -> "Settlement":
  359. req = requests.get(urljoin(SERVER_URL, f"/settlements/{settlement_id}"))
  360. try:
  361. data = req.json()
  362. except ValueError:
  363. LOG.error(
  364. "Did not get JSON on retrieving Settlement (%s): %s",
  365. req.status_code,
  366. req.content,
  367. )
  368. return None
  369. if "error" in data or req.status_code != 200:
  370. LOG.error("Could not get Export (%s): %s", req.status_code, data)
  371. return None
  372. data["settlement"]["count_info"] = data["count_info"]
  373. return cls.from_dict(data["settlement"])