How to Schedule Function to Run on Specific Date and Time in Python
Tags: python, scheduletasks, apscheduler, webdev
Published: 2023-06-29 16:14:57

During my bootcamp at Flatiron School, I wanted to create a reminder function for a group project's CLI app that would send email reminder to the user on a specific date and time that the user inputs. However, there was no straightforward answer online on how to achieve this, and it took me a whole day to figure it out while overcomplicating the issue. I even tried [Celery](https://docs.celeryq.dev/en/stable/#) and [RabbitMQ](https://www.rabbitmq.com/), but that didn't take me anywhere. So here I am, sharing with you the simple solution with the hope to save you time and energy in case you want to try doing something similar. Here we go!

## Pmail
[Pmail](https://pypi.org/project/pmail/) is a nifty command-line e-mail sender, written in Python that allows you to send email with simple configuration and commands. To install, you'll need to run the following command: `pip install pmail`


Then to configure, run this command `pmail --configure` and just fill out information for each requirement. If you're using Gmail, you will need to create an App password and use that in lieu of your regular email password. You can find the instructions [here](https://support.google.com/mail/answer/185833?hl=en).


Now that we've got the easiest part out of the way, let's move on to the next.

## Subprocess
In order to run **pmail** command from inside of the Python script, I had to import the subprocess module. In Python, the **subprocess** module provides a way to create new processes, interact with them, and manage input/output streams. It allows you to run external programs or system commands from within your Python script. The subprocess module provides various functions and classes to work with processes. In our case, we used **subprocess.run()**. This function is used to run a command in a new process and wait for it to complete. It is a high-level function that simplifies the process creation and management. Below is a function from our app using **subprocess.run()**:

```
def send_mail(user_email, subject, body):
    command_args = ["pmail", user_email, subject, body]
    try:
        subprocess.run(command_args, check=True)
        print("Email sent successfully!")
    except subprocess.CalledProcessError as e:
        print(f"Error occurred while sending email: {e}")
```

Here's an explanation of the code:

1. The function starts by constructing a list called command_args. This list contains the command-line arguments required to invoke **pmail**. The list includes the user_email, subject, and body as command-line arguments.

2. The subprocess.run() function is used to execute the **pmail** command with the specified command-line arguments. The **check=True** parameter ensures that if the **pmail** command returns a non-zero exit code (indicating an error), a **CalledProcessError** exception will be raised.

3. Inside the try block, **subprocess.run()** is called to execute the **pmail** command. If the command is executed successfully (i.e., returns a zero exit code), the message "Email sent successfully!" is printed.

4. If the **subprocess.run()** call raises a **CalledProcessError**, it means that the **pmail** command returned a non-zero exit code, indicating an error. In that case, the exception is caught in the except block, and an error message is printed.

Now that we've have written our send_mail function, it's time to introduce the next important element: **APScheduler**

## APScheduler
When you go to Google and search for a method to schedule task in Python, you will definitely find **Scheduler** first. However, **Scheduler** is only great for scheduling periodic task, not for scheduling on a specific date and time. So first, let's install [**APScheduler**](https://apscheduler.readthedocs.io/en/master/userguide.html): `pip install apscheduler`

Then we import the **BackgroundScheduler** from **APScheduler** and **datetime** and **timedelta**
`from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime, timedelta`

The BackgroundScheduler is a scheduler that runs in the background and uses a separate thread to execute scheduled tasks. It is suitable for long-running applications or when you want the scheduler to operate independently of the main program's execution.

Below is a portion of the **reminder** function that executes the **send_mail** function at a specific **remind_datetime**

```
def reminder(applications):
    view_app(applications)
    app_id = input("Enter the ID of the application for this reminder: ")
    when = int(
        input(
            "In how many days would you like to be reminded? (e.g: 0 for today, 1 for tomorrow, etc.)"
        )
    )
    remind_time = input(
        "Enter the time (24-hour format) you want to receive your notification (HH:MM): "
    )
    user_email = input("Enter the email address to receive notification: ")
    reminder_message = input("Enter the reminder message: ")

    today = datetime.today().date()
    remind_date = today + timedelta(days=when)
    remind_date_str = remind_date.strftime("%Y-%m-%d")
    remind_datetime = datetime.strptime(
        remind_date.strftime("%Y-%m-%d") + " " + remind_time, "%Y-%m-%d %H:%M"
    )
    print(
        f"You will be reminded on \033[1;32m{remind_date_str} at {remind_time}\033[0m about the following job application:"
    )

    for application in applications:
        if application.id == int(app_id):
            subject = f"Reminder about the {application.job_title} Application"
            body = f"Regarding {application.job_title} at {application.company.name}.\nHere's your message: {reminder_message}\nGood luck!"

            scheduler = BackgroundScheduler()
            scheduler.add_job(
                send_mail,
                "date",
                run_date=remind_datetime,
                args=[user_email, subject, body],
            )
            scheduler.start()
```
Here's an explanation of the reminder function:

1. The **reminder** function takes a parameter **applications**, which represents a list of job applications. The function starts by displaying the list of job applications using the **view_app** function.

2. It then prompts the user to enter the **ID** of the application they want to set a reminder for, the **number of days** from today they want to be reminded, the **time** (in 24-hour format) they want to receive the notification, their **email address**, and the **reminder message**.

3. Next, the function calculates the **remind date** by adding the specified number of days to the current date using the **datetime.today().date(**) and **timedelta** functions. It also formats the remind date as a string in the format **"%Y-%m-%d"** using **strftime**.

4. The function combines the remind date string and the remind time entered by the user to create a **remind_datetime** object using **datetime.strptime**. This object is required in the APScheduler method. 

5. Then, the function prints a message indicating the remind date and time.

6. It then iterates over the list of applications to find the application with the matching ID entered by the user.

7. Inside the loop, the function creates a **subject** and **body** for the reminder email based on the **selected application** and the **reminder message** entered by the user.

8. It creates an instance of the **BackgroundScheduler** class. The function adds a job to the scheduler using the **add_job** method. The job is scheduled to run at the **remind_datetime** specified. It calls the **send_mail** function with the provided **email address**, **subject**, and **body** as arguments.

9. Finally, the scheduler is started using the start method.

By following the steps above, you'll be able to create an email reminder feature for your CLI app. If you'd like to check out our OnTrack app, check out this [Github repo](https://github.com/phungmaiton/ontrack). Hope you find this helpful!

Happy coding!