-
-
Save bbengfort/4df612b0155ca2a362ae to your computer and use it in GitHub Desktop.
Randomly generate gift giving combinations for one big gift Christmas.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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