Skip to content

Commit c70295a

Browse files
authored
Merge pull request #545 from Der-Henning/dev
Update dependencies, update localization
2 parents ec579f3 + 20e6892 commit c70295a

13 files changed

+689
-575
lines changed

.devcontainer/devcontainer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"VARIANT": "3.11",
1313
// Options
1414
// "NODE_VERSION": "lts/*",
15-
"POETRY_VERSION": "1.8.2"
15+
"POETRY_VERSION": "1.8.3"
1616
}
1717
},
1818
// Set *default* container specific settings.json values on container create.

.pre-commit-config.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ repos:
1313
- id: check-added-large-files
1414

1515
- repo: https://github.com/ambv/black
16-
rev: 24.4.2
16+
rev: 24.8.0
1717
hooks:
1818
- id: black
1919
args: [--line-length, '130', --target-version, py38]
@@ -25,13 +25,13 @@ repos:
2525
args: [--profile, black]
2626

2727
- repo: https://github.com/PyCQA/flake8
28-
rev: 7.1.0
28+
rev: 7.1.1
2929
hooks:
3030
- id: flake8
3131
args: [--max-line-length, '130']
3232

3333
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
34-
rev: v2.13.0
34+
rev: v2.14.0
3535
hooks:
3636
- id: pretty-format-toml
3737
args: [--autofix]
@@ -42,13 +42,13 @@ repos:
4242
args: [--autofix]
4343

4444
- repo: https://github.com/igorshubovych/markdownlint-cli
45-
rev: v0.41.0
45+
rev: v0.42.0
4646
hooks:
4747
- id: markdownlint
4848
args: [--fix]
4949

5050
- repo: https://github.com/python-poetry/poetry
51-
rev: 1.8.2
51+
rev: 1.8.3
5252
hooks:
5353
- id: poetry-check
5454
- id: poetry-lock
@@ -57,7 +57,7 @@ repos:
5757
args: [-f, requirements.txt, -o, requirements.txt, --without-hashes]
5858

5959
- repo: https://github.com/pre-commit/mirrors-mypy
60-
rev: v1.10.1
60+
rev: v1.11.2
6161
hooks:
6262
- id: mypy
6363
additional_dependencies: [types-requests]

poetry.lock

+580-492
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ name = "tgtg-scanner"
1212
packages = [{include = "tgtg_scanner"}]
1313
readme = "README.md"
1414
repository = "https://github.com/Der-Henning/tgtg"
15-
version = "1.21.2"
15+
version = "1.22.0"
1616

1717
[tool.poetry.dependencies]
1818
apprise = "^1.4.0"
19+
babel = "^2.16.0"
1920
colorlog = "^6.7.0"
2021
cron-descriptor = "^1.4.0"
2122
discord = "^2.3.2"
2223
googlemaps = "^4.10.0"
2324
humanize = "^4.7.0"
2425
packaging = "^24.0"
2526
progress = "^1.6"
26-
prometheus-client = "^0.20.0"
27+
prometheus-client = "^0.21.0"
2728
pycron = "^3.0.0"
2829
python = ">=3.9,<3.13"
2930
python-pushsafer = "^1.1"

requirements.txt

