Often your application will require some kind of recurring task that should happen at a specific time of the week, every minute, or every few months. These tasks can be things like:
Reading XML from a directory and importing it into a remote database
Checking if a customer is still a subscriber in Stripe and updating your local database
Cleaning your database of unneeded data every minute or so
Send an API call to a service in order to fire certain events
Or anything in between. There are lots of use cases for simple tasks to be ran during certain parts of the day or even "offline" hours when your employees are gone.
First we will need to install the scheduler feature. We can simply pip install it:
terminal$ pip install masonite-scheduler
and then add the Service Provider to our
PROVIDERS list in config/providers.py:
config/providers.py...from scheduler.providers import ScheduleProviderPROVIDERS = [AppProvider,......ScheduleProvider, # Here]
This provider will add several new features to Masonite. The first is that it will add two new commands.
The first command that will be added is the
craft schedule:run command which will run all the schedules tasks (which we will create in a bit).
The second command is a
craft task command which will create a new task under the
Now that we added the Service Provider, we can start creating tasks. Let's create a super basic task that prints "Hi". First let's create the task itself:
$ craft task SayHi
This will create a file under app/tasks/SayHi.py
from scheduler.Task import Taskclass SayHi(Task):def __init__(self):passdef handle(self):pass
This will be the simple boilerplate for our tasks.
All tasks should inherit the
scheduler.task.Task class. This adds some needed methods to the task itself but also allows a way to fetch all the tasks from the container by collecting them.
In order for tasks to be discovered they need to be inside the container. Once inside the container, we collect them, see if they need to run and then decide to execute them or not.
There are two ways to get classes into the container. The first is to bind them into the container manually by creating a Service Provider. You can read the documentation on creating Service Providers if you don't know how to do that.
The other way is to Autoload them. Starting with Masonite 2.0, You can autoload entire directories which will find classes in that directory and load them into the container. This can be done by adding the directory your tasks are located in to the AUTOLOAD config variable inside config/application.py:
...AUTOLOAD = ['app','app/tasks']...
This will find all the tasks in the app/tasks directory and load them into the container for you with the key binding being the name of the class.
Now that our task is able to be added to the container automatically, let's start building the class.
Firstly, the constructor of all tasks are resolved by the container. You can fetch anything from the container that doesn't need the WSGI server to be running (which is pretty much everything). So we can fetch things like the Upload, Mail, Broadcast and Request objects. This will look something like:
from scheduler.Task import Taskfrom masonite.request import Requestclass SayHi(Task):def __init__(self, request: Request):self.request = requestdef handle(self):pass
The handle method is where the logic of the task should live. This is where you should put the logic of the task that should be running recurrently.
We can do something like fire an API call here:
from scheduler.Task import Taskimport requestsclass SayHi(Task):def __init__(self):passdef handle(self):requests.post('http://url.com/api/store')
The awesomeness of recurring tasks is telling the task when it should run. There are a few options we can go over here:
A complete task could look something like:
from scheduler.Task import Taskimport requestsclass SayHi(Task):run_every = '3 days'run_at = '17:00'def __init__(self):passdef handle(self):requests.post('http://url.com/api/store')
This task will fire that API call every 3 days at 5pm.
All possible options are
False by default. The options here are:
Either a singular or plural version of the accepted time units:
run_every = '1 day'
The time in military time ("17:00" for 5pm)
run_at = '17:00'
Boolean on whether to run every hour or not:
run_every_hour = True
Boolean on whether to run every minute or not:
run_every_minute = True
A tuple on the hours of the day the task should run. Also in military time.
twice_daily = (1, 13)
You can also set timezones on individual tasks by setting a
timezone attribute on the task:
from scheduler.Task import Taskimport requestsclass SayHi(Task):run_every = '3 days'run_at = '17:00'timezone = 'America/New_York'def __init__(self):passdef handle(self):requests.post('http://url.com/api/store')
Since the scheduler doesn't actually know when the server starts, it doesn't know from what day to start the count down. In order to get around this, Masonite calculates the current day using a modulus operator to see if the modulus of the tasks time and the current time are 0.
For example, if the task above is to be ran (every 3 days) in May then the task will be ran at midnight on May 3rd, May 6th, May 9th, May 12th etc etc. So it's important to note that if the task is created on May 11th and should be ran every 3 days, then it will run the next day on the 12th and then 3 days after that.
After we add the directory to the
AUTOLOAD list, we can run the
schedule:run command which will find the command and execute it.
$ craft schedule:run
Masonite will fetch all tasks from the container by finding all subclasses of
scheduler.tasks.Task, check if they should run and then either execute it or not execute it.
Even though we ran the task, we should not see any output. Let's change the task a bit by printing "Hi" and setting it to run every minute:
from scheduler.Task import Taskclass SayHi(Task):run_every = '1 minute'def __init__(self):passdef handle(self):print('Hi!')
Now let's run the command again:
$ craft schedule:run
We should now see "Hi!" output to the terminal window.
You may also run a specific task by running the schedule:run command with a --task flag. The flag value is the container binding (usually the task class name):
craft schedule:run --task SayHi
Or you can give your task a name explicitly:
from scheduler.Task import Taskclass SayHi(Task):run_every = '1 minute'name = 'hey'def __init__(self):passdef handle(self):print('Hi!')
and then run the command by name
craft schedule:run --task hey
Although the command above is useful, it is not very practical in a production setup. In production, we should setup a cron job to run that command every minute so Masonite can decide on what jobs need to be ran.
We'll show you an example cron job and then we will walk through how to build it.
PATH=/Users/Masonite/Programming/project_name/venv/bin:/Library/Frameworks/Python.framework/Versions/3.6/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Frameworks/Python.framework/Versions/3.6/bin* * * * * cd /Users/Masonite/Programming/project_name && source venv/bin/activate && craft schedule:run
When a cron job runs, it will typically run commands with a /bin/sh command instead of the usual /bin/bash. Because of this, craft may not be found on the machine so we need to tell the cron job the PATH that should be loaded in. We can simply find the PATH by going to our project directory and running:
Which will show an output of something like:
We can then copy the PATH and put it in the cron job.
To enter into cron, just run:
$ env EDITOR=nano crontab -e
and paste the
PATH we just copied. Once we do that our cron should look like:
Exit out of nano. Now we just need to setup the actual cron job:
Now we just need to setup the cron task itself. This could be as easy as copying it and pasting it into the nano editor again. You may need to change a few things though.
* * * * * part is a must and bascially means "run this every minute by default". The next part is the location of your application which is dependant on where you installed your Masonite application.
The next part is dependant on your setup. If you have a virtual environment then you need to activate it by appending
&& source venv/bin/activate to the cron task. If you are not running in a virtual environment then you can leave that part out.
Lastly we need to run the schedule command so we can append
&& craft schedule:run
Great! Now we have a cron job that will run the craft command every minute. Masonite will decide which classes need to be executed.