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