Developer Quick Start

Note if you are viewing this on github, you may need to view it on Google Colab Open In GoogleColab

Introduction

This Developer Quick Start guide tries to demonstrate

  • How to integrate SEPT into your existing application

  • Building a simple Qt application that uses SEPT and Shotgun to build output paths

Common SEPT Concepts

Before continuing with this tutorial, there are a few components of SEPT that you should be aware of.

PathTemplateParser

This is the main driver of SEPT. You will instantiate this class and pass it your template strings to validate and construct sept.Template objects from.

You can also output html documentation data from this class based on the state of Token and Operator classes configured in your environment.

Token

This is a code based resolver that will create a keyword for the User to type and contains logic for resolving that value out of a dictionary of data.
SEPT starts with a basic DefaultToken which will automatically resolve any Token for top level keys in your dictionary.
This means that if you have the data {"name": "Alex"}, your User can use the “name” Token without you having to write any code. Your custom Token classes need to have a name attribute which is the keyword the User can type and a method called getValue(data: dict) -> str|None that resolves the correct value from the dictionary.

Operator

This is a way to allow the User to mutate data from your Token classes. It’s implementation is a bit beyond the scope of this document. SEPT comes with several Operator classes out of the box.

Sample Qt Application

In this tutorial we are going to create a simple Qt4 based dialog that gives the user the ability to edit a SEPT template and get live feedback.
In addition to being able to see their updates in realtime, we will also provide a help documentation dialog that will be automatically generated based on the Token objects we expose to the user.

An example of what our application will look like when you are finished is shown below:

Finished Qt Application from the Sample Qt Application Developer Tutorial

Prerequisites

You should have an intermediate understanding of using the Qt Gui framework to continue through this part of the tutorial.
If you have experience with C++ Qt4, PySide, PySide2, PyQt4, or PyQt5 your knowledge should transfer over.

Starting off our application, we will need to install any dependencies.

[ ]:
!apt-get install -y xvfb python-opengl maim > /dev/null 2>&1
!pip install PySide2 sept pyvirtualdisplay pyscreenshot


[ ]:
import os, time
os.environ["QTWEBENGINE_DISABLE_SANDBOX"] = "1"

from IPython.display import display
from pyvirtualdisplay import Display
from PIL import Image, ImageChops
import pyscreenshot as ImageGrab


def autocrop(im, bgcolor):
    """Crop borders off an image.
        :param im: Source image.
        :param bgcolor: Background color, using either a color tuple.
        :return: An image without borders, or None if there's no actual content in the image.
    """
    if im.mode != "RGB":
        im = im.convert("RGB")
    bg = Image.new("RGB", im.size, bgcolor)
    diff = ImageChops.difference(im, bg)
    bbox = diff.getbbox()
    if bbox:
        return im.crop(bbox)
    return None  # no contents


virtual_display = Display(size=(1024, 768), color_depth=24)
virtual_display.start()
print("Display is {}".format(virtual_display.new_display_var))
os.environ["DISPLAY"] = virtual_display.new_display_var

from PySide2 import QtWidgets, QtCore
application = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])


def register_screenshot(widget):
    def trigger_screenshot():
        application.processEvents()
        img = ImageGrab.grab(backend="maim")

        img = autocrop(img, virtual_display._bgcolor)

        display(img)
        widget.close()
        application.quit()
    return trigger_screenshot


def run_gui(widget):
    try:
        from IPython.lib.guisupport import start_event_loop_qt4
        QtCore.QTimer.singleShot(1000, register_screenshot(widget))
        start_event_loop_qt4(application)
    except ImportError:
        application.exec_()

Building Code

We are going to start working on our documentation widget which will be a simple QWebView with html data populated by our SEPT parser object.

documentation_widget.py

[ ]:
# documentation_widget.py
from PySide2 import QtWidgets, QtWebEngineWidgets

class DocumentationWidget(QtWidgets.QWidget):
    def __init__(self, sept_parser, parent=None):
        super(DocumentationWidget, self).__init__(parent)
        self.resize(500, 500)
        self.sept_parser = sept_parser
        self.webview = QtWebEngineWidgets.QWebEngineView()
        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().addWidget(self.webview)

    def refreshDocumentation(self):
        token_html = self.sept_parser.token_documentation()
        operator_html = self.sept_parser.operator_documentation()
        self.webview.setHtml(token_html + operator_html)

