Digitale bierlijst

model.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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
  7. from urllib.parse import urljoin
  8. import requests
  9. LOG = logging.getLogger(__name__)
  10. SERVER_URL = "http://127.0.0.1:5000"
  11. class ServerStatus:
  12. """ Provides helper classes to check whether the server is up. """
  13. @classmethod
  14. def is_server_running(cls) -> bool:
  15. try:
  16. req = requests.get(urljoin(SERVER_URL, "ping"))
  17. if req.status_code == 200:
  18. return True, req.content
  19. return False, req.content
  20. except requests.ConnectionError as ex:
  21. return False, ex
  22. datetime_format = "%Y-%m-%dT%H:%M:%S.%f"
  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"], cls.datetime_format
  30. )
  31. data["unsettled"]["last"] = datetime.datetime.strptime(
  32. data["unsettled"]["last"], cls.datetime_format
  33. )
  34. return data
  35. class Person(NamedTuple):
  36. """ Represents a Person, as retrieved from the database. """
  37. name: str
  38. person_id: int = None
  39. consumptions: dict = {}
  40. def add_consumption(self, type_id: str) -> bool:
  41. """ Register a consumption for this Person. """
  42. req = requests.post(
  43. urljoin(SERVER_URL, f"people/{self.person_id}/add_consumption/{type_id}")
  44. )
  45. try:
  46. data = req.json()
  47. if "error" in data:
  48. LOG.error(
  49. "Could not add consumption for %s (%s): %s",
  50. self.person_id,
  51. req.status_code,
  52. data,
  53. )
  54. return False
  55. self.consumptions.update(data["person"]["consumptions"])
  56. return Consumption.from_dict(data["consumption"])
  57. except ValueError:
  58. LOG.error(
  59. "Did not get JSON on adding Consumption (%s): %s",
  60. req.status_code,
  61. req.content,
  62. )
  63. return False
  64. def create(self) -> "Person":
  65. """ Create a new Person from the current attributes. As tuples are
  66. immutable, a new Person with the correct id is returned. """
  67. req = requests.post(
  68. urljoin(SERVER_URL, "people"),
  69. json={"person": {"name": self.name, "active": True}},
  70. )
  71. try:
  72. data = req.json()
  73. except ValueError:
  74. LOG.error(
  75. "Did not get JSON on adding Person (%s): %s",
  76. req.status_code,
  77. req.content,
  78. )
  79. return None
  80. if "error" in data or req.status_code != 201:
  81. LOG.error("Could not create Person (%s): %s", req.status_code, data)
  82. return None
  83. return Person.from_dict(data["person"])
  84. def set_active(self, new_state=True) -> "Person":
  85. req = requests.patch(
  86. urljoin(SERVER_URL, f"people/{self.person_id}"),
  87. json={"person": {"active": new_state}},
  88. )
  89. try:
  90. data = req.json()
  91. except ValueError:
  92. LOG.error(
  93. "Did not get JSON on updating Person (%s): %s",
  94. req.status_code,
  95. req.content,
  96. )
  97. return None
  98. if "error" in data or req.status_code != 200:
  99. LOG.error("Could not update Person (%s): %s", req.status_code, data)
  100. return None
  101. return Person.from_dict(data["person"])
  102. @classmethod
  103. def get(cls, person_id: int) -> "Person":
  104. """ Retrieve a Person by id. """
  105. req = requests.get(urljoin(SERVER_URL, f"/people/{person_id}"))
  106. try:
  107. data = req.json()
  108. if "error" in data:
  109. LOG.warning(
  110. "Could not get person %s (%s): %s", person_id, req.status_code, data
  111. )
  112. return None
  113. return Person.from_dict(data["person"])
  114. except ValueError:
  115. LOG.error(
  116. "Did not get JSON from server on getting Person (%s): %s",
  117. req.status_code,
  118. req.content,
  119. )
  120. return None
  121. @classmethod
  122. def get_all(cls, active=None) -> ["Person"]:
  123. """ Get all active People. """
  124. active = int(active)
  125. req = requests.get(urljoin(SERVER_URL, "/people"), params={"active": active})
  126. try:
  127. data = req.json()
  128. if "error" in data:
  129. LOG.warning("Could not get people (%s): %s", req.status_code, data)
  130. return [Person.from_dict(item) for item in data["people"]]
  131. except ValueError:
  132. LOG.error(
  133. "Did not get JSON from server on getting People (%s): %s",
  134. req.status_code,
  135. req.content,
  136. )
  137. return None
  138. @classmethod
  139. def from_dict(cls, data: dict) -> "Person":
  140. """ Reconstruct a Person object from a dict. """
  141. return Person(
  142. name=data["name"],
  143. person_id=data["person_id"],
  144. consumptions=data["consumptions"],
  145. )
  146. class ConsumptionType(NamedTuple):
  147. """ Represents a stored ConsumptionType. """
  148. name: str
  149. consumption_type_id: int = None
  150. icon: str = None
  151. def create(self) -> "ConsumptionType":
  152. """ Create a new ConsumptionType from the current attributes. As tuples
  153. are immutable, a new ConsumptionType with the correct id is returned.
  154. """
  155. req = requests.post(
  156. urljoin(SERVER_URL, "consumption_types"),
  157. json={"consumption_type": {"name": self.name, "icon": self.icon}},
  158. )
  159. try:
  160. data = req.json()
  161. except ValueError:
  162. LOG.error(
  163. "Did not get JSON on adding ConsumptionType (%s): %s",
  164. req.status_code,
  165. req.content,
  166. )
  167. return None
  168. if "error" in data or req.status_code != 201:
  169. LOG.error(
  170. "Could not create ConsumptionType (%s): %s", req.status_code, data
  171. )
  172. return None
  173. return ConsumptionType.from_dict(data["consumption_type"])
  174. @classmethod
  175. def get(cls, consumption_type_id: int) -> "ConsumptionType":
  176. """ Retrieve a ConsumptionType by id. """
  177. req = requests.get(
  178. urljoin(SERVER_URL, f"/consumption_types/{consumption_type_id}")
  179. )
  180. try:
  181. data = req.json()
  182. if "error" in data:
  183. LOG.warning(
  184. "Could not get consumption type %s (%s): %s",
  185. consumption_type_id,
  186. req.status_code,
  187. data,
  188. )
  189. return None
  190. return cls.from_dict(data["consumption_type"])
  191. except ValueError:
  192. LOG.error(
  193. "Did not get JSON from server on getting consumption type (%s): %s",
  194. req.status_code,
  195. req.content,
  196. )
  197. return None
  198. @classmethod
  199. def get_all(cls) -> ["ConsumptionType"]:
  200. """ Get all active ConsumptionTypes. """
  201. req = requests.get(urljoin(SERVER_URL, "/consumption_types"))
  202. try:
  203. data = req.json()
  204. if "error" in data:
  205. LOG.warning(
  206. "Could not get consumption types (%s): %s", req.status_code, data
  207. )
  208. return [cls.from_dict(item) for item in data["consumption_types"]]
  209. except ValueError:
  210. LOG.error(
  211. "Did not get JSON from server on getting ConsumptionTypes (%s): %s",
  212. req.status_code,
  213. req.content,
  214. )
  215. return None
  216. @classmethod
  217. def from_dict(cls, data: dict) -> "ConsumptionType":
  218. """ Reconstruct a ConsumptionType from a dict. """
  219. return cls(
  220. name=data["name"],
  221. consumption_type_id=data["consumption_type_id"],
  222. icon=data.get("icon"),
  223. )
  224. class Consumption(NamedTuple):
  225. """ Represents a stored Consumption. """
  226. consumption_id: int
  227. person_id: int
  228. consumption_type_id: int
  229. created_at: datetime.datetime
  230. reversed: bool = False
  231. settlement_id: int = None
  232. @classmethod
  233. def from_dict(cls, data: dict) -> "Consumption":
  234. """ Reconstruct a Consumption from a dict. """
  235. datetime_format = "%Y-%m-%dT%H:%M:%S.%f"
  236. # 2018-08-31T17:30:47.871521
  237. return cls(
  238. consumption_id=data["consumption_id"],
  239. person_id=data["person_id"],
  240. consumption_type_id=data["consumption_type_id"],
  241. settlement_id=data["settlement_id"],
  242. created_at=datetime.datetime.strptime(data["created_at"], datetime_format),
  243. reversed=data["reversed"],
  244. )
  245. def reverse(self) -> "Consumption":
  246. """ Reverse this consumption. """
  247. req = requests.delete(
  248. urljoin(SERVER_URL, f"/consumptions/{self.consumption_id}")
  249. )
  250. try:
  251. data = req.json()
  252. if "error" in data:
  253. LOG.error(
  254. "Could not reverse consumption %s (%s): %s",
  255. self.consumption_id,
  256. req.status_code,
  257. data,
  258. )
  259. return False
  260. return Consumption.from_dict(data["consumption"])
  261. except ValueError:
  262. LOG.error(
  263. "Did not get JSON on reversing Consumption (%s): %s",
  264. req.status_code,
  265. req.content,
  266. )
  267. return False
  268. class Settlement(NamedTuple):
  269. """ Represents a stored Settlement. """
  270. settlement_id: int
  271. name: str
  272. consumption_summary: dict
  273. @classmethod
  274. def from_dict(cls, data: dict) -> "Settlement":
  275. return Settlement(
  276. settlement_id=data["settlement_id"],
  277. name=data["name"],
  278. consumption_summary=data["consumption_summary"],
  279. )
  280. @classmethod
  281. def create(cls, name: str) -> "Settlement":
  282. req = requests.post(
  283. urljoin(SERVER_URL, "/settlements"), json={"settlement": {"name": name}}
  284. )
  285. return cls.from_dict(req.json()["settlement"])