-
Notifications
You must be signed in to change notification settings - Fork 0
Example: Task Status Triggers
NOTE: This is intended to show the basic general workflow of how you can create triggers. It is not intended as a guide to writing triggers for the shotgunEventDaemon trigger script in our repository. There is documentation provided for the shotgunEventDaemon project that covers writing plugins specifically for that script.
We strongly recommend using the shotgunEventDaemon for setting up your triggers as it does most of the dirty work for you and is supported by the Shotgun open source community. However, the below article will provide you a good background of generally how triggers work with Shotgun, so read it anyway!
Let's say the status for the "Layout" Task of a shot is marked as 'Final'. It would be really nice to be able to have that automatically set the "Animation" Task status to 'ready to start' so the animator will know it's ready to start working on. Using the API and the EventLog, we can hook that up no problem...
First let's find the 'id' of the latest EventLogEntry to start watching from:
result = sg.find_one("EventLogEntry",filters=[], fields=['id'], order=[{'column':'created_at','direction':'desc'}])
current = result['id']Now lets select all of the EventLogEntries that have occurred since we last checked. Since we're just starting it may not find any right away but we'll be repeating this call continuously so events will happen quickly.
for event in sg.find("EventLogEntry",filters=[['id', 'greater_than', current]],
fields=['id','event_type','attribute_name','meta','entity'],
order=[{'column':'created_at','direction':'asc'}], filter_operator='all'):Next we'll check to see if this event is one we care about by looking at the event_type and the column (attribute_name) that is being changed
if event['event_type'] == 'Shotgun_Task_Change' and event['attribute_name'] == 'sg_status_list':We don't care about every status change, just ones that are changed to Final ('fin'). So if wasn't changed to Final, we'll just ignore it and move along.
if event['meta']['new_value'] != 'fin':
continueAt this point we know someone has changed the status on a Task to 'Final'. In order to see if this is a layout Task, we have to load the Task up to look at it closer.
task = sg.find_one("Task",filters=[['id', 'is', event['entity']['id']]], fields=['id','entity','content'])Now we can see this is a "Layout" Task. We need to make sure the Task is linked to something by checking the entity column, the entity is a Shot, and the Task name (content) is "Layout". If there's an animation Task for the same Shot, we then need to make sure it's not already being worked on, and then update it to 'Ready to Start' (rdy).
if task['entity'] and task['entity']['type'] == 'Shot' and task['content'] == 'Layout':
# find an animation task for the same Shot
task_filters = [
['entity', 'is', {'type':'Shot','id':task['entity']['id']} ],
['content', 'is', 'Animation']
]
animation_task = sg.find_one("Task",filters=task_filters, fields=['id','sg_status_list'])
# update it to 'rdy' if it's currently 'wtg'
if animation_task['sg_status_list'] == 'wtg':
sg.update("Task",entity_id=animation_task['id'], data={'sg_status_list' : 'rdy'})
log("updated animation task on %s %s to rdy" %
(task['entity']['type'],task['entity']['name']))In order to make this an effective trigger script, we want to wrap it all up in a never-ending loop so that it will keep running. It will find any events that have happened since we last checked, process them one at a time, keep track of the id of the current EventLogEntry we're processing, sleep for 1 second*, then check to see if there's been any new events since we last polled the EventLog, and so on... and so on... We can do that:
while True:
# all of the stuff above goes here
...
...
...
try:
current = event['id']
except NameError, e:
pass
sleep(1)*In order to use sleep() you will have to import it at the beginning of your script. You can see how in the complete example below. Also, if you're worried about bogging down the server by pinging it every second, don't fret. The query is very quick and lightweight and we've found it to have little to no effect on performance.
So we did that piece by piece and it's really not complicated at all, so lets put it all together with the script below. We've added some comments, a log() method which just echos statements out to STDOUT with a timestamp* and some messages to help with debugging just for this example.
*the log method requires you import the whole time module so we've added that here as well.
#!/usr/bin/env python
# ---------------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------------
# Imports
# ---------------------------------------------------------------------------------------------
from shotgun_api3 import Shotgun
from time import sleep
import time
from pprint import pprint # useful for debugging
# ---------------------------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------------------------
SERVER_PATH = 'https://awesomez.shotgunstudio.com' # make sure to change this to https if your studio uses it.
SCRIPT_USER = 'my_script'
SCRIPT_KEY = '27b65d7063f46b82e670fe807bd2b6f3fd1676c1'
def log(msg):
print time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()) +": "+msg
# ---------------------------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------------------------
if __name__ == '__main__':
sg = Shotgun(SERVER_PATH, SCRIPT_USER, SCRIPT_KEY)
# ---------------------------------------------------------------------------------------------
# get the id of the latest EventLogEntry
# ---------------------------------------------------------------------------------------------
result = sg.find_one("EventLogEntry",filters=[], fields=['id'], order=[{'column':'created_at','direction':'desc'}])
current = result['id']
log('beginning processing starting at event #%d' % current)
# ---------------------------------------------------------------------------------------------
# find all Events since the last check
# ---------------------------------------------------------------------------------------------
while True:
for event in sg.find("EventLogEntry",filters=[['id', 'greater_than', current]],
fields=['id','event_type','attribute_name','meta','entity'],
order=[{'column':'created_at','direction':'asc'}], filter_operator='all'):
log('processing event id %d' % event['id'])
# check if we care about the event
if event['event_type'] == 'Shotgun_Task_Change' and event['attribute_name'] == 'sg_status_list':
# Task status isn't 'final' so disregard...
if event['meta']['new_value'] != 'fin':
continue
# Load up the Task so we can check if we care about it
task = sg.find_one("Task",filters=[['id', 'is', event['entity']['id']]], fields=['id','entity','content'])
# Task is a Layout task for a Shot.
if task['entity'] and task['entity']['type'] == 'Shot' and task['content'] == 'Layout':
# find an animation task for the same Shot
task_filters = [
['entity', 'is', {'type':'Shot','id':task['entity']['id']} ],
['content', 'is', 'Animation']
]
animation_task = sg.find_one("Task",filters=task_filters, fields=['id','sg_status_list'])
# update it to 'rdy' if it's currently 'wtg'
if animation_task['sg_status_list'] == 'wtg':
sg.update("Task",entity_id=animation_task['id'], data={'sg_status_list' : 'rdy'})
log("updated animation task on %s %s to rdy" %
(task['entity']['type'],task['entity']['name']))
try:
current = event['id']
except NameError, e:
pass
sleep(1)This example shows you how to put all of the pieces together for simple triggering using the API, but it's not an advanced production-ready script yet. There's several things to consider that are beyond the scope of this introduction. Some of these topics include:
- daemonizing the script and making sure it's bulletproof and won't die randomly
- keeping track of the last id processed on disk so if you do stop the script, it will remember where it left off and continue at a later time without missing events
- log to a file instead of just using STDOUT
- ??? anything you want!
Shotgun executes destructive database queries in transactions and only writes to the EventLog when the transaction is complete. Because of this, it's possible that you may miss events using the "highest id" method here. We will be introducing a new more reliable trigger framework in the future. But in the meantime, one method of solving this possible issue, is to add an additional multi-entity column to the EventLogEntry entity type called "Processed" or something similar and link it to ScriptUsers. When your script processes an event, it should then update this field with it's own info to mark it as processed. By using a multi-entity field instead of say a checkbox, you can have multiple scripts acting as EventLog watchers and can each process events individually if need be. If you only have one script acting as your EventLog watcher, a checkbox or timestamp would suffice.
*Note that this may have noticeable impact on performance, we haven't thoroughly tested the performance implications of marking every event as processed so we recommend you test this first on a staging environment (if you have one).