Exemplo n.º 1
0
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()
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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"])])
Exemplo n.º 7
0
    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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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
Exemplo n.º 10
0
 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)
Exemplo n.º 11
0
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() == []
Exemplo n.º 12
0
    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"]
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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,
        )
    }
Exemplo n.º 16
0
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
    ]
Exemplo n.º 17
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
Exemplo n.º 18
0
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)
Exemplo n.º 19
0
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')))
Exemplo n.º 20
0
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)