Digitale bierlijst

model.py 14KB

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