We have created a fairly basic QWidget object that contains a single QWebView that we can set the html data on from the sept.PathTemplateParserdocumentation methods token_documentation and operator_documentation.

If we want to display this widget, we can add the following code at the bottom of documentation_widget.py

[ ]:
if __name__ == "__main__":
    from sept import PathTemplateParser
    parser = PathTemplateParser()
    doc_widget = DocumentationWidget(
        sept_parser=parser
    )
    doc_widget.show()
    doc_widget.refreshDocumentation()
    run_gui(doc_widget)

This will popup a dialog that should look similar to this
Basic Documentation Qt Widget

You should be able to scroll through the page as if it was a webpage (spoilers it is).

Now it feels a little bit silly to have our Token documentation sitting on top of our Operator documentation.
Lets separate these out into a separate tabs.
To do this, we can change our DocumentationWidget into a QTabWidget and create a separate QWebView for each tab.
Let’s first change our base class from a QWidget to QTabWidget
[ ]:
# documentation_widget.py
from PySide2 import QtWidgets

class DocumentationWidget(QtWidgets.QTabWidget):
    def __init__(self, sept_parser, parent=None):
        super(DocumentationWidget, self).__init__(parent)
        self.sept_parser = sept_parser
        ...

Now that we are a QTabWidget instead of a QWidget we should be adding our QWebView via QTabWidget.addTab instead of adding it to the layout directly.

[ ]:
# documentation_widget.py
from PySide2 import QtWidgets, QtWebEngineWidgets

class DocumentationWidget(QtWidgets.QTabWidget):
    def __init__(self, sept_parser, parent=None):
        super(DocumentationWidget, self).__init__(parent)
        self.sept_parser = sept_parser
        self.webview = QtWebEngineWidgets.QWebEngineView()
        # self.setLayout(QtWidgets.QVBoxLayout())
        # self.layout().addWidget(self.webview)
        self.addTab(self.webview, "Tokens")
        ...

Now to separate out our documentation into two tabs is fairly simple, we just create two web views and call setHtml on each of them individually.

[ ]:
# documentation_widget.py
from PySide2 import QtWidgets, QtWebEngineWidgets

class DocumentationWidget(QtWidgets.QTabWidget):
    def __init__(self, sept_parser, parent=None):
        super(DocumentationWidget, self).__init__(parent)
        self.resize(500, 500)
        self.sept_parser = sept_parser
        self.token_webview = QtWebEngineWidgets.QWebEngineView()
        self.operator_webview = QtWebEngineWidgets.QWebEngineView()
        self.addTab(self.token_webview, "Tokens")
        self.addTab(self.operator_webview, "Operator")
        self.setCurrentIndex(1)

    def refreshDocumentation(self):
        token_html = self.sept_parser.token_documentation()
        operator_html = self.sept_parser.operator_documentation()
        self.token_webview.setHtml(token_html)
        self.operator_webview.setHtml(operator_html)

[ ]:
if __name__ == "__main__":
    from sept import PathTemplateParser
    parser = PathTemplateParser()
    doc_widget = DocumentationWidget(
        sept_parser=parser
    )
    doc_widget.show()
    doc_widget.refreshDocumentation()
    run_gui(doc_widget)

You should now have a dialog that looks similar to this

Finished Documentation Qt Widget

If your dialog is similar you should be ready to move on to the next step.

Our second part of this example is some widget that will render a preview of the resulting sept.Template object.

We will build this off of a QPlainTextEdit widget.

[ ]:
# preview_widget.py
from PySide2 import QtWidgets

from sept import errors


class PreviewWidget(QtWidgets.QPlainTextEdit):
    def __init__(self, sept_parser, examples, text=None, parent=None):
        super(PreviewWidget, self).__init__(text, parent)
        self.setReadOnly(True)
        self.sept_parser = sept_parser
        self.examples = examples
        self.setEnabled(False)

    def preview_template_str(self, template_str):
        previews = []
        for example in self.examples:
            try:
                template = self.sept_parser.validate_template(template_str)
                output_path = template.resolve(example)
            except errors.ParsingError as err:
                print(str(err))
                return
            previews.append(output_path)
        text = "\n".join(previews)
        self.setPlainText(text)