+22-20
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
1-
aiohttp==3.9.5 ; python_version >= "3.9" and python_version < "3.13"
1+
aiohappyeyeballs==2.4.2 ; python_version >= "3.9" and python_version < "3.13"
2+
aiohttp==3.10.8 ; python_version >= "3.9" and python_version < "3.13"
23
aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "3.13"
3-
anyio==4.4.0 ; python_version >= "3.9" and python_version < "3.13"
4-
apprise==1.8.0 ; python_version >= "3.9" and python_version < "3.13"
4+
anyio==4.6.0 ; python_version >= "3.9" and python_version < "3.13"
5+
apprise==1.9.0 ; python_version >= "3.9" and python_version < "3.13"
56
async-timeout==4.0.3 ; python_version >= "3.9" and python_version < "3.11"
6-
attrs==23.2.0 ; python_version >= "3.9" and python_version < "3.13"
7-
cachetools==5.3.3 ; python_version >= "3.9" and python_version < "3.13"
8-
certifi==2024.7.4 ; python_version >= "3.9" and python_version < "3.13"
7+
attrs==24.2.0 ; python_version >= "3.9" and python_version < "3.13"
8+
babel==2.16.0 ; python_version >= "3.9" and python_version < "3.13"
9+
cachetools==5.5.0 ; python_version >= "3.9" and python_version < "3.13"
10+
certifi==2024.8.30 ; python_version >= "3.9" and python_version < "3.13"
911
charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "3.13"
1012
click==8.1.7 ; python_version >= "3.9" and python_version < "3.13"
1113
colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows")
1214
colorlog==6.8.2 ; python_version >= "3.9" and python_version < "3.13"
13-
cron-descriptor==1.4.3 ; python_version >= "3.9" and python_version < "3.13"
15+
cron-descriptor==1.4.5 ; python_version >= "3.9" and python_version < "3.13"
1416
discord-py==2.4.0 ; python_version >= "3.9" and python_version < "3.13"
1517
discord==2.3.2 ; python_version >= "3.9" and python_version < "3.13"
16-
exceptiongroup==1.2.1 ; python_version >= "3.9" and python_version < "3.11"
18+
exceptiongroup==1.2.2 ; python_version >= "3.9" and python_version < "3.11"
1719
frozenlist==1.4.1 ; python_version >= "3.9" and python_version < "3.13"
1820
googlemaps==4.10.0 ; python_version >= "3.9" and python_version < "3.13"
1921
h11==0.14.0 ; python_version >= "3.9" and python_version < "3.13"
2022
httpcore==1.0.5 ; python_version >= "3.9" and python_version < "3.13"
21-
httpx==0.27.0 ; python_version >= "3.9" and python_version < "3.13"
23+
httpx==0.27.2 ; python_version >= "3.9" and python_version < "3.13"
2224
humanize==4.10.0 ; python_version >= "3.9" and python_version < "3.13"
23-
idna==3.7 ; python_version >= "3.9" and python_version < "3.13"
24-
importlib-metadata==8.0.0 ; python_version >= "3.9" and python_version < "3.10"
25-
markdown==3.6 ; python_version >= "3.9" and python_version < "3.13"
26-
multidict==6.0.5 ; python_version >= "3.9" and python_version < "3.13"
25+
idna==3.10 ; python_version >= "3.9" and python_version < "3.13"
26+
importlib-metadata==8.5.0 ; python_version >= "3.9" and python_version < "3.10"
27+
markdown==3.7 ; python_version >= "3.9" and python_version < "3.13"
28+
multidict==6.1.0 ; python_version >= "3.9" and python_version < "3.13"
2729
oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "3.13"
2830
packaging==24.1 ; python_version >= "3.9" and python_version < "3.13"
2931
progress==1.6 ; python_version >= "3.9" and python_version < "3.13"
30-
prometheus-client==0.20.0 ; python_version >= "3.9" and python_version < "3.13"
31-
pycron==3.0.0 ; python_version >= "3.9" and python_version < "3.13"
32+
prometheus-client==0.21.0 ; python_version >= "3.9" and python_version < "3.13"
33+
pycron==3.1.1 ; python_version >= "3.9" and python_version < "3.13"
3234
python-pushsafer==1.1 ; python_version >= "3.9" and python_version < "3.13"
33-
python-telegram-bot[callback-data]==21.4 ; python_version >= "3.9" and python_version < "3.13"
34-
pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "3.13"
35+
python-telegram-bot[callback-data]==21.6 ; python_version >= "3.9" and python_version < "3.13"
36+
pyyaml==6.0.2 ; python_version >= "3.9" and python_version < "3.13"
3537
requests-oauthlib==2.0.0 ; python_version >= "3.9" and python_version < "3.13"
3638
requests==2.32.3 ; python_version >= "3.9" and python_version < "3.13"
3739
sniffio==1.3.1 ; python_version >= "3.9" and python_version < "3.13"
3840
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "3.11"
39-
urllib3==2.2.2 ; python_version >= "3.9" and python_version < "3.13"
40-
yarl==1.9.4 ; python_version >= "3.9" and python_version < "3.13"
41-
zipp==3.19.2 ; python_version >= "3.9" and python_version < "3.10"
41+
urllib3==2.2.3 ; python_version >= "3.9" and python_version < "3.13"
42+
yarl==1.13.1 ; python_version >= "3.9" and python_version < "3.13"
43+
zipp==3.20.2 ; python_version >= "3.9" and python_version < "3.10"

tests/test_item.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def test_item(tgtg_item: dict, monkeypatch: pytest.MonkeyPatch):
1717
assert item.item_category == tgtg_item.get("item", {}).get("item_category", "-")
1818
assert item.description == tgtg_item.get("item", {}).get("description", "-")
1919
assert item.link == "https://share.toogoodtogo.com/item/774625"
20-
assert item.price == "3.00"
21-
assert item.value == "9.00"
20+
assert item.price == "3.00"
21+
assert item.value == "9.00"
2222
assert item.currency == "EUR"
2323
assert item.store_name == tgtg_item.get("store", {}).get("store_name", "-")
2424
assert item.item_logo == tgtg_item.get("item", {}).get("logo_picture", {}).get("current_url", "-")

