Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE!] [New feature]: Incorporate Delivery/Livraison bags #549

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.sample.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ MetricsPort = 8000
## Disable Test Notifications
; DisableTests = true

## Disable notifications for Delivery Items from the delivery panel (new feature)
; DisableDeliveryItems = true

## Disable all console outputs. only displays errors or Console notifier messages
; Quiet = true

Expand Down
1,086 changes: 602 additions & 484 deletions poetry.lock

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
aiohappyeyeballs==2.4.2 ; python_version >= "3.9" and python_version < "3.13"
aiohttp==3.10.8 ; python_version >= "3.9" and python_version < "3.13"
aiohappyeyeballs==2.4.3 ; python_version >= "3.9" and python_version < "3.13"
aiohttp==3.10.10 ; python_version >= "3.9" and python_version < "3.13"
aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "3.13"
anyio==4.6.0 ; python_version >= "3.9" and python_version < "3.13"
anyio==4.6.2.post1 ; python_version >= "3.9" and python_version < "3.13"
apprise==1.9.0 ; python_version >= "3.9" and python_version < "3.13"
async-timeout==4.0.3 ; python_version >= "3.9" and python_version < "3.11"
attrs==24.2.0 ; python_version >= "3.9" and python_version < "3.13"
babel==2.16.0 ; python_version >= "3.9" and python_version < "3.13"
cachetools==5.5.0 ; python_version >= "3.9" and python_version < "3.13"
certifi==2024.8.30 ; python_version >= "3.9" and python_version < "3.13"
charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "3.13"
charset-normalizer==3.4.0 ; python_version >= "3.9" and python_version < "3.13"
click==8.1.7 ; python_version >= "3.9" and python_version < "3.13"
colorama==0.4.6 ; python_version >= "3.9" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows")
colorlog==6.8.2 ; python_version >= "3.9" and python_version < "3.13"
colorlog==6.9.0 ; python_version >= "3.9" and python_version < "3.13"
cron-descriptor==1.4.5 ; python_version >= "3.9" and python_version < "3.13"
discord-py==2.4.0 ; python_version >= "3.9" and python_version < "3.13"
discord==2.3.2 ; python_version >= "3.9" and python_version < "3.13"
exceptiongroup==1.2.2 ; python_version >= "3.9" and python_version < "3.11"
frozenlist==1.4.1 ; python_version >= "3.9" and python_version < "3.13"
frozenlist==1.5.0 ; python_version >= "3.9" and python_version < "3.13"
googlemaps==4.10.0 ; python_version >= "3.9" and python_version < "3.13"
h11==0.14.0 ; python_version >= "3.9" and python_version < "3.13"
httpcore==1.0.5 ; python_version >= "3.9" and python_version < "3.13"
httpcore==1.0.6 ; python_version >= "3.9" and python_version < "3.13"
httpx==0.27.2 ; python_version >= "3.9" and python_version < "3.13"
humanize==4.10.0 ; python_version >= "3.9" and python_version < "3.13"
humanize==4.11.0 ; python_version >= "3.9" and python_version < "3.13"
idna==3.10 ; python_version >= "3.9" and python_version < "3.13"
importlib-metadata==8.5.0 ; python_version >= "3.9" and python_version < "3.10"
markdown==3.7 ; python_version >= "3.9" and python_version < "3.13"
Expand All @@ -30,6 +30,7 @@ oauthlib==3.2.2 ; python_version >= "3.9" and python_version < "3.13"
packaging==24.1 ; python_version >= "3.9" and python_version < "3.13"
progress==1.6 ; python_version >= "3.9" and python_version < "3.13"
prometheus-client==0.21.0 ; python_version >= "3.9" and python_version < "3.13"
propcache==0.2.0 ; python_version >= "3.9" and python_version < "3.13"
pycron==3.1.1 ; python_version >= "3.9" and python_version < "3.13"
python-pushsafer==1.1 ; python_version >= "3.9" and python_version < "3.13"
python-telegram-bot[callback-data]==21.6 ; python_version >= "3.9" and python_version < "3.13"
Expand All @@ -39,5 +40,5 @@ requests==2.32.3 ; python_version >= "3.9" and python_version < "3.13"
sniffio==1.3.1 ; python_version >= "3.9" and python_version < "3.13"
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "3.11"
urllib3==2.2.3 ; python_version >= "3.9" and python_version < "3.13"
yarl==1.13.1 ; python_version >= "3.9" and python_version < "3.13"
yarl==1.17.1 ; python_version >= "3.9" and python_version < "3.13"
zipp==3.20.2 ; python_version >= "3.9" and python_version < "3.10"
3 changes: 3 additions & 0 deletions tgtg_scanner/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ class Config(BaseConfig):
metrics: bool = False
metrics_port: int = 8000
disable_tests: bool = False
disable_delivery_items: bool = True
quiet: bool = False
docker: bool = False
activity: bool = True
Expand Down Expand Up @@ -623,6 +624,7 @@ def _read_ini(self, parser: configparser.ConfigParser):
self._ini_get_boolean(parser, "MAIN", "Metrics", "metrics")
self._ini_get_int(parser, "MAIN", "MetricsPort", "metrics_port")
self._ini_get_boolean(parser, "MAIN", "DisableTests", "disable_tests")
self._ini_get_boolean(parser, "MAIN", "DisableDeliveryItems", "disable_delivery_items")
self._ini_get_boolean(parser, "MAIN", "Quiet", "quiet")
self._ini_get_boolean(parser, "MAIN", "Docker", "docker")
self._ini_get_boolean(parser, "MAIN", "Activity", "activity")
Expand All @@ -636,6 +638,7 @@ def _read_env(self):
self._env_get_boolean("METRICS", "metrics")
self._env_get_int("METRICS_PORT", "metrics_port")
self._env_get_boolean("DISABLE_TESTS", "disable_tests")
self._env_get_boolean("DISABLE_DELIVERY_ITEMS", "DisableDeliveryItems")
self._env_get_boolean("QUIET", "quiet")
self._env_get_boolean("DOCKER", "docker")
self._env_get_boolean("ACTIVITY", "activity")
Expand Down
16 changes: 16 additions & 0 deletions tgtg_scanner/models/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ def __init__(self, data: dict, location: Union[Location, None] = None, locale: s
store: dict = data.get("store", {})
self.store_name: str = store.get("store_name", "-")

self.manufacturer_properties: dict = data.get("manufacturer_properties", {})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New attributes for delivery items

self.tags: list = data.get("tags", [])

self.scanned_on: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.location = location
self.locale = locale
Expand Down Expand Up @@ -221,3 +224,16 @@ def __getattribute__(self, __name: str) -> Any:
if _type == "duration":
return self._get_duration(_mode)
raise

@classmethod
def delivery_item_conversion(cls):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed because delivery bags have not the same attributes in the API call than Items

"""
Returns a mapping of "DeliveryItem" keys to Item keys.
"""
return {
"subtitle": "description",
"item_type": "item_category",
"name": "display_name",
"available_stock": "items_available",
"cover_picture": "item_cover",
}
71 changes: 71 additions & 0 deletions tgtg_scanner/scanner.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import logging
import sys
from random import random
Expand Down Expand Up @@ -52,6 +53,7 @@ def __init__(self, config: Config):
self.item_ids = set(self.config.item_ids)
self.cron = self.config.schedule_cron
self.state: Dict[str, Item] = {}
self.delivery_state: Dict[str, Item] = {}
self.notifiers: Union[Notifiers, None] = None
self.location: Union[Location, None] = None
self.tgtg_client = TgtgClient(
Expand Down Expand Up @@ -104,6 +106,15 @@ def _job(self) -> None:
except TgtgAPIError as err:
log.error(err)
items += self._get_favorites()

# if state is empty (first scanning iteration), initialize it with the current favorite items
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SEE PR #548

# and set `items_available` property to 0.
# It allows to be able to receive notifications at start if some magic bags are already available.
if not self.state:
self.state = {item.item_id: copy.deepcopy(item) for item in items}
for item in self.state.values():
item.items_available = 0

for item in items:
self._check_item(item)

Expand All @@ -114,6 +125,9 @@ def _job(self) -> None:
if len(self.state) == 0:
log.warning("No items in observation! Did you add any favorites?")

if not self.config.disable_delivery_items:
self._check_delivery_items()

self.config.save_tokens(
self.tgtg_client.access_token,
self.tgtg_client.refresh_token,
Expand All @@ -135,6 +149,41 @@ def _get_favorites(self) -> list[Item]:
return []
return [Item(item, self.location, self.config.locale) for item in items]

def convert_raw_delivery_item(self, raw_delivery_item: dict, mapping: dict) -> Item:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversion of delivery data, not in the same format as item data. Adding some logic to have correct format to build the Item object

"""Converts a raw delivery item to an Item object using a mapping dictionary."""
item_data = raw_delivery_item.get("item", {})

# Create a new dictionary to hold the modified item data
modified_item_data = {}

# Update item data keys based on the mapping
for key, value in item_data.items():
new_key = mapping.get(key, key)
modified_item_data[new_key] = value

# Flattening the original data and integrating the modified item data
flattened_data = {**raw_delivery_item}
flattened_data.update(modified_item_data)

return Item(flattened_data, self.location, self.config.locale)

def get_delivery_items(self) -> List[Item]:
"""Returns delivery items available in the delivery panel.

Returns:
List: List of delivery items, still available in the delivery panel (not Out of stock)
"""
raw_delivery_items = self.tgtg_client.get_raw_delivery_items()

delivery_items = [
self.convert_raw_delivery_item(raw_delivery_item, Item.delivery_item_conversion())
for raw_delivery_item in raw_delivery_items
]

in_stock_delivery_items = [item for item in delivery_items if item.items_available > 0]

return in_stock_delivery_items

def _check_item(self, item: Item) -> None:
"""
Checks if the available item amount raised from zero to something
Expand All @@ -151,6 +200,28 @@ def _check_item(self, item: Item) -> None:
self.metrics.update(item)
self.state[item.item_id] = item

def _check_delivery_items(self) -> None:
"""
Check for new delivery items and send notifications if new items are available
"""
# 1. Retrieve the current delivery items data available on TGTG
delivery_items: list[Item] = self.get_delivery_items()
delivery_items_ids: list[str] = [item.item_id for item in delivery_items]

# 2. Compare the delivery items with the current state
for item in delivery_items:
item_id = item.item_id
if item_id not in self.delivery_state:
# New item, send notification
self._send_messages(item)
self.metrics.send_notifications.labels(item_id, item.display_name).inc()
self.delivery_state[item_id] = item

# 3. Remove items that are no longer available - out of stock
for item_id in list(self.delivery_state.keys()):
if item_id not in delivery_items_ids:
self.delivery_state.pop(item_id)

def _send_messages(self, item: Item) -> None:
"""
Send notifications for Item
Expand Down
44 changes: 44 additions & 0 deletions tgtg_scanner/tgtg/tgtg_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CREATE_ORDER_ENDPOINT = "order/v7/create/"
ABORT_ORDER_ENDPOINT = "order/v7/{}/abort"
ORDER_STATUS_ENDPOINT = "order/v7/{}/status"
MANUFACTURERITEM_ENDPOINT = "manufactureritem/v2/"
USER_AGENTS = [
"TGTG/{} Dalvik/2.1.0 (Linux; U; Android 9; Nexus 5 Build/M4B30Z)",
"TGTG/{} Dalvik/2.1.0 (Linux; U; Android 10; SM-G935F Build/NRD90M)",
Expand Down Expand Up @@ -403,3 +404,46 @@ def abort_order(self, order_id: str) -> None:
response = self._post(ABORT_ORDER_ENDPOINT.format(order_id), json={"cancel_reason_id": 1})
if response.json().get("state") != "SUCCESS":
raise TgtgAPIError(response.status_code, response.content)

def _extract_delivery_items(self, delivery_items_response_data: dict) -> List[dict]:
"""
Extracts all items from the delivery items response data.

Args:
delivery_items_response_data (dict): The response data from the TGTG API.

Returns:
List[dict]: List of all items in the response.
"""
all_delivery_items = []
for group in delivery_items_response_data.get("groups", []):
all_delivery_items.extend(group.get("elements", []))

return [item for item in all_delivery_items]

def get_raw_delivery_items(self):
"""Returns all raw items in delivery from the TGTG API

Returns:
List: List of raw items
"""
# Commented element types have not been tested yet.
response = self._post(
MANUFACTURERITEM_ENDPOINT,
json={
"action_types_accepted": ["QUERY"],
"display_types_accepted": ["LIST"],
"element_types_accepted": [
"ITEM", # All items/products in delivery
"HIGHLIGHTED_ITEM", # Item with a special highlight on the top of the delivery pannel
# "DUO_ITEMS",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not tested yet so still there but commented

# "DUO_ITEMS_V2",
# "TEXT",
# "PARCEL_TEXT",
# "NPS",
],
},
)

items_data = self._extract_delivery_items(response.json())
return items_data
1 change: 1 addition & 0 deletions wiki/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ You can combine multiple crons as semicolon separated list.
| Metrics | METRICS | enable Prometheus metrics HTTP server | `false` |
| MetricsPort | METRICS_PORT | port for metrics server | `8000` |
| DisableTests | DISABLE_TESTS | disable test notifications on startup | `false` |
| DisableDeliveryItems | DISABLE_DELIVERY_ITEMS | disable notifications for delivery items | `true` |
| Quiet | QUIET | minimal console output | `false` |
| Locale | LOCALE | localization | `en_US` |
| Activity | ACTIVITY | show running indicator (always disabled in docker) | `true` |
Expand Down