Introduction to scheduled tasks helper scripts


For all PythonAnywhere users who like to automate their workflow using scripts there’s already the pythonanywhere package which provides an interface for some PythonAnywhere API features. If you’re one of them, you might be interested in some recent additions for programmatic management of Scheduled Tasks.

To use those scripts you should open a Bash console in your PythonAnywhere account and install the latest version of the package:

pip3.6 install --user pythonanywhere

Then, as well as some web app scripts, these new commands will be available:

  • pa_create_scheduled_task.py
  • pa_delete_scheduled_task.py
  • pa_get_scheduled_task_specs.py
  • pa_get_scheduled_tasks_list.py
  • pa_update_scheduled_task.py

Their names should be self-explanatory and each script prints a help message when executed with the --help flag, but let’s take a quick tour and see what they can do in action.

Creating and listing tasks

If you have a paid account, you may create tasks which run hourly or daily; free accounts can create just one task, which must be daily. Each task will execute a command (a Bash script, a Django management command, essentially everything your app needs to do periodically). So, in order to create a new scheduled task, a command and time when the task should be executed need to be provided. The script will guess you want to create an hourly task when you give it only the minute value; if you specify the hour as well, the task will be run daily. Let’s create our first task! (BTW it’s 3:10.)

pa_create_scheduled_task.py --command pa_get_scheduled_tasks_list.py --minute 42

We should see the PythonAnywhere mascot saying:

  __________________________________________________________________
/                                                                    \
| Task 'pa_get_scheduled_tasks_list.py' successfully created with id |
| 123456 and will be run hourly at 42 mins past                      |
\                                                                    /
  ------------------------------------------------------------------
   \
    ~<:>>>>>>>>>

OK, that means we’ve just created a task which will log all our tasks every hour. Because we’re in a hurry we won’t wait for the task to run for the first time. Instead we might run the command pa_get_scheduled_tasks_list.py to check if task 123456 actually would be run.

    id  interval    at            status    command
------  ----------  ------------  --------  ------------------------------
123456  hourly      42 mins past  enabled   pa_get_scheduled_tasks_list.py

Great! We should see similar output in our task’s log file. But… where is the log file?

Getting more information about a task

To get a known task data provided by PythonAnywhere API we’d run the pa_get_scheduled_task_specs.py command:

pa_get_scheduled_task_specs.py 123456

Task 123456 specs:
--------------  ----------------------------------------------------------------------------
command         pa_get_scheduled_tasks_list.py
enabled         True
expiry
hour
interval        hourly
logfile         /var/log/tasklog-123456-hourly-at-42-past-pa_get_scheduled_tasks_list.py.log
minute          42
printable_time  42 mins past
--------------  ----------------------------------------------------------------------------

To check task 123456's output we need to see contents of its log file (you check the time, it’s 3:12, the train to Yuma has left Contention City, but what concerns you more is the fact that the task will be run in half an hour from now… you type anyway):

# --logfile will output only the row containing... the log file name
# combined with --nospec it will output only the path
# which we may pass directly to another command
cat $(pa_get_scheduled_task_specs.py 123456 --logfile --no-spec)

As should be expected the output is:

Task has not been run yet

All right, you take a deep breath and recall Thales of Miletus saying “time is the wisest of things, for it finds out everything”. During your free time before 3:42 you check --help messages provided by the pythonanywhere commands and find out everything. Or at least two things: commands take short flags like -l and each task can be modified. Phew, let’s check the log now:

cat $(pa_get_scheduled_task_specs.py 123456 -ln)

    id  interval    at            status    command
------  ----------  ------------  --------  ------------------------------
123456  hourly      42 mins past  enabled   pa_get_scheduled_tasks_list.py

2020-05-22 15:43:08 -- Completed task, took 4.00 seconds, return code was 0.

Modifying and deleting tasks

Enlightened by the new knowledge we might play with our scheduled task a bit. It’s said the tasks may be run daily or hourly, right? What about a task which would run every minute? Challenge accepted! First, let’s create a short Bash script in our home directory called every-minute-task.sh (and make it executable with chmod +x every-minute-task.sh):

#!/bin/bash
pa_update_scheduled_task.py $1 -m $(date -d"$(date) + 1 minute" +"%M") -p

The script will use the first argument passed to it ($1) as id of a task whose minute (-m) will be increased by one (-p flag at the end, which is equal to --porcelain, will disable “talking snakes effect” and render command output with an easy-to-parse format because this is what we want in log files, don’t we?).

Now we can update our task which was previously logging the list of our tasks (note that the task ID appears twice in this line!):

pa_update_scheduled_task.py 123456 --command "$HOME/every-minute-task.sh 123456"

Gentle snake informs us:

  ___________________________________________________________________
/                                                                     \
| Task 123456 updated: <command> from 'pa_get_scheduled_tasks_list.py'|
| to '/home/marshalofbisbee/every-minute-task.sh 123456'              |
\                                                                     /
  -------------------------------------------------------------------
   \
    ~<:>>>>>>>>>

Assuming some time passed so our task could run after the update we might take a look into our logs. Let’s check /var/log directory where all logs belong:

ls /var/log/
...
tasklog-123456-hourly-at-42-past-pa_get_scheduled_tasks_list.py.log
tasklog-123456-hourly-at-42-past-.home.marshalofbisbee.every-minute-task.sh_123456.log
tasklog-123456-hourly-at-43-past-.home.marshalofbisbee.every-minute-task.sh_123456.log
tasklog-123456-hourly-at-44-past-.home.marshalofbisbee.every-minute-task.sh_123456.log
...

We see that our little hack worked and task 123456 was busy updating itself every minute. More importantly, we notice that each update changes the log file name while keeping the task id. So, in a real world scenario, if a task was updated and for some reason you’d like to parse all its logs, you’ll have to keep in mind that the path returned by pa_get_scheduled_task_specs.py command will point only to the most recent log. (In such a case you’d rather use the task’s id and grep "Phrase to look for" /var/logs/tasklog-<id>-* where <id> should be replaced by id number).

OK, it’s high time to do some clean up. Before we delete our dummy task let’s check if the logs contain what we expect.

cat /var/log/*every-minute-task*


Task 123456 updated:
<minute> from '42' to '43'

2020-05-22 16:42:11 -- Completed task, took 5.57 seconds, return code was 0.


Task 123456 updated:
<minute> from '43' to '44'

2020-05-22 16:43:08 -- Completed task, took 4.09 seconds, return code was 0.


Task 123456 updated:
<minute> from '44' to '45'

2020-05-22 16:44:11 -- Completed task, took 5.03 seconds, return code was 0.

...

Yup, that’s what we wanted. Now, deleting a task is as easy as:

pa_delete_scheduled_task.py id 123456

Importantly, the log files will persist even if the task is deleted.

What’s next?

The current pythonanywhere version1 provides simple scripting functionality to manage scheduled tasks. If you find some options lacking – try to write your own command, all of the helper scripts are open source!

Further reading


  1. 0.9.2 released on May 22, 2020. ↩︎

comments powered by Disqus