Created script.

This commit is contained in:
Vivek Santayana 2023-12-01 15:36:40 +00:00
parent f3a461296c
commit c3887eace9
7 changed files with 288 additions and 1 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
secret.py
data.json
output.json
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/

122
README.md
View File

@ -1,2 +1,122 @@
# secret-santa
# README
This is a Python script for running a secret Santa draw.
This script:
- Ingests participant data in JSON format,
- Automates the randomised pairing of people to buy gifts for,
- Includes information about preferences or restrictions for gifts that participants may have,
- Automatically emails participants via GMail SMTP (requires external set-up), and
- Creates an output file of gift pairings for troubleshooting later.
The script only uses standard libraries that come bundled with Python distributions, and does not require installing any dependencies.
## Set Up
### GMail SMTP
This app uses GMail as an SMTP provider.
- This is because GMail accounts are free and easy to create,
- The server is trusted by spam filters and is unlikely to be flagged as junk, and
- The app can authenticate without using any external libraries.
This means that there will be a few steps of external set up.
- [ ] Create `secret.py` from a copy of `secret.py.bak`.
- [ ] Log in to (or create) a GMail account (a standard, free account should suffice).
- [ ] Enable two-factor authentication on the account.
- [ ] Under the two-factor authentication section, generate an `App Password`.
- [ ] Enter your GMail username and the generated app password in `secret.py`.
`secret.py`
```py
USERNAME = '<username>@gmail.com'
PASSWORD = '<app password>'
```
You can get more information on [how to do this in the GMail documentation](https://support.google.com/mail/answer/185833?hl=en-GB).
#### Using a Different SMTP Server
You can set up a different SMTP provider by editing the server information in `config.py` and providing the appropriate username and password in `secret.py`.
### Data
The script ingests data as a JSON file.
Create a file `data.json` (matching the file path in `config.py`) in the following format:
`data.json`
```json
{
"name0": {
"email": "string",
"preference": "string"
},
"name1": {
"email": "string",
"preference": "string"
}
}
```
> [!NOTE]
> **Punctuation**: Comma after every entry, except the last entry.
JSON syntax is very fragile and can break if not entered correctly.
> [!TIP]
> **Email Addresses**: If you have provided an `EMAIL_DOMAIN` value in `config.py`, then you do not need to add the full email address, and can just provide the username before the `@`.
If you do provide a full email address for a participant in the data file, it will override the domain name given.
### Configuration
You can configure the app to send the draw for the secret Santa, including the greeting and the sign-off used, via the values in `config.py`.
You can also edit the files in the `./templates` directory to customise the email message to include any additional instructions or text as needed.
The most important configurations, however, are the credentials in `secret.py` and the data in `data.json`
## Running the script
Because this script should work using standard libraries in Python 3, you should be able to execute the script using whatever Python 3 interpreter you have.
`PowerShell`
```powershell
> python app.py
```
`Bash`
```bash
$ python3 app.py
```
## The Algorithm
The algorithm I used for the secret santa draw is a very elegant one, as [illustrated by Prof Hannah Fry](https://www.youtube.com/watch?v=GhnCj7Fvqt0).
- Randomise the order of participants.
- Generate a derangement from the randomised order by shifting the top name to the bottom of the list.
- Match participants between the randomised list and derangement to pair who is buying for whom.
This ensures that:
- People know whom they are buying for, but do not know who else is buying for whom, and
- Everybody has an equal chance of buying for everybody else.
- The person running the draw also does not know who is buying for them (unless they peek behind the curtain, as it were).
## Troubleshooting
If you do need to see who is buying for whom, you can check the `output.json` file, or check the sent emails from the SMTP server.
## Isn't This an Over-Engineered Solution for a Trivial Problem?
Of course it is.
## Why Not Use an Existing Site for This?
Because where is the fun in that?

79
app.py Normal file
View File

@ -0,0 +1,79 @@
import json, os, random, smtplib, ssl
from email.message import EmailMessage
from string import Template
from config import Config as Config
from secret import USERNAME, PASSWORD
context = ssl.create_default_context()
# Load data file
data_path = os.path.normpath(Config.DATA)
with open(data_path, 'r') as data_file:
data = json.loads(data_file.read())
print('Parsed data file.')
if not data: raise Exception('Error: data file is empty.')
# Randomise participants.
participants = list(data.keys())
random.shuffle(participants)
# Create a derangement in the permutation by moving the first participant to the bottom
derangement = list(participants)
derangement.append(derangement.pop(0))
# Create a dictionary pairing participants and gift recipients
buying = {}
for index, participant in enumerate(participants):
buying[participant] = derangement[index]
buying = {key: value for key, value in sorted(buying.items())}
# Create output file
output_path = os.path.normpath(Config.OUTPUT)
with open(output_path, 'w') as output:
output.write(json.dumps(buying, indent=4))
# Parse Templates
plain_path = os.path.normpath('./templates/plain.txt')
with open(plain_path, 'r') as plain_file:
plain_template = Template(plain_file.read())
rich_path = os.path.normpath('./templates/rich.html')
with open(rich_path, 'r') as rich_file:
rich_template = Template(rich_file.read())
def construct_message(participant, gift_recipient):
message = EmailMessage()
message['From'] = USERNAME
if '@' in data[participant]['email']:
participant_email = data[participant]['email']
elif hasattr(Config,'EMAIL_DOMAIN') and Config.EMAIL_DOMAIN:
participant_email = f'{data[participant]["email"]}@{Config.EMAIL_DOMAIN}'
else:
raise Exception(f'Error: Invalid email address for participant {participant}.')
if hasattr(Config,'OVERRIDE_RECIPIENT') and Config.OVERRIDE_RECIPIENT and hasattr(Config,'RECIPIENT_EMAIL') and Config.RECIPIENT_EMAIL:
message['To'] = Config.RECIPIENT_EMAIL
else: message['To'] = participant_email
message['Subject'] = Config.SUBJECT
preference = 'None' if not data[gift_recipient].get('preference') else data[gift_recipient].get('preference')
template_substitutions = {
'subject': Config.SUBJECT,
'greeting': Config.GREETING,
'participant': participant.title(),
'gift_recipient': gift_recipient.title(),
'preference': preference,
'sender': Config.SENDER
}
body_html = rich_template.substitute(template_substitutions)
body_plain = plain_template.substitute(template_substitutions)
message.set_content(body_plain)
message.add_alternative(body_html, subtype='html')
return message
messages = [construct_message(participant=participant, gift_recipient=gift_recipient) for participant, gift_recipient in buying.items()]
with smtplib.SMTP_SSL(Config.SERVER, Config.PORT, context=context) as smtp:
smtp.login(user=USERNAME, password=PASSWORD)
for message in messages: smtp.send_message(message)
smtp.quit()

22
config.py Normal file
View File

@ -0,0 +1,22 @@
class Config(object):
# SMTP Configuration
SERVER = 'smtp.gmail.com' # Uses GMail SMTP by default
PORT = 465 # Default GMail SSL SMTP port
# Customisation
GROUP = '' # Group/Organisation Name
EMAIL_DOMAIN = 'bar.com' # Email domain for organisation
SUBJECT = f'{GROUP} Secret Santa Draw' if GROUP else 'Secret Santa Draw' # Email subject line
GREETING = 'Ho Ho Ho' # Email greeting
SENDER = 'The Secret Santa Elf' # Email sign-off
# App Configs
DATA = './data.json' # Data file
OUTPUT = './output.json' # Output file
class Test(Config):
# Test Config
OVERRIDE_RECIPIENT = True # Override the recipient for all emails
RECIPIENT_EMAIL = '' # Test email address to receive all outgoing email

2
secret.bak.py Normal file
View File

@ -0,0 +1,2 @@
USERNAME = '' # GMail Username
PASSWORD = '' # 16-character 'App Password' (without spaces).

15
templates/plain.txt Normal file
View File

@ -0,0 +1,15 @@
$subject
$greeting $participant!
You are buying a gift for $gift_recipient.
That they have given the following preferences for their gift:
$preference
I hope you enjoy the secret Santa!
$sender
This is an automatically generated email. Please do not reply to this address as it will not be monitored.

45
templates/rich.html Normal file
View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
$subject
</title>
</head>
<body>
<h1>
$subject
</h1>
<h2>
$greeting $participant!
</h2>
<p>
You are buying a gift for
<strong>
$gift_recipient
</strong>
.
</p>
<p>
That they have given the following preferences for their gift:
<p>
<strong>
$preference
</strong>
</p>
</p>
<p>
I hope you enjoy the secret Santa!
</p>
<p>
$sender
</p>
<p>
<strong>
This is an automatically generated email.
Please do not reply to this address as it will not be monitored.
</strong>
</p>
</body>
</html>