def test_get_items_fail(login_response): responses.add(responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={}, status=400) client = TgtgClient(email="*****@*****.**", password="******") with pytest.raises(TgtgAPIError): client.get_items()
def test_get_items_fail(refresh_tokens_response): responses.add(responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={}, status=400) client = TgtgClient(**tgtg_client_fake_tokens) with pytest.raises(TgtgAPIError): client.get_items()
def test_get_items_fail(): responses.add(responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={}, status=400) client = TgtgClient(access_token="an_access_token", user_id=1234) with pytest.raises(TgtgAPIError): client.get_items()
def test_get_items_fail(): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"status": 401, "error": "Unauthorized"}, status=200, ) client = TgtgClient(access_token="an_access_token", user_id=1234) with pytest.raises(TgtgAPIError): client.get_items()
def test_get_items_custom_user_agent(): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) custom_user_agent = "test" client = TgtgClient(access_token="an_access_token", user_id=1234, user_agent=custom_user_agent) client.get_items() assert len(responses.calls) == 1 assert responses.calls[0].request.headers[ "user-agent"] == custom_user_agent
def setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the sensor platform.""" username = config[CONF_USERNAME] item = config[CONF_ITEM] access_token = config[CONF_ACCESS_TOKEN] refresh_token = config[CONF_REFRESH_TOKEN] user_id = config[CONF_USER_ID] global tgtg_client # Log in with tokens tgtg_client = TgtgClient( access_token=access_token, refresh_token=refresh_token, user_id=user_id ) # If item: isn't defined, use favorites - otherwise use defined items if item != [""]: for each_item_id in item: add_entities([TGTGSensor(each_item_id)]) else: tgtgReply = tgtg_client.get_items() for item in tgtgReply: add_entities([TGTGSensor(item["item"]["item_id"])])
def test_get_items(self): username = environ.get("TGTG_USERNAME", None) timeout = environ.get("TGTG_TIMEOUT", 60) access_token = environ.get("TGTG_ACCESS_TOKEN", None) refresh_token = environ.get("TGTG_REFRESH_TOKEN", None) user_id = environ.get("TGTG_USER_ID", None) client = TgtgClient( email=username, timeout=timeout, access_token=access_token, refresh_token=refresh_token, user_id=user_id, ) # Tests items = client.get_items(favorites_only=True) assert len(items) > 0 item = items[0] item_id = item["item"]["item_id"] for prop in GLOBAL_PROPERTIES: assert prop in item for prop in ITEM_PROPERTIES: assert prop in item["item"] for prop in PRICE_PROPERTIES: assert prop in item["item"]["price_including_taxes"] client.set_favorite(item_id, False) client.set_favorite(item_id, True) item = client.get_item(item_id) assert item["item"]["item_id"] == item_id
def test_get_items_custom_user_agent(refresh_tokens_response): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) custom_user_agent = "test" client = TgtgClient(**tgtg_client_fake_tokens, user_agent=custom_user_agent) client.get_items() assert (len([ call for call in responses.calls if API_ITEM_ENDPOINT in call.request.url ]) == 1) for call in responses.calls: assert call.request.headers["user-agent"] == custom_user_agent
def test_get_items_custom_user_agent(login_response): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) custom_user_agent = "test" client = TgtgClient(email="*****@*****.**", password="******", user_agent=custom_user_agent) client.get_items() assert (len([ call for call in responses.calls if API_ITEM_ENDPOINT in call.request.url ]) == 1) assert responses.calls[0].request.headers[ "user-agent"] == custom_user_agent
def test_get_items(self): client = TgtgClient(email=os.environ["TGTG_EMAIL"], password=os.environ["TGTG_PASSWORD"]) data = client.get_items(favorites_only=False, radius=10, latitude=48.126, longitude=-1.723) assert len(data) == 20 assert all(prop in data[0] for prop in GLOBAL_PROPERTIES)
def test_get_items_success(): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) client = TgtgClient(access_token="an_access_token", user_id=1234) assert client.get_items() == []
def test_get_items(self): passkey = environ.get('REPO_ACCESS_TOKEN') username = environ.get("TGTG_USERNAME", None) env_file = environ.get('GITHUB_ENV', None) timeout = environ.get('TGTG_TIMEOUT', 60) if passkey: encrypted_access_token = environ.get("TGTG_ACCESS_TOKEN", None) encrypted_refresh_token = environ.get("TGTG_REFRESH_TOKEN", None) encrypted_user_id = environ.get("TGTG_USER_ID", None) access_token = cryptocode.decrypt( encrypted_access_token, passkey) if encrypted_access_token else None refresh_token = cryptocode.decrypt( encrypted_refresh_token, passkey) if encrypted_refresh_token else None user_id = cryptocode.decrypt( encrypted_user_id, passkey) if encrypted_user_id else None else: access_token = environ.get("TGTG_ACCESS_TOKEN", None) refresh_token = environ.get("TGTG_REFRESH_TOKEN", None) user_id = environ.get("TGTG_USER_ID", None) client = TgtgClient( email=username, timeout=timeout, access_token=access_token, refresh_token=refresh_token, user_id=user_id, ) # get credentials and safe tokens to GITHUB_ENV file # this enables github workflow to reuse the access_token on sheduled runs # the credentials are encrypted with the REPO_ACCESS_TOKEN credentials = client.get_credentials() if env_file: with open(env_file, "a") as file: file.write("TGTG_ACCESS_TOKEN={}\n".format( cryptocode.encrypt(credentials["access_token"], passkey))) file.write("TGTG_REFRESH_TOKEN={}\n".format( cryptocode.encrypt(credentials["refresh_token"], passkey))) file.write("TGTG_USER_ID={}\n".format( cryptocode.encrypt(credentials["user_id"], passkey))) # Tests data = client.get_items(favorites_only=True) assert len(data) > 0 for prop in GLOBAL_PROPERTIES: assert prop in data[0] for prop in ITEM_PROPERTIES: assert prop in data[0]["item"] for prop in PRICE_PROPERTIES: assert prop in data[0]["item"]["price_including_taxes"]
def test_get_items_success(login_response): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) client = TgtgClient(email="*****@*****.**", password="******") assert client.get_items() == [] assert (len([ call for call in responses.calls if API_ITEM_ENDPOINT in call.request.url ]) == 1)
def test_get_items_success(refresh_tokens_response): responses.add( responses.POST, urljoin(BASE_URL, API_ITEM_ENDPOINT), json={"items": []}, status=200, ) client = TgtgClient(**tgtg_client_fake_tokens) assert client.get_items() == [] assert (len([ call for call in responses.calls if API_ITEM_ENDPOINT in call.request.url ]) == 1)
def get_items(client: TgtgClient): return { Item( pk=item["item"]["item_id"] name=item["item"]["name"] description=item["item"]["description"] price=Decimal(item["item"]["price"]["minor_units"]/100) store=Store( pk=item["store"]["store_id"] name=item["store"]["store_name"] ) ) for item in client.get_items( favorites_only=False, latitude=LAT, longitude=LONG, radius=10, ) }
def get_items(client: TgtgClient): return [ Item( pk=item["item"]["item_id"], name=item["item"]["name"], count=item["items_available"], display_name=item["display_name"], description=item["item"]["description"], price=Decimal(item["item"]["price"]["minor_units"] / 100), store=Store(pk=item["store"]["store_id"], name=item["store"]["store_name"]), ) for item in client.get_items( favorites_only=False, latitude=LAT, longitude=LONG, radius=RADIUS, ) # if item["items_available"] > 0 ]
class Scanner(): def __init__(self, notifiers=True): self.config = Config(config_file) if path.isfile( config_file) else Config() if self.config.debug: # pylint: disable=E1103 loggers = [ logging.getLogger(name) for name in logging.root.manager.loggerDict ] # pylint: enable=E1103 for logger in loggers: logger.setLevel(logging.DEBUG) log.info("Debugging mode enabled") self.metrics = Metrics() if self.config.metrics: self.metrics.enable_metrics() self.item_ids = self.config.item_ids self.amounts = {} try: self.tgtg_client = TgtgClient( email=self.config.tgtg["username"], timeout=self.config.tgtg["timeout"], access_token_lifetime=self.config. tgtg["access_token_lifetime"], max_polling_tries=self.config.tgtg["max_polling_tries"], polling_wait_time=self.config.tgtg["polling_wait_time"], access_token=self.config.tgtg["access_token"], refresh_token=self.config.tgtg["refresh_token"], user_id=self.config.tgtg["user_id"]) self.tgtg_client.login() except TgtgAPIError as err: raise except Error as err: log.error(err) raise TGTGConfigurationError() from err if notifiers: self.notifiers = Notifiers(self.config) def _job(self): for item_id in self.item_ids: try: if item_id != "": data = self.tgtg_client.get_item(item_id) self._check_item(Item(data)) except Exception: log.error("itemID %s Error! - %s", item_id, sys.exc_info()) for data in self._get_favorites(): try: self._check_item(Item(data)) except Exception: log.error("check item error! - %s", sys.exc_info()) log.debug("new State: %s", self.amounts) self.config.save_tokens(self.tgtg_client.access_token, self.tgtg_client.refresh_token, self.tgtg_client.user_id) def _get_favorites(self): items = [] page = 1 page_size = 100 error_count = 0 while True and error_count < 5: try: new_items = self.tgtg_client.get_items(favorites_only=True, page_size=page_size, page=page) items += new_items if len(new_items) < page_size: break page += 1 except Exception: log.error("get item error! - %s", sys.exc_info()) error_count += 1 self.metrics.get_favorites_errors.inc() return items def _check_item(self, item: Item): try: if self.amounts[ item.item_id] == 0 and item.items_available > self.amounts[ item.item_id]: self._send_messages(item) self.metrics.send_notifications.labels( item.item_id, item.display_name).inc() self.metrics.item_count.labels( item.item_id, item.display_name).set(item.items_available) except Exception: self.amounts[item.item_id] = item.items_available finally: if self.amounts[item.item_id] != item.items_available: log.info("%s - new amount: %s", item.display_name, item.items_available) self.amounts[item.item_id] = item.items_available def _send_messages(self, item: Item): log.info("Sending notifications for %s - %s bags available", item.display_name, item.items_available) self.notifiers.send(item) def run(self): log.info("Scanner started ...") while True: try: self._job() except Exception: log.error("Job Error! - %s", sys.exc_info()) finally: sleep(self.config.sleep_time * (0.9 + 0.2 * random())) def __del__(self): try: if self.notifiers.telegram.updater: self.notifiers.telegram.updater.stop() except: pass
class Scanner(): def __init__(self, notifiers: bool = True): self.config = Config(config_file) if path.isfile( config_file) else Config() if self.config.debug: # pylint: disable=E1103 loggers = [ logging.getLogger(name) for name in logging.root.manager.loggerDict ] # pylint: enable=E1103 for logger in loggers: logger.setLevel(logging.DEBUG) log.info("Debugging mode enabled") self.metrics = Metrics() self.item_ids = self.config.item_ids self.amounts = {} try: self.tgtg_client = TgtgClient( email=self.config.tgtg["username"], timeout=self.config.tgtg["timeout"], access_token_lifetime=self.config. tgtg["access_token_lifetime"], max_polling_tries=self.config.tgtg["max_polling_tries"], polling_wait_time=self.config.tgtg["polling_wait_time"], access_token=self.config.tgtg["access_token"], refresh_token=self.config.tgtg["refresh_token"], user_id=self.config.tgtg["user_id"]) self.tgtg_client.login() self.config.save_tokens(self.tgtg_client.access_token, self.tgtg_client.refresh_token, self.tgtg_client.user_id) except TgtgAPIError as err: raise err except Error as err: log.error(err) raise TGTGConfigurationError() from err if notifiers: if self.config.metrics: self.metrics.enable_metrics() self.notifiers = Notifiers(self.config) if not self.config.disable_tests: log.info("Sending test Notifications ...") self.notifiers.send(self._test_item) @property def _test_item(self) -> Item: """ Returns an item for test notifications """ items = sorted(self._get_favorites(), key=lambda x: x.items_available, reverse=True) if items: return items[0] items = sorted([ Item(item) for item in self.tgtg_client.get_items(favorites_only=False, latitude=53.5511, longitude=9.9937, radius=50) ], key=lambda x: x.items_available, reverse=True) return items[0] def _job(self) -> None: """ Job iterates over all monitored items """ for item_id in self.item_ids: try: if item_id != "": item = Item(self.tgtg_client.get_item(item_id)) self._check_item(item) except Exception: log.error("itemID %s Error! - %s", item_id, sys.exc_info()) for item in self._get_favorites(): try: self._check_item(item) except Exception: log.error("check item error! - %s", sys.exc_info()) log.debug("new State: %s", self.amounts) if len(self.amounts) == 0: log.warning("No items in observation! Did you add any favorites?") self.config.save_tokens(self.tgtg_client.access_token, self.tgtg_client.refresh_token, self.tgtg_client.user_id) def _get_favorites(self) -> list[Item]: """ Get favorites as list of Items """ items = [] page = 1 page_size = 100 error_count = 0 while error_count < 5: try: new_items = self.tgtg_client.get_items(favorites_only=True, page_size=page_size, page=page) items += new_items if len(new_items) < page_size: break page += 1 except Exception: log.error("get item error! - %s", sys.exc_info()) error_count += 1 self.metrics.get_favorites_errors.inc() return [Item(item) for item in items] def _check_item(self, item: Item) -> None: """ Checks if the available item amount raised from zero to something and triggers notifications. """ try: if self.amounts[ item.item_id] == 0 and item.items_available > self.amounts[ item.item_id]: self._send_messages(item) self.metrics.send_notifications.labels( item.item_id, item.display_name).inc() self.metrics.item_count.labels( item.item_id, item.display_name).set(item.items_available) except Exception: self.amounts[item.item_id] = item.items_available finally: if self.amounts[item.item_id] != item.items_available: log.info("%s - new amount: %s", item.display_name, item.items_available) self.amounts[item.item_id] = item.items_available def _send_messages(self, item: Item) -> None: """ Send notifications for Item """ log.info("Sending notifications for %s - %s bags available", item.display_name, item.items_available) self.notifiers.send(item) def run(self) -> NoReturn: """ Main Loop of the Scanner """ log.info("Scanner started ...") while True: try: self._job() if self.tgtg_client.captcha_error_count > 10: log.warning("Too many 403 Errors. Sleeping for 1 hour.") sleep(60 * 60) log.info("Continuing scanning.") self.tgtg_client.captcha_error_count = 0 except Exception: log.error("Job Error! - %s", sys.exc_info()) finally: sleep(self.config.sleep_time * (0.9 + 0.2 * random())) def __del__(self) -> None: """ Cleanup on shutdown """ try: if hasattr(self, 'notifiers') and self.notifiers.telegram.updater: self.notifiers.telegram.updater.stop() except Exception as exc: log.warning(exc)
def watch_tgtg(): if tgtg_email is not None and tgtg_password is not None: tgtg_client = TgtgClient(email=tgtg_email, password=tgtg_password) elif tgtg_user_id is not None and tgtg_access_token is not None: tgtg_client = TgtgClient(user_id=tgtg_user_id, access_token=tgtg_access_token) else: print( "Neither email and password nor user id and access token for TGTG were specified. Aborting..." ) pb_client = None if pb_api_key is not None: pb_client = Pushbullet(pb_api_key) pb_notification_channel = pb_client.get_channel( pb_notification_channel_tag ) if pb_notification_channel_tag is not None else None if bool(environ.get('PB_CLEAR_CHANNEL', False)) and pb_notification_channel is not None: for push in pb_client.get_pushes(): if 'channel_iden' in push and push[ 'channel_iden'] == pb_notification_channel.iden: pb_client.delete_push(push['iden']) available_items = {} while True: for available_item in available_items.values(): available_item['still_available'] = False items = tgtg_client.get_items(favorites_only=True, latitude=tgtg_search_lat, longitude=tgtg_search_lon, radius=tgtg_search_range) print( f"Found {len(items)} favourited stores of which {len([_ for _ in items if _['items_available'] > 0])} have available items..." ) for item in items: if item['items_available'] > 0: if item['item']['item_id'] in available_items: available_items[item['item'] ['item_id']]['still_available'] = True else: print( f"Found newly available product: {item['display_name']} since {datetime.now().strftime('%H:%M:%S (%d.%m.%Y)')}" ) if pb_client is not None: push_guid = uuid4().hex pb_client.push_link( f"New TGTG product available", f"https://share.toogoodtogo.com/item/{item['item']['item_id']}", f"{item['display_name']} since {datetime.now().strftime('%H:%M:%S (%d.%m.%Y)')}", channel=pb_notification_channel, guid=push_guid) available_items[item['item']['item_id']] = { 'item': item, 'still_available': True, 'push_guid': push_guid } keys_to_delete = [] for available_item_id, available_item in available_items.items(): if not available_item['still_available']: print( f"Product is no longer available: {available_item['item']['display_name']} since {datetime.now().strftime('%H:%M:%S (%d.%m.%Y)')}" ) if pb_client is not None: push_to_delete = next( (push for push in pb_client.get_pushes() if 'guid' in push and push['guid'] == available_item['push_guid']), None) if push_to_delete is not None: pb_client.delete_push(push_to_delete['iden']) keys_to_delete.append(available_item_id) for key_to_delete in keys_to_delete: del available_items[key_to_delete] print( f"All favourited stores were processed. Sleeping {environ.get('SLEEP_INTERVAL', '60')} seconds..." ) time.sleep(int(environ.get('SLEEP_INTERVAL', '60')))
def main(): """Main entry point of the app""" parser = argparse.ArgumentParser() parser.add_argument("-e", "--email", default=os.environ.get("TGTG_EMAIL")) parser.add_argument("-cid", "--chat-id", default=os.environ.get("CHAT_ID")) parser.add_argument("-cto", "--chat-token", default=os.environ.get("CHAT_TOKEN")) parser.add_argument("-U", "--url", default=os.environ.get("URL")) # Optional verbosity counter (eg. -v, -vv, -vvv, etc.) parser.add_argument( "-v", "--verbose", action="count", default=0, help="Verbosity (-v, -vv, etc)" ) # Specify output of "--version" parser.add_argument( "--version", action="version", version="%(prog)s (version {version})".format(version=VERSION), ) args = parser.parse_args() level = getLoggingLevel(args.verbose) logging.basicConfig(level=level) logging.debug("Logging level set to DEBUG") bot = telegram.Bot(args.chat_token) logging.debug("Reading credentials from JSON file") try: with open("credentials.json") as f: credentials = json.load(f) except FileNotFoundError: credentials = None logging.debug("No token file exists") if not credentials: bot.send_message( chat_id=args.chat_id, text="Please open email to authenticate", parse_mode="Markdown", ) client = TgtgClient(email=args.email) credentials = client.get_credentials() with open("credentials.json", "w") as f: json.dump(credentials, f) kwargs = credentials if args.url: kwargs["url"] = args.url client = TgtgClient( **kwargs, ) previous_stock = set() while True: messages = [] current_stock = client.get_items(page_size=400) for store in current_stock: item_id = store["item"]["item_id"] if store["items_available"] < 1: # drop from list if no more items if item_id in previous_stock: logging.debug(f"removed item {item_id} from stock list") previous_stock.remove(item_id) continue if item_id in previous_stock: logging.debug( f"item {item_id} already in stock list, user already notified" ) continue message = f"*{store['display_name']}*\n→ {store['items_available']} item(s) available" messages.append(message) logging.info(message) logging.debug(f"(re)adding {item_id} to stock list") previous_stock.add(item_id) if messages: bot.send_message( chat_id=args.chat_id, text="\n\n".join(messages), parse_mode="Markdown" ) time.sleep(60)