Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Der-Henning
GitHub Repository: Der-Henning/tgtg
Path: blob/main/tgtg_scanner/models/item.py
725 views
1
import datetime
2
import logging
3
import re
4
from http import HTTPStatus
5
from typing import Any, Union
6
7
import babel.numbers
8
import humanize
9
import requests
10
11
from tgtg_scanner.errors import MaskConfigurationError
12
from tgtg_scanner.models.location import DistanceTime, Location
13
14
ATTRS = [
15
"item_id",
16
"items_available",
17
"display_name",
18
"description",
19
"price",
20
"value",
21
"currency",
22
"pickupdate",
23
"favorite",
24
"rating",
25
"buffet",
26
"item_category",
27
"item_name",
28
"packaging_option",
29
"pickup_location",
30
"store_name",
31
"item_logo",
32
"item_cover",
33
"scanned_on",
34
"item_logo_bytes",
35
"item_cover_bytes",
36
"link",
37
"distance_walking",
38
"distance_driving",
39
"distance_transit",
40
"distance_biking",
41
"duration_walking",
42
"duration_driving",
43
"duration_transit",
44
"duration_biking",
45
]
46
47
log = logging.getLogger("tgtg")
48
49
50
class Item:
51
"""
52
Takes the raw data from the TGTG API and
53
returns well formated data for notifications.
54
"""
55
56
def __init__(self, data: dict, location: Union[Location, None] = None, locale: str = "en_US"):
57
self.items_available: int = data.get("items_available", 0)
58
self.display_name: str = data.get("display_name", "-")
59
self.favorite: str = "Yes" if data.get("favorite", False) else "No"
60
self.pickup_interval_start: Union[str, None] = data.get("pickup_interval", {}).get("start", None)
61
self.pickup_interval_end: Union[str, None] = data.get("pickup_interval", {}).get("end", None)
62
self.pickup_location: str = data.get("pickup_location", {}).get("address", {}).get("address_line", "-")
63
64
item: dict = data.get("item", {})
65
self.item_id: str = item.get("item_id", None)
66
self._rating: Union[float, None] = item.get("average_overall_rating", {}).get("average_overall_rating", None)
67
self.packaging_option: str = item.get("packaging_option", "-")
68
self.item_name: str = item.get("name", "-")
69
self.buffet: str = "Yes" if item.get("buffet", False) else "No"
70
self.item_category: str = item.get("item_category", "-")
71
self.description: str = item.get("description", "-")
72
item_price: dict = item.get("item_price", {})
73
item_value: dict = item.get("item_value", {})
74
self._price: float = item_price.get("minor_units", 0) / 10 ** item_price.get("decimals", 0)
75
self._value: float = item_value.get("minor_units", 0) / 10 ** item_value.get("decimals", 0)
76
self.currency: str = item_price.get("code", "-")
77
self.item_logo: str = item.get("logo_picture", {}).get(
78
"current_url",
79
"https://tgtg-mkt-cms-prod.s3.eu-west-1.amazonaws.com/13512/TGTG_Icon_White_Cirle_1988x1988px_RGB.png",
80
)
81
self.item_cover: str = item.get("cover_picture", {}).get(
82
"current_url",
83
"https://images.tgtg.ninja/standard_images/GENERAL/other1.jpg",
84
)
85
86
store: dict = data.get("store", {})
87
self.store_name: str = store.get("store_name", "-")
88
89
self.scanned_on: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
90
self.location = location
91
self.locale = locale
92
93
@property
94
def rating(self) -> str:
95
if self._rating is None:
96
return "-"
97
return self._format_decimal(round(self._rating, 1))
98
99
@property
100
def price(self) -> str:
101
return self._format_currency(self._price)
102
103
@property
104
def value(self) -> str:
105
return self._format_currency(self._value)
106
107
def _format_decimal(self, number: float) -> str:
108
return babel.numbers.format_decimal(number, locale=self.locale)
109
110
def _format_currency(self, number: float) -> str:
111
if self.currency == "-":
112
return self._format_decimal(number)
113
return babel.numbers.format_currency(number, self.currency, locale=self.locale)
114
115
@staticmethod
116
def _datetimeparse(datestr: str) -> datetime.datetime:
117
"""
118
Formates datetime string from tgtg api
119
"""
120
fmt = "%Y-%m-%dT%H:%M:%SZ"
121
value = datetime.datetime.strptime(datestr, fmt)
122
return value.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None)
123
124
@staticmethod
125
def check_mask(text: str) -> None:
126
"""
127
Checks whether the variables in the provided string are available
128
129
Raises MaskConfigurationError
130
"""
131
for match in re.finditer(r"\${{([a-zA-Z0-9_]+)}}", text):
132
if not match.group(1) in ATTRS:
133
raise MaskConfigurationError(match.group(0))
134
135
@staticmethod
136
def get_image(url: str) -> Union[bytes, None]:
137
response = requests.get(url)
138
if not response.status_code == HTTPStatus.OK:
139
log.warning("Get Image Error: %s - %s", response.status_code, response.content)
140
return None
141
return response.content
142
143
@property
144
def item_logo_bytes(self) -> Union[bytes, None]:
145
return self.get_image(self.item_logo)
146
147
@property
148
def item_cover_bytes(self) -> Union[bytes, None]:
149
return self.get_image(self.item_cover)
150
151
@property
152
def link(self) -> str:
153
return f"https://share.toogoodtogo.com/item/{self.item_id}"
154
155
def _get_variables(self, text: str) -> list[re.Match]:
156
"""
157
Returns a list of all variables in the provided string
158
"""
159
return list(re.finditer(r"\${{([a-zA-Z0-9_]+)}}", text))
160
161
def unmask(self, text: str) -> str:
162
"""
163
Replaces variables with the current values.
164
"""
165
if text in ["${{item_logo_bytes}}", "${{item_cover_bytes}}"]:
166
matches = self._get_variables(text)
167
return getattr(self, matches[0].group(1))
168
for match in self._get_variables(text):
169
if hasattr(self, match.group(1)):
170
val = getattr(self, match.group(1))
171
text = text.replace(match.group(0), str(val))
172
return text
173
174
@property
175
def pickupdate(self) -> str:
176
"""
177
Returns a well formated string, providing the pickup time range
178
"""
179
if self.pickup_interval_start is None or self.pickup_interval_end is None:
180
return "-"
181
now = datetime.datetime.now()
182
pfr = self._datetimeparse(self.pickup_interval_start)
183
pto = self._datetimeparse(self.pickup_interval_end)
184
prange = f"{pfr.hour:02d}:{pfr.minute:02d} - {pto.hour:02d}:{pto.minute:02d}"
185
tommorrow = now + datetime.timedelta(days=1)
186
if now.date() == pfr.date():
187
return f"{humanize.naturalday(now)}, {prange}"
188
if (pfr.date() - now.date()).days == 1:
189
return f"{humanize.naturalday(tommorrow)}, {prange}"
190
return f"{pfr.day}/{pfr.month}, {prange}"
191
192
def _get_distance_time(self, travel_mode: str) -> Union[DistanceTime, None]:
193
if self.location is None:
194
return None
195
return self.location.calculate_distance_time(self.pickup_location, travel_mode)
196
197
def _get_distance(self, travel_mode: str) -> str:
198
distance_time = self._get_distance_time(travel_mode)
199
if distance_time is None:
200
return "n/a"
201
return f"{distance_time.distance / 1000:.1f} km"
202
203
def _get_duration(self, travel_mode: str) -> str:
204
distance_time = self._get_distance_time(travel_mode)
205
if distance_time is None:
206
return "n/a"
207
return humanize.precisedelta(
208
datetime.timedelta(seconds=distance_time.duration),
209
minimum_unit="minutes",
210
format="%0.0f",
211
)
212
213
def __getattribute__(self, __name: str) -> Any:
214
try:
215
return super().__getattribute__(__name)
216
except AttributeError:
217
if __name in ATTRS and __name.startswith(("distance", "duration")):
218
_type, _mode = __name.split("_")
219
if _type == "distance":
220
return self._get_distance(_mode)
221
if _type == "duration":
222
return self._get_duration(_mode)
223
raise
224
225