Compare commits

...

9 Commits

Author SHA1 Message Date
jeanGaston 23c4276afb Update Readme
- Correction on file tree
2024-10-25 14:14:41 +02:00
jeanGaston 95f0dea5b3 Update Readme 2024-10-25 11:05:11 +02:00
jeanGaston 0f2df17584 Update Gitignore
- Add srv/env.py
2024-10-25 10:49:59 +02:00
jeanGaston 8934f58292 Update env.py
- Add CSV_PATH
- Add CSV_Prefix
_ Add History_years
2024-10-25 10:49:37 +02:00
jeanGaston 2f3f1d9a13 minor Updates 2024-10-25 10:45:38 +02:00
jeanGaston 541b947609 Update function to load n years of history
Add a function to load participants data from the current year file
2024-10-25 10:45:22 +02:00
jeanGaston 598bae34ba Update draw function to take n years of history in account 2024-10-25 10:44:39 +02:00
jeanGaston 0d9f5264d4 Merge remote-tracking branch 'origin/main' 2024-10-20 19:16:59 +02:00
jeanGaston 1d376d6b56 Change in files organization
Readme updates
fix bu in env.py
2024-10-20 19:14:36 +02:00
7 changed files with 147 additions and 75 deletions
+1 -1
View File
@@ -160,4 +160,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
config/env.py src/env.py
+35 -21
View File
@@ -1,12 +1,12 @@
# Random Christmas Bot # Random Christmas Bot
This Python project automates the process of organizing a Secret Santa event. It randomly assigns each participant one or two recipients and ensures participants do not receive the same recipients from the previous years. The results are sent via email using an SMTP relay, and participant data (names, emails, and previous draw results) are managed in a CSV file.
This Python project automates the process of organizing a Secret Santa event. It randomly assigns each participant one or two recipients and ensures participants do not receive the same recipients from the previous year. The results are sent via email using an SMTP relay, and participant data (names, emails, and previous draw results) are managed in a CSV file.
## Features ## Features
- Randomly assign one or two recipients for each participant. - Randomly assign one or two recipients for each participant.
- Ensure participants do not receive the same recipients as last year. - Ensure participants do not receive the same recipients as the last `n` years.
- Sends personalized emails with draw results to participants. - Sends personalized emails with draw results to participants.
- Stores participant data (names, emails, and draw results) in a CSV file. - Stores participant data (names, emails, and draw results) in a CSV file.
- Modular structure for better code maintenance. - Modular structure for better code maintenance.
@@ -15,33 +15,34 @@ This Python project automates the process of organizing a Secret Santa event. It
## Project Structure ## Project Structure
```bash ```bash
secret-santa/ Random-Christmas-Bot/
├── config/
│ └── env.py # Configuration settings (SMTP, file paths, etc.)
├── src/ ├── src/
│ ├── draw.py # Logic for drawing names │ ├── draw.py # Logic for drawing names
│ ├── emailer.py # Email sending functionality │ ├── emailer.py # Email sending functionality
│ ├── file_io.py # File handling (CSV reading/writing) │ ├── file_io.py # File handling (CSV reading/writing)
│ ├── main.py # Main program logic │ ├── main.py # Main program logic
── utils.py # Utility functions (date, time handling) ── utils.py # Utility functions (date, time handling)
│ └── env.py # Configuration settings (SMTP, file paths, etc.)
└── README.md # Project readme └── README.md # Project readme
``` ```
### `config/env.py` ### `src/env.py`
The configuration file contains SMTP settings, file paths, and customizable parameters for the draw. The configuration file contains SMTP settings, file paths, and customizable parameters for the draw.
Example `env.py`: Example `env.py`:
```python ```python
# Configuration file with SMTP settings and CSV path settings
SMTP_SERVER = "smtp.example.com" SMTP_SERVER = "smtp.example.com"
SMTP_PORT = 25 SMTP_PORT = 25
SENDER_EMAIL = "santa@example.com" SENDER_EMAIL = "santa@example.com"
CSV_FILE_PATH = "secret_santa_DB.csv" CSV_PATH = r"path\to\your\csv\files" # Path to the CSV files
DRAW_PER_PERSON = 2 # Choose 1 or 2 recipients per person CSV_PREFIX = r"secret_santa_DB" # Prefix for CSV files
HISTORY_YEARS = 2 # Number of past years to consider in the draw
DRAW_PER_PERSON = 2 # Number of recipients per person
# Email content # Email content
EMAIL_SUBJECT = "Secret Santa {year} Draw" EMAIL_SUBJECT = "Secret Santa {year} Draw"
@@ -68,44 +69,57 @@ This email was sent automatically, please do not reply.
1. Clone the repository or download the script files. 1. Clone the repository or download the script files.
2. Ensure you have Python installed on your system. If not, download and install Python from [here](https://www.python.org/downloads/). 2. Ensure you have Python installed on your system. If not, download and install Python from [here](https://www.python.org/downloads/).
3. Set up the `env.py` file in the `config/` directory, adjusting the SMTP settings, CSV file path, and draw parameters as needed. 3. Set up the `env.py` file in the `src/` directory, adjusting the SMTP settings, CSV file path, and draw parameters as needed.
Example structure of the CSV file:
Example structure of the CSV file with the following columns:
```csv ```csv
Name,Email,Last_Year_Recipient_1,Last_Year_Recipient_2 Name,Email,Last_Year_Recipient_1,Last_Year_Recipient_2
```
```csv
Alice,alice@example.com,Bob,Charlie Alice,alice@example.com,Bob,Charlie
Bob,bob@example.com,Alice,David Bob,bob@example.com,Alice,David
Charlie,charlie@example.com,David,Alice Charlie,charlie@example.com,David,Alice
``` ```
4. Ensure the CSV file is in the correct location as specified in `env.py`. 4. Name your CSVs using a consistent naming convention `[prefix]_20xx.csv` as the program will retrieve those using the prefix set in `env.py`.
5. Ensure the CSV file is in the correct location as specified in `env.py`.
## Usage ## Usage
1. Run the main script by executing: 1. Create a CSV file for the current year draw with the participants' information (Name and email).
Like this:
```csv
Alice,alice@example.com
Bob,bob@example.com
Charlie,charlie@example.com
```
2. Run the main script by executing:
```bash ```bash
python src/main.py python src/main.py
``` ```
2. The script will: 3. The script will:
- Load participant data from the CSV file. - Load participant data from the CSV file.
- Perform the Secret Santa draw based on the configuration (1 or 2 recipients). - Perform the Secret Santa draw based on the configuration (1 or 2 recipients).
- Send an email to each participant with the names of their gift recipients. - Send an email to each participant with the names of their gift recipients.
- Save the updated draw results back to the CSV file. - Save the updated draw results back to the CSV file.
3. If any errors occur, they will be displayed in the console, and you can retry or debug as needed. 4. If any errors occur, they will be displayed in the console, and you can retry or debug as needed.
## Customization ## Customization
- **Number of recipients**: Modify `DRAW_PER_PERSON` in `env.py` to choose whether participants receive one or two recipients. - **Number of recipients**: Modify `DRAW_PER_PERSON` in `env.py` to choose whether participants receive one or two recipients.
- **Email content**: Customize the email subject and body in `env.py` using placeholders like `{name}` for the participant's name and `{draws}` for their recipients. - **Email content**: Customize the email subject and body in `env.py` using placeholders like `{name}` for the participant's name and `{draws}` for their recipients.
- **CSV file location**: Adjust the `CSV_FILE_PATH` in `env.py` if you prefer a different directory for the participant data. - **CSV file location**: Adjust the `CSV_PATH` in `env.py` if you prefer a different directory for the participant data.
- **Number of historical years**: Change `HISTORY_YEARS` in `env.py` to set how many previous years of draws should be considered.
## File Descriptions ## File Descriptions
- **`draw.py`**: Contains the logic for performing the Secret Santa draw, ensuring no repeat recipients from last year. - **`draw.py`**: Contains the logic for performing the Secret Santa draw, ensuring no repeat recipients from the last years.
- **`emailer.py`**: Handles email sending via the SMTP server. - **`emailer.py`**: Handles email sending via the SMTP server.
- **`file_io.py`**: Responsible for reading and writing the participant data from/to the CSV file. - **`file_io.py`**: Responsible for reading and writing the participant data from/to the CSV file.
- **`main.py`**: The main program that ties everything together and coordinates the draw and email sending. - **`main.py`**: The main program that ties everything together and coordinates the draw and email sending.
@@ -123,4 +137,4 @@ This project is licensed under the MIT License. You are free to modify and distr
## Contributions ## Contributions
Contributions are welcome! Feel free to open issues or submit pull requests to improve this project. Contributions are welcome! Feel free to open issues or submit pull requests to improve this project.
+38 -27
View File
@@ -1,40 +1,51 @@
from random import choice from random import choice
def draw_names(previous_draw, draws_per_person): def draw_names(current_participants, history_data, draws_per_person, max_attempts=5):
""" """
Perform the Secret Santa draw considering past draws. Perform the Secret Santa draw considering past years' draws.
:param previous_draw: Last year's draw data (list of participants and recipients). :param current_participants: This year's participant list.
:param history_data: Historical draw data to avoid repeats.
:param draws_per_person: Number of people each participant should give gifts to. :param draws_per_person: Number of people each participant should give gifts to.
:param max_attempts: Maximum number of retry attempts before loosening exclusions. by default : 5.
:return: The new draw results. :return: The new draw results.
""" """
participants = [a[0] for a in previous_draw] # Get participant names participants = [p[0] for p in current_participants] # Get participant names
already_drawn = [] # Track who has been drawn already_drawn = [] # Track who has been drawn
new_draw = [] # Store new draw results new_draw = [] # Store new draw results
for i in range(len(participants)): for attempt in range(max_attempts):
last_year_r1 = previous_draw[i][2] new_draw.clear()
last_year_r2 = previous_draw[i][3] already_drawn.clear()
giver = previous_draw[i][0] success = True # Track if draw is successful in this attempt
email = previous_draw[i][1]
available_participants = participants.copy() for i, giver in enumerate(participants):
try: email = current_participants[i][1]
available_participants.remove(giver) # Collect previous recipients to avoid drawing the same person again
available_participants.remove(last_year_r1) previous_recipients = {recipient for record in history_data if record[0] == giver for recipient in record[2:]}
available_participants.remove(last_year_r2)
except ValueError:
pass
new_recipients = [] # Create a set of available participants excluding the giver and previous recipients
while len(new_recipients) < draws_per_person: available_participants = set(participants) - {giver} - previous_recipients
selected = choice(available_participants) new_recipients = []
if already_drawn.count(selected) >= draws_per_person:
available_participants.remove(selected)
else:
new_recipients.append(selected)
already_drawn.append(selected)
available_participants.remove(selected)
new_draw.append([giver, email] + new_recipients) # Ensure there are enough available participants for the draw
if len(available_participants) < draws_per_person:
success = False
break
return new_draw # Select recipients for the current giver
while len(new_recipients) < draws_per_person:
selected = choice(list(available_participants))
if already_drawn.count(selected) < draws_per_person:
new_recipients.append(selected)
already_drawn.append(selected)
available_participants.discard(selected)
new_draw.append([giver, email] + new_recipients)
# If the draw was successful, break out of attempts
if success:
return new_draw
print(f"Attempt {attempt + 1} failed, retrying...")
# If all attempts fail, raise an error
raise ValueError("Unable to complete a valid draw after maximum attempts.")
+1 -1
View File
@@ -1,7 +1,7 @@
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from config.env import SMTP_SERVER, SMTP_PORT, SENDER_EMAIL from env import SMTP_SERVER, SMTP_PORT, SENDER_EMAIL
def send_email(receiver_email, subject, msg): def send_email(receiver_email, subject, msg):
""" """
+6 -3
View File
@@ -1,8 +1,11 @@
# Configuration file with SMTP settings and CSV path settings
SMTP_SERVER = "smtp.example.com" SMTP_SERVER = "smtp.example.com"
SMTP_PORT = 25 SMTP_PORT = 25
SENDER_EMAIL = "santa@example.com" SENDER_EMAIL = "santa@example.com"
CSV_FILE_PATH = r"secret_santa_DB.csv" CSV_PATH = r"path\to\your\csv\files" # Path to the CSV files
DRAW_PER_PERSON = 2 # Choose 1 or 2 recipients per person CSV_PREFIX = r"secret_santa_DB" # Prefix for CSV files
HISTORY_YEARS = 2 # Number of past years to consider in the draw
DRAW_PER_PERSON = 2 # Number of recipients per person
# Email content # Email content
EMAIL_SUBJECT = "Secret Santa {year} Draw" EMAIL_SUBJECT = "Secret Santa {year} Draw"
@@ -15,4 +18,4 @@ Feel free to use your imagination and make their Christmas magical!
Merry Christmas! Merry Christmas!
This email was sent automatically, please do not reply. This email was sent automatically, please do not reply.
""" """
+52 -8
View File
@@ -1,13 +1,57 @@
import csv import csv
from datetime import date
from env import CSV_PREFIX, CSV_PATH
def open_csv(file_name): def load_history(years):
"""Open the CSV file and return its contents as a list of rows""" """
with open(file_name, "r", encoding='utf-8') as file: Load participant data from multiple CSV files based on the specified history years.
reader = csv.reader(file) :param years: Number of years of history to load.
return list(reader) :return: A list of all historical draw data.
"""
current_year = date.today().year
history_data = []
def save_csv(data, file_name): # Load data for each of the past specified years
"""Save the updated draw results to the CSV file""" for i in range(1, years + 1):
try:
year = current_year - i
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{year}.csv"
with open(file_name, "r", encoding='utf-8') as file:
reader = csv.reader(file)
history_data.extend(list(reader)) # Add each year's data
except FileNotFoundError:
print(f"No historical file found for year {year}, skipping.")
continue
return history_data
def load_participants():
"""
Load participant data from the current year CSV file.
:return: A list of all historical draw data.
"""
current_year = date.today().year
participants_data = []
try:
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{current_year}.csv"
print(file_name)
with open(file_name, "r", encoding='utf-8') as file:
reader = csv.reader(file)
participants_data.extend(list(reader)) # Add each year's data
except FileNotFoundError:
print(f"No file found for year {current_year}, skipping.")
return participants_data
def save_csv(data, year):
"""
Save the new draw results to a CSV file named with the current year.
:param data: New draw data.
:param year: The current year for naming the file.
"""
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{year}.csv"
with open(file_name, 'w', newline='', encoding='utf-8') as file: with open(file_name, 'w', newline='', encoding='utf-8') as file:
writer = csv.writer(file) writer = csv.writer(file)
writer.writerows(data) writer.writerows(data)
+14 -14
View File
@@ -1,8 +1,8 @@
from config.env import CSV_FILE_PATH, DRAW_PER_PERSON, EMAIL_SUBJECT, EMAIL_BODY from env import DRAW_PER_PERSON, HISTORY_YEARS, EMAIL_SUBJECT, EMAIL_BODY
from src.file_io import open_csv, save_csv from file_io import load_history, save_csv, load_participants
from src.draw import draw_names from draw import draw_names
from src.emailer import send_email from emailer import send_email
from src.utils import get_current_time from utils import get_current_time
from datetime import date from datetime import date
def send_all_emails(new_draw): def send_all_emails(new_draw):
@@ -11,29 +11,29 @@ def send_all_emails(new_draw):
for participant in new_draw: for participant in new_draw:
name = participant[0] name = participant[0]
receiver_email = participant[1] receiver_email = participant[1]
draws = ", ".join(participant[2:]) # List of recipients draws = ", ".join(participant[2:])
message = EMAIL_BODY.format(name=name, draws=draws) message = EMAIL_BODY.format(name=name, draws=draws)
subject = EMAIL_SUBJECT.format(year=current_year) subject = EMAIL_SUBJECT.format(year=current_year)
send_email(receiver_email, subject, message) # Send the email send_email(receiver_email, subject, message)
print(f"Email sent to {name} ({receiver_email})") print(f"Email sent to {name} ({receiver_email})")
if __name__ == "__main__": if __name__ == "__main__":
try: try:
# Load previous draw data # Load data from historical CSVs and this year's participants
old_draw = open_csv(CSV_FILE_PATH) history_data = load_history(HISTORY_YEARS)
# Perform new draw current_year = date.today().year
new_draw = draw_names(old_draw, DRAW_PER_PERSON) current_participants = load_participants() # Load participants data
# Perform the draw
new_draw = draw_names(current_participants, history_data, DRAW_PER_PERSON)
# Send emails to participants # Send emails to participants
send_all_emails(new_draw) send_all_emails(new_draw)
# Save new draw results # Save new draw results
save_csv(new_draw, CSV_FILE_PATH) save_csv(new_draw, current_year)
# Output completion time
print(f"Process completed at {get_current_time()[1]} on {get_current_time()[0]}") print(f"Process completed at {get_current_time()[1]} on {get_current_time()[0]}")
except Exception as e: except Exception as e:
print(f"Error occurred: {e}") print(f"Error occurred: {e}")
# Retry or handle errors