Digitale bierlijst

model.py 14KB

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