Skip to content

Instantly share code, notes, and snippets.

@bbengfort
Last active November 21, 2020 21:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bbengfort/4df612b0155ca2a362ae to your computer and use it in GitHub Desktop.
Save bbengfort/4df612b0155ca2a362ae to your computer and use it in GitHub Desktop.
Randomly generate gift giving combinations for one big gift Christmas.
#!/usr/bin/env python3
# gifter
# Randomly selects who gives what big Christmas gift.
#
# Author: Benjamin Bengfort <benjamin@bengfort.com>
# Created: Fri Dec 25 10:07:41 2015 -0500
#
# ID: gifter.py [] benjamin@bengfort.com $
"""
Randomly selects who gives what big Christmas gift.
Version 2.0 now includes the ability to send emails to the recipients in order to keep
the results a secret from everyone, including the person generating the results!
"""
import os
import smtplib
import argparse
import email.utils
from random import shuffle
from collections import Counter
from itertools import combinations
from collections import defaultdict
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
VERSION = "%(prog)s v2.0"
DESCRIPTION = "Randomly select partners for secret santa gift exchange"
EPILOG = "https://gist.github.com/bbengfort/4df612b0155ca2a362ae"
def random_combinations(items, iterations=100):
"""
Randomly combines items until a group reaches the minimum number of votes.
This function will yield both the item voted for and the # of votes.
"""
votes = defaultdict(Counter)
items = list(items)
giftees = set([])
for _ in range(iterations):
shuffle(items)
for (giver, giftee) in combinations(items, 2):
votes[giver][giftee] += 1
combos = []
for giver, votes in votes.items():
for giftee, vote in votes.most_common():
if giftee not in giftees:
combos.append((giver, giftee, vote))
giftees.add(giftee)
break
if len(combos) != len(items):
return random_combinations(items, iterations)
return combos
def read_names(path):
"""
Reads the names from the associated text file (newline delimited). There
must be an even number of names otherwise this won't work very well.
"""
with open(path, 'r') as f:
for name in f:
name = name.strip()
if name and not name.startswith("#"):
yield name
class Postman(object):
BODY_TEXT = (
"Hello {giftr},\r\n\r\n"
"I'm writing to let you know that you are the Secret Santa "
"for {giftee} this year!\r\n\r\n"
"Happy Christmas!\r\n"
"The Bengfort.com Server\r\n"
)
BODY_HTML = (
"<html>\n"
"<head></head>\n"
"<body>\n"
"<p>Hello {giftr},</p>\n"
"<p>I'm writing to let you know that you are the Secret Santa "
"for <strong>{giftee}</strong> this year!<p>\n"
"<p>Happy Christmas!<br />The Bengfort.com Server</p>\n"
"</body>\n"
"</html>"
)
SUBJECT = "Christmas Giftr"
def __init__(self, sender, username, password, host, port):
self.sender = email.utils.formataddr(self.parseaddr(sender))
self.username = username
self.password = password
self.host = host
self.port = port
self._server = None
def connect(self):
if self._server is not None:
return
self._server = smtplib.SMTP(self.host, self.port)
self._server.ehlo()
self._server.starttls()
self._server.ehlo()
self._server.login(self.username, self.password)
def close(self):
if self._server is None:
return
self._server.close()
self._server = None
def make_mesage(self, giftr, giftee):
names = {}
names["giftr"], _ = self.parseaddr(giftr)
names["giftee"], _ = self.parseaddr(giftee)
msg = MIMEMultipart("alternative")
msg["Subject"] = self.SUBJECT
msg["From"] = self.sender
msg["To"] = giftr
plain = MIMEText(self.BODY_TEXT.format(**names), "plain")
rich = MIMEText(self.BODY_HTML.format(**names), "html")
msg.attach(plain)
msg.attach(rich)
return msg
def send(self, giftr, giftee):
print(giftr, giftee)
return
self.connect()
msg = self.make_mesage(giftr, giftee)
self._server.sendmail(self.sender, giftr, msg.as_string())
def parseaddr(self, addr):
name, emailaddr = email.utils.parseaddr(addr)
if not name or not emailaddr:
raise ValueError(f"could not parse '{addr}' into name <email> parts")
return name, emailaddr
def main(args):
postman = None
if not args.no_email:
postman = Postman(
args.sender, args.username, args.password, args.host, args.port
)
# Print the names and the votes!
for data in random_combinations(read_names(args.names)):
if postman:
postman.send(data[0], data[1])
else:
print("{} --> {} ({} votes)".format(*data))
if __name__ == '__main__':
args = {
("-v", "--version"): {
"action": "version",
"version": VERSION,
},
("-n", "--names"): {
"metavar": "PATH",
"type": str,
"default": "names.txt",
"help": "path to newline delimited file with name <email>",
},
("-E", "--no-email"): {
"action": "store_true",
"help": "do not send emails but print results instead",
},
("-s", "--sender"): {
"metavar": "EMAIL",
"type": str,
"default": os.getenv("GIFTR_SENDER"),
"help": "the name <email> of the verified SES address [$GIFTR_SENDER]",
},
("-u", "--username"): {
"metavar": "USER",
"type": str,
"default": os.getenv("GIFTR_USERNAME"),
"help": "the username of the SES SMTP server [$GIFTR_USERNAME]",
},
("-p", "--password"): {
"metavar": "PASS",
"type": str,
"default": os.getenv("GIFTR_PASSWORD"),
"help": "the password for the SES SMTP server [$GIFTR_PASSWORD]",
},
("-H", "--host"): {
"metavar": "HOST",
"type": str,
"default": os.getenv("GIFTR_HOST", "email-smtp.us-east-1.amazonaws.com"),
"help": "the host of the SES SMTP server [$GIFTR_HOST]",
},
("-P", "--port"): {
"metavar": "P",
"type": int,
"default": os.getenv("GIFTR_PORT", 587),
"help": "the port of the SES SMTP server [$GIFTR_PORT]",
},
}
parser = argparse.ArgumentParser(description=DESCRIPTION, epilog=EPILOG)
for pargs, kwargs in args.items():
if isinstance(pargs, str):
pargs = (pargs,)
parser.add_argument(*pargs, **kwargs)
main(parser.parse_args())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment