Module check

Tesco delivery checker

Build Status

Documentation

butterflybug.github.io/Tesco_delivery_checker

Development

Recommended Python version 3.8.0

Dependencies

$ pipenv install --dev

Running the script

The script run_checker.py requires proper environmental variables to be set up before its code is run. They are essential for that program so that it is able to successfully log into the website and obtain all needed information about available or unavailable slots.

Environment variables

Variable Description Default
$ TESCO_EMAIL Login to your Tesco account
$ TESCO_PASSWORD Password to your Tesco account
$ WAIT_TIME How often run_checker.py is performed in seconds 3600
$ SENDGRID_API_KEY API key to your SendGrid account
$ EMAIL_NOTIFICATION Email address which notification should be sent on

Tests

$ pytest

Update cassettes

To record a new cassette needed to run tests, that invalid one should be deleted. Once the file is removed, the newest version and updated content of the website can be recorded again.

$ pytest --record-mode=all

Deployment

The whole project's deployment is prepared to be supported with dokku. To correctly deploy the application to external server some steps need to be followed:

  1. Set up dokku on remote server.
  2. Add remote to your local repository:

git remote add [remote_name] dokku@[server_address]:[application_name]

i.e. git remote add dokku@example.com:tesco

  1. Deploy with:

git push [remote_name] master

  1. Make sure that environment variables are set:

dokku config:set [application_name] VARIABLE_NAME=VALUE

Code style

This project follows PEP8 style guide.

$ python -m flake8

$ black

Type annotation PEP484 with Mypy

$ mypy [file_path]

Expand source code
"""
.. include:: ../README.md
"""
import requests
from datetime import datetime, timezone, timedelta
import os
from bs4 import BeautifulSoup  # type: ignore
from sendgrid import SendGridAPIClient  # type: ignore
from sendgrid.helpers.mail import Mail  # type: ignore
from typing import List, Dict


def get_slots_for_date(url: str, session: requests.Session) -> List[Dict]:
    """
    Provides with a list of available slots so that booking of groceries' delivery
    is possible on a specified day.

    Parameters
    -----------
    url : string
        URL of the slot endpoint
    session : requests.Session
        Instance of Session class to preserve cookie between requests

    Returns
    --------
    slots : list
        List of available slots filtered from the list of all slots approachable on the endpoint

    Examples
    ---------
        >>> get_slots_for_date(
              "https://ezakupy.tesco.pl/groceries/pl-PL/slots/delivery/2020-05-13?slotGroup=2",
              session
            )
    """
    response = session.get(
        url,
        headers={
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Adrum": "isAjax:true",
            "X-Requested-With": "XMLHttpRequest",
        },
    )

    slots = list(
        filter(lambda item: item["status"] != "UnAvailable", response.json()["slots"])
    )

    return slots


def send_email(email: str, subject: str, message_body: str) -> bool:
    """
    Sends an email to address passed as first parameter.
    The message is delivered using SendGrid service. It requires API key determined
    under `SENDGRID_API_KEY` environment variable for successful email's delivery.

    Parameters
    ------------
    email : string
        Recepient's address email
    subject : string
        Subject of message
    message_body : string
        Content of message

    Returns
    -----------
    is_delivered : Boolean

    Examples
    ---------
    >>> send_email(
          "notification@notify.com",
          "Free slots available",
          "Free slots"
        )
    True
    """
    message = Mail(
        from_email="notifications@grocery.com",
        to_emails=email,
        subject=subject,
        plain_text_content=message_body,
    )
    sg = SendGridAPIClient(os.environ.get("SENDGRID_API_KEY"))
    sg.send(message)
    return True


def email_address() -> str:
    """
    Returns a value of the environment variable `EMAIL_NOTIFICATION`
    that indicates the email address which the notification should be sent to.
    However, if this variable is not set,  its value will be an empty string.

    Returns
    ------------
    string
        Value of `EMAIL_NOTIFICATION` or `""`
    """

    return os.environ.get("EMAIL_NOTIFICATION", "")


def check() -> bool:
    """
    Function examines whether there is at least one available slot on Tesco's website
    and in case of success the value `True` is returned. Except for that the function `send_email()`
    is called so that the notification with desired information about free slots is sent.
    If there are no available slots `False` is returned.

    Returns
    --------
    Boolean
        True if slots are available, otherwise False
    """
    today = datetime.now(timezone(timedelta(hours=2)))
    second_period = today + timedelta(days=7)
    third_period = second_period + timedelta(days=7)
    periods = list(
        map(
            lambda item: item.strftime("%Y-%m-%d"), [today, second_period, third_period]
        )
    )

    periods_urls = map(
        lambda item: f"https://ezakupy.tesco.pl/groceries/pl-PL/slots/delivery/{item}?slotGroup=2",
        periods,
    )
    url_login = "https://ezakupy.tesco.pl/groceries/pl-PL/login"

    session = requests.Session()

    response_login_form = session.get(url_login)
    soup = BeautifulSoup(response_login_form.content, features="html.parser")
    csrf_token = soup.find(attrs={"name": "_csrf"}).attrs["value"]

    session.post(
        url_login,
        data={
            "onSuccessUrl": "",
            "email": os.environ.get("TESCO_EMAIL", ""),
            "password": os.environ.get("TESCO_PASSWORD", ""),
            "_csrf": csrf_token,
        },
        headers={"Content-Type": "application/x-www-form-urlencoded"},
    )

    period_results = map(lambda url: get_slots_for_date(url, session), periods_urls)
    list_of_slots = []

    for period_result in period_results:
        for slot in period_result:
            list_of_slots.append(slot)

    if len(list_of_slots) > 0:
        send_email(
            email_address(), "Free slot available", f"Free slots {len(list_of_slots)}"
        )
        print("Free slot available. ", len(list_of_slots))
        return True
    else:
        print("No available slots")
        return False

Functions

def check() -> bool

Function examines whether there is at least one available slot on Tesco's website and in case of success the value True is returned. Except for that the function send_email() is called so that the notification with desired information about free slots is sent. If there are no available slots False is returned.

Returns

Boolean
True if slots are available, otherwise False
Expand source code
def check() -> bool:
    """
    Function examines whether there is at least one available slot on Tesco's website
    and in case of success the value `True` is returned. Except for that the function `send_email()`
    is called so that the notification with desired information about free slots is sent.
    If there are no available slots `False` is returned.

    Returns
    --------
    Boolean
        True if slots are available, otherwise False
    """
    today = datetime.now(timezone(timedelta(hours=2)))
    second_period = today + timedelta(days=7)
    third_period = second_period + timedelta(days=7)
    periods = list(
        map(
            lambda item: item.strftime("%Y-%m-%d"), [today, second_period, third_period]
        )
    )

    periods_urls = map(
        lambda item: f"https://ezakupy.tesco.pl/groceries/pl-PL/slots/delivery/{item}?slotGroup=2",
        periods,
    )
    url_login = "https://ezakupy.tesco.pl/groceries/pl-PL/login"

    session = requests.Session()

    response_login_form = session.get(url_login)
    soup = BeautifulSoup(response_login_form.content, features="html.parser")
    csrf_token = soup.find(attrs={"name": "_csrf"}).attrs["value"]

    session.post(
        url_login,
        data={
            "onSuccessUrl": "",
            "email": os.environ.get("TESCO_EMAIL", ""),
            "password": os.environ.get("TESCO_PASSWORD", ""),
            "_csrf": csrf_token,
        },
        headers={"Content-Type": "application/x-www-form-urlencoded"},
    )

    period_results = map(lambda url: get_slots_for_date(url, session), periods_urls)
    list_of_slots = []

    for period_result in period_results:
        for slot in period_result:
            list_of_slots.append(slot)

    if len(list_of_slots) > 0:
        send_email(
            email_address(), "Free slot available", f"Free slots {len(list_of_slots)}"
        )
        print("Free slot available. ", len(list_of_slots))
        return True
    else:
        print("No available slots")
        return False
def email_address() -> str

Returns a value of the environment variable EMAIL_NOTIFICATION that indicates the email address which the notification should be sent to. However, if this variable is not set, its value will be an empty string.

Returns

string
Value of EMAIL_NOTIFICATION or ""
Expand source code
def email_address() -> str:
    """
    Returns a value of the environment variable `EMAIL_NOTIFICATION`
    that indicates the email address which the notification should be sent to.
    However, if this variable is not set,  its value will be an empty string.

    Returns
    ------------
    string
        Value of `EMAIL_NOTIFICATION` or `""`
    """

    return os.environ.get("EMAIL_NOTIFICATION", "")
def get_slots_for_date(url: str, session: requests.sessions.Session) -> List[Dict]

Provides with a list of available slots so that booking of groceries' delivery is possible on a specified day.

Parameters

url : string
URL of the slot endpoint
session : requests.Session
Instance of Session class to preserve cookie between requests

Returns

slots : list
List of available slots filtered from the list of all slots approachable on the endpoint

Examples

>>> get_slots_for_date(
      "https://ezakupy.tesco.pl/groceries/pl-PL/slots/delivery/2020-05-13?slotGroup=2",
      session
    )
Expand source code
def get_slots_for_date(url: str, session: requests.Session) -> List[Dict]:
    """
    Provides with a list of available slots so that booking of groceries' delivery
    is possible on a specified day.

    Parameters
    -----------
    url : string
        URL of the slot endpoint
    session : requests.Session
        Instance of Session class to preserve cookie between requests

    Returns
    --------
    slots : list
        List of available slots filtered from the list of all slots approachable on the endpoint

    Examples
    ---------
        >>> get_slots_for_date(
              "https://ezakupy.tesco.pl/groceries/pl-PL/slots/delivery/2020-05-13?slotGroup=2",
              session
            )
    """
    response = session.get(
        url,
        headers={
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Adrum": "isAjax:true",
            "X-Requested-With": "XMLHttpRequest",
        },
    )

    slots = list(
        filter(lambda item: item["status"] != "UnAvailable", response.json()["slots"])
    )

    return slots
def send_email(email: str, subject: str, message_body: str) -> bool

Sends an email to address passed as first parameter. The message is delivered using SendGrid service. It requires API key determined under SENDGRID_API_KEY environment variable for successful email's delivery.

Parameters

email : string
Recepient's address email
subject : string
Subject of message
message_body : string
Content of message

Returns

is_delivered : Boolean
 

Examples

>>> send_email(
      "notification@notify.com",
      "Free slots available",
      "Free slots"
    )
True
Expand source code
def send_email(email: str, subject: str, message_body: str) -> bool:
    """
    Sends an email to address passed as first parameter.
    The message is delivered using SendGrid service. It requires API key determined
    under `SENDGRID_API_KEY` environment variable for successful email's delivery.

    Parameters
    ------------
    email : string
        Recepient's address email
    subject : string
        Subject of message
    message_body : string
        Content of message

    Returns
    -----------
    is_delivered : Boolean

    Examples
    ---------
    >>> send_email(
          "notification@notify.com",
          "Free slots available",
          "Free slots"
        )
    True
    """
    message = Mail(
        from_email="notifications@grocery.com",
        to_emails=email,
        subject=subject,
        plain_text_content=message_body,
    )
    sg = SendGridAPIClient(os.environ.get("SENDGRID_API_KEY"))
    sg.send(message)
    return True