Module check
Tesco delivery checker
- Tesco delivery checker
- Documentation
- Development
- Dependencies
- Running the script
- Tests
- Deployment
- Code style
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:
- Set up
dokkuon remote server. - 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
- Deploy with:
git push [remote_name] master
- 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
Trueis returned. Except for that the functionsend_email()is called so that the notification with desired information about free slots is sent. If there are no available slotsFalseis 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_NOTIFICATIONthat 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_NOTIFICATIONor""
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_KEYenvironment 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" ) TrueExpand 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