[ ]:
if __name__ == "__main__":
    from sept import PathTemplateParser
    parser = PathTemplateParser()
    examples = [
        {
            "Project": "HeroJourney",
            "sequence": "Battle",
            "shot": "001",
            "step": "comp",
            "version": 2,
            "extension": "mov",
        }
    ]
    preview_widget = PreviewWidget(
        sept_parser=parser,
        examples=examples
    )
    preview_widget.show()
    preview_widget.preview_template_str(
        "{{sequence}}_{{shot}}_v{{pad[3,0]:version}}.{{extension}}"
    )
    run_gui(preview_widget)

This will popup a dialog that should look similar to this
Only Preview Qt Widget

You won’t be able to do anything because we haven’t added an input text box yet.

Next thing to do would be adding an input box so that we can type our own template str in.

We are going to make a QWidget class that will contain our input widget and our PreviewWidget.

[ ]:
# preview_widget.py


class PreviewInputWidget(QtWidgets.QWidget):
    def __init__(self, sept_parser, examples, parent=None):
        super(PreviewInputWidget, self).__init__(parent)

        self.input_widget = QtWidgets.QLineEdit(self)
        self.preview_widget = PreviewWidget(
            sept_parser=sept_parser,
            examples=examples,
            parent=self
        )

        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().addWidget(self.input_widget)
        self.layout().addWidget(self.preview_widget)

[ ]:
if __name__ == "__main__":
    from sept import PathTemplateParser
    parser = PathTemplateParser()
    examples = [
        {
            "Project": "HeroJourney",
            "sequence": "Battle",
            "shot": "001",
            "step": "comp",
            "version": 2,
            "extension": "mov",
        }
    ]
    preview_widget = PreviewInputWidget(
        sept_parser=parser,
        examples=examples
    )
    preview_widget.show()
    # preview_widget.preview_template_str(
    #     "{{sequence}}_{{shot}}_v{{pad[3,0]:version}}.{{extension}}"
    # )
    run_gui(preview_widget)

Now we have a dialog that also contains an input
Unlinked Input Qt Widget

However…When we type into the input nothing happens, we need to connect some Qt signals.

We want to listen to the textChanged signal from our QLineEdit and link that to our PreviewWidget.preview_template_str method.
To do that we need to first make preview_template_str a slot and then add a connect call.
[ ]:
# preview_widget.py
from PySide2 import QtWidgets, QtCore

from sept import errors


class PreviewWidget(QtWidgets.QPlainTextEdit):
    def __init__(self, sept_parser, examples, text=None, parent=None):
        super(PreviewWidget, self).__init__(text, parent)
        self.setReadOnly(True)
        self.sept_parser = sept_parser
        self.examples = examples
        self.setEnabled(False)

    # Turn preview_template_str into a Qt Slot
    @QtCore.Slot(str)
    def preview_template_str(self, template_str):
        previews = []
        for example in self.examples:
            try:
                template = self.sept_parser.validate_template(template_str)
                output_path = template.resolve(example)
            except errors.ParsingError as err:
                print(str(err))
                return
            previews.append(output_path)
        text = "\n".join(previews)
        self.setPlainText(text)

class PreviewInputWidget(QtWidgets.QWidget):
    def __init__(self, sept_parser, examples, parent=None):
        super(PreviewInputWidget, self).__init__(parent)

        self.input_widget = QtWidgets.QLineEdit(self)
        self.preview_widget = PreviewWidget(
            sept_parser=sept_parser,
            examples=examples,
            parent=self
        )
        # Add our connect line here
        self.input_widget.textChanged.connect(
            self.preview_widget.preview_template_str
        )

        self.setLayout(QtWidgets.QVBoxLayout())
        self.layout().addWidget(self.input_widget)
        self.layout().addWidget(self.preview_widget)
[ ]:
if __name__ == "__main__":
    from sept import PathTemplateParser
    parser = PathTemplateParser()
    examples = [
        {
            "Project": "HeroJourney",
            "sequence": "Battle",
            "shot": "001",
            "step": "comp",
            "version": 2,
            "extension": "mov",
        }
    ]
    preview_widget = PreviewInputWidget(
        sept_parser=parser,
        examples=examples
    )
    preview_widget.show()
    preview_widget.input_widget.setText(
        "{{sequence}}_{{shot}}_v{{pad[3,0]:version}}.{{extension}}"
    )
    run_gui(preview_widget)


Perfect, now our widgets should talk to each other!
Linked Input Qt Widget
Now that we have our two main components, we can put them together into an application.
In addition to making a containing dialog for these widgets, we are going to define some custom Token classes and demonstrate our DocumentationWidget automatically updating.
[ ]:
# sept_sample_dialog.py
from PySide2 import QtWidgets

