Created
December 5, 2019 14:31
-
-
Save allsey87/4db34926a437d90d3164ae07b296175b to your computer and use it in GitHub Desktop.
Secret Santa Script
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
# 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 | |
""" |
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/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