tests/test_notifiers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ def test_smtp(test_item: Item, reservations: Reservations, favorites: Favorites,
262262
assert body[2] == "From: [email protected]"
263263
assert body[3] == "To: [email protected]"
264264
assert body[4] == "Subject: New Magic Bags"
265-
assert body[7] == 'Content-Type: text/html; charset="utf-8"'
266-
assert body[11] == f"<b>=C3=81 =C3=AA</b> </br>Amount: {test_item.items_available}"
265+
assert body[8] == 'Content-Type: text/html; charset="utf-8"'
266+
assert body[12] == f"<b>=C3=81 =C3=AA</b> </br>Amount: {test_item.items_available}"
267267

268268

269269
@pytest.fixture

tgtg_scanner/models/item.py

+62-41
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from http import HTTPStatus
55
from typing import Any, Union
66

7+
import babel.numbers
78
import humanize
89
import requests
910

@@ -52,44 +53,64 @@ class Item:
5253
returns well formated data for notifications.
5354
"""
5455

55-
def __init__(self, data: dict, location: Union[Location, None] = None):
56-
self.items_available = data.get("items_available", 0)
57-
self.display_name = data.get("display_name", "-")
58-
self.favorite = "Yes" if data.get("favorite", False) else "No"
59-
self.pickup_interval_start = data.get("pickup_interval", {}).get("start", None)
60-
self.pickup_interval_end = data.get("pickup_interval", {}).get("end", None)
61-
self.pickup_location = data.get("pickup_location", {}).get("address", {}).get("address_line", "-")
62-
63-
item = data.get("item", {})
64-
self.item_id = item.get("item_id")
65-
self.rating = item.get("average_overall_rating", {}).get("average_overall_rating", None)
66-
self.rating = "-" if not self.rating else f"{self.rating:.1f}"
67-
self.packaging_option = item.get("packaging_option", "-")
68-
self.item_name = item.get("name", "-")
69-
self.buffet = "Yes" if item.get("buffet", False) else "No"
70-
self.item_category = item.get("item_category", "-")
71-
self.description = item.get("description", "-")
72-
item_price = item.get("item_price", {})
73-
item_value = item.get("item_value", {})
74-
price = item_price.get("minor_units", 0) / 10 ** item_price.get("decimals", 0)
75-
value = item_value.get("minor_units", 0) / 10 ** item_value.get("decimals", 0)
76-
self.price = f"{price:.2f}"
77-
self.value = f"{value:.2f}"
78-
self.currency = item_price.get("code", "-")
79-
self.item_logo = item.get("logo_picture", {}).get(
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(
8078
"current_url",
8179
"https://tgtg-mkt-cms-prod.s3.eu-west-1.amazonaws.com/13512/TGTG_Icon_White_Cirle_1988x1988px_RGB.png",
8280
)
83-
self.item_cover = item.get("cover_picture", {}).get(
81+
self.item_cover: str = item.get("cover_picture", {}).get(
8482
"current_url",
8583
"https://images.tgtg.ninja/standard_images/GENERAL/other1.jpg",
8684
)
8785

88-
store = data.get("store", {})
89-
self.store_name = store.get("store_name", "-")
86+
store: dict = data.get("store", {})
87+
self.store_name: str = store.get("store_name", "-")
9088

91-
self.scanned_on = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
89+
self.scanned_on: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
9290
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)
93114

94115
@staticmethod
95116
def _datetimeparse(datestr: str) -> datetime.datetime:
@@ -155,18 +176,18 @@ def pickupdate(self) -> str:
155176
"""
156177
Returns a well formated string, providing the pickup time range
157178
"""
158-
if self.pickup_interval_start and self.pickup_interval_end:
159-
now = datetime.datetime.now()
160-
pfr = self._datetimeparse(self.pickup_interval_start)
161-
pto = self._datetimeparse(self.pickup_interval_end)
162-
prange = f"{pfr.hour:02d}:{pfr.minute:02d} - {pto.hour:02d}:{pto.minute:02d}"
163-
tommorow = now + datetime.timedelta(days=1)
164-
if now.date() == pfr.date():
165-
return f"{humanize.naturalday(now)}, {prange}"
166-
if (pfr.date() - now.date()).days == 1:
167-
return f"{humanize.naturalday(tommorow)}, {prange}"
168-
return f"{pfr.day}/{pfr.month}, {prange}"
169-
return "-"
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+
tommorow = 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(tommorow)}, {prange}"
190+
return f"{pfr.day}/{pfr.month}, {prange}"
170191

171192
def _get_distance_time(self, travel_mode: str) -> Union[DistanceTime, None]:
172193
if self.location is None:

tgtg_scanner/models/metrics.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def update(self, item: Item) -> None:
4040
"""
4141
try:
4242
self.item_count.labels(item.item_id, item.display_name).set(item.items_available)
43-
self.item_price.labels(item.item_id, item.display_name).set(float(item.price))
44-
self.item_value.labels(item.item_id, item.display_name).set(float(item.value))
43+
self.item_price.labels(item.item_id, item.display_name).set(item._price)
44+
self.item_value.labels(item.item_id, item.display_name).set(item._value)
4545
except ValueError as err:
4646
log.warning("Error updating metrics: %s", err)

tgtg_scanner/notifiers/discord.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async def _listen_for_items(self):
6969
item = self.queue.get(block=False)
7070
if item is None:
7171
self.bot.dispatch("close")
72+
await self.bot.close()
7273
return
7374
log.debug("Sending %s Notification", self.name)
7475
await self._send(item)
@@ -89,7 +90,6 @@ def _run(self):
8990
# Commands are handled separately, in case commands are not enabled
9091
self._setup_commands()
9192
asyncio.run(self.bot.start(self.token))
92-
self.bot.http.connector.close()
9393

9494
def _setup_events(self):
9595
@self.bot.event

tgtg_scanner/notifiers/smtp.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import smtplib
44
from email.mime.multipart import MIMEMultipart
55
from email.mime.text import MIMEText
6+
from email.utils import formatdate
67
from smtplib import SMTPException, SMTPServerDisconnected
78
from typing import Union
89

@@ -93,7 +94,7 @@ def _stay_connected(self) -> None:
9394
if status != 250:
9495
self._connect()
9596

96-
def _send_mail(self, subject: str, html: str, item_id: int) -> None:
97+
def _send_mail(self, subject: str, html: str, item_id: str) -> None:
9798
"""Sends mail with html body"""
9899
if self.server is None:
99100
self._connect()
@@ -104,10 +105,11 @@ def _send_mail(self, subject: str, html: str, item_id: int) -> None:
104105

105106
# Contains either the main recipient(s) or recipient(s) that should be
106107
# notified for the specific item. First, initalize with main recipient(s)
107-
recipients = self.item_recipients.get(str(item_id), self.recipients)
108+
recipients = self.item_recipients.get(item_id, self.recipients)
108109

109110
message["To"] = ", ".join(recipients)
110111
message["Subject"] = subject
112+
message["Date"] = formatdate(localtime=True)
111113
message.attach(MIMEText(html, "html", "utf-8"))
112114
body = message.as_string()
113115
self._stay_connected()

tgtg_scanner/scanner.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def _get_test_item(self) -> Item:
7979
return items[0]
8080
items = sorted(
8181
[
82-
Item(item, self.location)
82+
Item(item, self.location, self.config.locale)
8383
for item in self.tgtg_client.get_items(favorites_only=False, latitude=53.5511, longitude=9.9937, radius=50)
8484
],
8585
key=lambda x: x.items_available,
@@ -100,7 +100,7 @@ def _job(self) -> None:
100100
try:
101101
if item_id != "":
102102
item_dict = self.tgtg_client.get_item(item_id)
103-
items.append(Item(item_dict, self.location))
103+
items.append(Item(item_dict, self.location, self.config.locale))
104104
except TgtgAPIError as err:
105105
log.error(err)
106106
items += self._get_favorites()
@@ -133,7 +133,7 @@ def _get_favorites(self) -> list[Item]:
133133
except TgtgAPIError as err:
134134
log.error(err)
135135
return []
136-
return [Item(item, self.location) for item in items]
136+
return [Item(item, self.location, self.config.locale) for item in items]
137137

138138
def _check_item(self, item: Item) -> None:
139139
"""

wiki/Configuration.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ You can combine multiple crons as semicolon separated list.
7171
| ScheduleCron | SCHEDULE_CRON | run only on schedule | `* * * * *` |
7272
| ItemIDs | ITEM_IDS | **Depreciated!** comma-separated list of additional (none favorite) items to scan | |
7373
| Metrics | METRICS | enable Prometheus metrics HTTP server | `false` |
74-
| MetricsPort | METRICS_PORTS | port for metrics server | `8000` |
74+
| MetricsPort | METRICS_PORT | port for metrics server | `8000` |
7575
| DisableTests | DISABLE_TESTS | disable test notifications on startup | `false` |
7676
| Quiet | QUIET | minimal console output | `false` |
7777
| Locale | LOCALE | localization | `en_US` |

0 commit comments

Comments
 (0)