from sept import PathTemplateParser


# from .preview_widget import PreviewInputWidget
# from .documentation_widget import DocumentationWidget


class SeptSampleDialog(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(SeptSampleDialog, self).__init__(parent)

        self.sept_parser = PathTemplateParser()

        self.setLayout(QtWidgets.QHBoxLayout())

        self.preview_widget = PreviewInputWidget(
            sept_parser=self.sept_parser,
            examples=self.get_examples(),
            parent=self
        )
        self.preview_widget.setMinimumWidth(250)
        self.preview_widget.input_widget.setText(
            "{{sequence}}_{{shot}}_v{{pad[3,0]:version}}.{{extension}}"
        )
        self.documentation_widget = DocumentationWidget(
            sept_parser=self.sept_parser
        )
        self.documentation_widget.refreshDocumentation()

        self.layout().addWidget(self.preview_widget)
        self.layout().addWidget(self.documentation_widget)

    def get_examples(self):
        return [
            {
                "Project": "HeroJourney",
                "sequence": "Battle",
                "shot": "001",
                "step": "comp",
                "version": 2,
                "extension": "mov",
            },
            {
                "Project": "HeroJourney",
                "sequence": "Battle",
                "shot": "002",
                "step": "paint",
                "version": 1,
                "extension": "mov",
            }
        ]


if __name__ == "__main__":
    dialog = SeptSampleDialog()
    dialog.show()
    run_gui(dialog)
Perfect! This is our dialog all put together.
Qt Widget Missing Custom Tokens

We will now modify the data so that we need to use a custom Token to provide data.

In production, you can turn off the DefaultToken and only allow Token resolving for your explicitly defined Token types.

[ ]:

class SeptSampleDialog(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(SeptSampleDialog, self).__init__(parent)

        self.sept_parser = PathTemplateParser(
            # We can pass our custom Token classes to the Parser.
            additional_tokens=self.get_custom_tokens()
        )

        self.setLayout(QtWidgets.QHBoxLayout())

        self.preview_widget = PreviewInputWidget(
            sept_parser=self.sept_parser,
            examples=self.get_examples(),
            parent=self
        )
        self.preview_widget.setMinimumWidth(250)
        self.preview_widget.input_widget.setText(
            "Name is {{lastname}}, {{firstname}} {{lastname}}"
        )
        self.documentation_widget = DocumentationWidget(
            sept_parser=self.sept_parser
        )
        self.documentation_widget.refreshDocumentation()

        self.layout().addWidget(self.preview_widget)
        self.layout().addWidget(self.documentation_widget)

    def get_custom_tokens(self):
        # We can create two new classes that inherit from `sept.Token`
        # We need to make sure that we set `name`, `getValue` and
        #   provide a useful docstring.
        from sept import Token

        class FirstnameToken(Token):
            """
            The <code>firstname</code> Token will return the "user.first" value from the data dictionary.
            """
            name = "firstname"
            def getValue(self, data):
                return data.get("user", {}).get("first")

        class LastnameToken(Token):
            """
            The <code>lastname</code> Token will return the "user.last" value from the data dictionary.
            """
            name = "lastname"
            def getValue(self, data):
                return data.get("user", {}).get("last")

        return [FirstnameToken, LastnameToken]

    def get_examples(self):
        return [
            {
                "Project": "HeroJourney",
                "sequence": "Battle",
                "shot": "001",
                "step": "comp",
                "version": 2,
                "extension": "mov",
                "user": {
                    "first": "James",
                    "last": "Bond",
                }
            },
            {
                "Project": "HeroJourney",
                "sequence": "Battle",
                "shot": "002",
                "step": "paint",
                "version": 1,
                "extension": "mov",
                "user": {
                    "first": "Alex",
                    "last": "Hughes",
                }
            }
        ]


if __name__ == "__main__":
    dialog = SeptSampleDialog()
    dialog.show()
    run_gui(dialog)

You can now see that we have our two custom Token classes automatically added to the DocumentationWidget
Finished Qt Widget
This is the end of the sample Qt Application tutorial.
The components that you created today are similar to Qt components that have been created in the sept_qt repository, however these are simple examples of the widgets you would want to use in production.
The widgets in sept_qt contain additional features like helpful error messages and error highlighting.