Skip to content

Instantly share code, notes, and snippets.

@allsey87
Created December 5, 2019 14: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 allsey87/4db34926a437d90d3164ae07b296175b to your computer and use it in GitHub Desktop.
Save allsey87/4db34926a437d90d3164ae07b296175b to your computer and use it in GitHub Desktop.
Secret Santa Script
# Configuration file for secret santa
participants = [
"User One <u1@email.com>",
"User two <u2@email.com>",
"User three <u3@email.com>",
]
dont_pair = [
"User One, User Two",
]
[email]
smtp_server = "smtp.example.net"
smtp_port = 587
# if you dont want to hardcode the following login values,
# you can comment them out and the program will ask you
# at run time for your credentials.
smtp_user = "login"
smtp_passwd = "password"
from = "Santa <santa@example.org>"
subject = "Your secret santa assignment"
message = """
Dear {santa},
Your are the Secret Santa of {santee} !
Place your wrapped gift (with the name tag saying "{santee}") in XYZ before december 1.
Santa
"""
#!/usr/bin/python3
import random
import smtplib
import argparse
import toml
import re
from getpass import getpass
from email.message import EmailMessage
class Pair:
def __init__(self, giver, receiver):
self.giver = giver
self.receiver = receiver
def print_line(self):
line = "{};{};{}\n".format(self.giver['name'], self.giver['email'], self.receiver['name'])
return(line)
def __str__(self):
return "{} --> {}".format(self.giver['name'], self.receiver['name'])
class Givers:
def __init__(self):
self.givers = ()
def add(self, giver):
self.givers = self.givers + (giver,)
def find_person_by_id(self, id):
return next((p for p in self.givers if p['id']==id), None)
def find_person_by_name(self, name):
return next((p for p in self.givers if p['name']==name), None)
def get(self):
return self.givers
def generate_pairs(config, outputfile):
print("output file is:", outputfile)
regex = re.compile("^(.+?)\s+<(.+)>$")
givers = Givers()
for id,person in enumerate(config['participants']):
match = re.match(regex, person)
name = match.group(1)
email = match.group(2)
givers.add({"id": id, "name": name, "email": email, "invalid_receivers":[] })
for person in givers.get():
name_list = filter(lambda i:i["name"] == person['name'], givers.get())
if len(list(name_list)) > 1:
print("WARNING: Multiple people with same name.")
for pair in config['dont_pair']:
names = [ x.strip() for x in pair.split(',') ]
givers.find_person_by_name(names[0])['invalid_receivers'].append(givers.find_person_by_name(names[1])['id'])
print("There are {} participants".format(len(givers.get())))
pairs = create_pairs_2(givers)
#pairs = create_pairs(givers) #only works for small number of people (<20)
with open(outputfile,"w",encoding='utf8') as output_file:
for p in pairs:
output_file.write(p.print_line());
# print(p)
print("Pair file {} generated".format(outputfile))
def create_pairs(obj_givers): #only works with small number of people
pairs = []
givers = obj_givers.get()
receivers = obj_givers.get()
graph = { d['id']:set() for d in givers }
for giver in givers:
for receiver in receivers:
if receiver['id'] not in giver['invalid_receivers'] and receiver['id'] is not giver['id']:
graph[giver['id']].add(receiver['id'])
all_full_cycles = get_all_full_cycles(graph, list(graph.keys())[0])
if len(all_full_cycles) == 0:
print('No possible combinaison')
exit(1)
chosen_full_cycle = random.choice(all_full_cycles)
#chosen_full_cycle = all_full_cycles[0]
print(chosen_full_cycle)
for giver, receiver in zip(chosen_full_cycle[:-1], chosen_full_cycle[1:]):
pairs.append(Pair(obj_givers.find_person_by_id(giver), obj_givers.find_person_by_id(receiver)))
return pairs
def get_all_cycles(graph, start):
stack = [(start, [start])]
all_cycles = []
while len(stack) > 0:
(vertex, path) = stack.pop()
children_not_on_path_or_start = graph[vertex] - set(path[1:])
for child in children_not_on_path_or_start:
if child == start:
all_cycles.append(path + [child])
else:
stack.append((child, path + [child]))
return all_cycles
def get_all_full_cycles(graph, start):
all_cycles = get_all_cycles(graph, start)
all_full_cycles = [cycle for cycle in all_cycles if len(cycle) == len(graph) + 1]
return all_full_cycles
def create_pairs_2(givers):
pairs = []
max_iterations = 1000
found_good_pairing = False
iteration = 0
id_list = [ d['id'] for d in givers.get() ]
while(iteration < max_iterations and not found_good_pairing):
iteration += 1
found_good_pairing = True
random.shuffle(id_list)
# print(id_list)
for index,person in enumerate(givers.get()):
if id_list[index] in person["invalid_receivers"]:
found_good_pairing = False
break
elif id_list[index] is person['id']:
found_good_pairing = False
break
elif person['id'] is id_list[givers.get().index( givers.find_person_by_id(id_list[index]) )]:
found_good_pairing = False
break
if found_good_pairing:
for index,giver in enumerate(givers.get()):
pairs.append(Pair(giver, givers.find_person_by_id(id_list[index])))
else:
print('No suitable combinaison found.')
exit(1)
return pairs
def send_mail(conf, send):
print("Sending...")
if args.test:
print('DRY RUN (not sending the messages)')
else:
if not (usr := conf["email"].get("smtp_user")): #Python3.8 FTW
usr = input('Username?')
if not (mdp := conf["email"].get("smtp_passwd")):
mdp = getpass('Password?')
# s = smtplib.SMTP_SSL(conf['email']['smtp_server'], port=conf['email']['smtp_port'])
s = smtplib.SMTP(conf['email']['smtp_server'], port=conf['email']['smtp_port'])
s.ehlo()
s.starttls()
s.ehlo()
s.login(usr, mdp)
print("Connected.")
try:
f = open(send)
except FileNotFoundError:
print("Result file {} dont exist".format(outputfile))
exit(1)
for line in f:
tmp = map(lambda obj:obj.strip(),line.split(";"))
santa, santa_email, santee = tmp
print("sending to {} at {}".format(santa, santa_email))
message = conf['email']['message'].format(santa=santa, santee=santee)
msg = EmailMessage()
msg.set_content(message)
msg['Subject'] = conf['email']['subject']
msg['From'] = conf['email']['from'] #"Santa <santa@ananas.space>"
msg['To'] = santa_email
if not args.test:
s.send_message(msg)
else:
# print(msg)
pass
if not args.test:
print("Messages sent !")
s.quit()
def main(args):
conf = toml.load(args.config)
if args.draw:
generate_pairs(conf, args.draw)
elif args.send:
print("Send emails for file {}".format(args.send))
send_mail(conf, args.send)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Secret santa script, config should be in config.toml')
parser.add_argument("-c", "--config", help="toml config file (default: config.toml)", required=False, default='config.toml')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-d","--draw", help="draws and saves in output file", required=False)
group.add_argument("-s", "--send" , help="send emails based on input file (input file should be output file of draw)", required=False)
parser.add_argument('-t', "--test", help="dry run before sending message (only as effect used in addition to -s (send))", action='store_true')
args = parser.parse_args()
# print(args)
main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment