Write script extensions - DLR-SC/DataFinder GitHub Wiki

Motivation

The idea behind developing a script API for DataFinder is, that it should be easy for any user to extend the DataFinder and make it suitable for a desired situation. The API is built upon the core layer of the DataFinder and masks it, it is the interface to the core layer. Only the most needed functions and operations for items and their properties are supported. Also the DataFinder objects are encapsulated and represented by Python objects. Using the API makes it easy for a developer to implement new features, without having further knowledge about the details of the implementation. For minor releases the script API will remain stable.

Setup Requirements to Develop Scripts

You can find a description of how to setup your developing environment here: Script Developing Environment

General Information for the Script API

The API has the same structure as the core layer of the DataFinder. It is extremely simplified and is restricted to the main functions. Furthermore, the DataFinder objects, such as Item, Properties and the Repository, are described with standard Python data types. They are explained in the following section.

For detailed information see the EpyDoc documentation

Item

In the script API the item is as a simple string, which represents the path information to the real item, implemented. Furthermore, the script API provides an ItemDescription class with with one can get additional information about the item.

Properties

Properties in the script API are dictionaries in the form of key-value pairs. The key corresponds to the PropertyIdentifier and the value to the value of that specific key. For further information, there is a PropertyDescription class available.

Repository

The repository is described via a RepositoryDescription class, which supplies the necessary information.

Script API Examples

In the following, there are code listings, which show how the script API is used. More examples can be found in the source code repository.

Connecting to a Repository and Canceling a Connection

import datetime
from decimal import Decimal

from datafinder.script_api.error import ItemSupportError, ScriptApiError
from datafinder.script_api import repository
from datafinder.script_api.item import item_support
from datafinder.script_api.properties import property_support

if __name__ ## "__main__":
    try:
        repo = repository.connectRepository("http://datafinder.dlr.de/repos/test/REL_2.0.0/data", 
                                            "http://datafinder.dlr.de/repos/test/REL_2.0.0/config",
                                            "guest", "guest")
    except SciptApiError:
        print "Cannot connect repository!"
    
    # ...

    try:
        repository.disconnectRepository(repo)  
    except ScriptApiError:
        print "Cannot disconnect repository!" 

Creating a Collection

collectionProperties = dict()
collectionProperties[u"____datatype____"] = u"Projekt"

try:
    item_support.createCollection("/User1/Projekt_Test", collectionProperties)
except ItemSupportError, error:
    print error.errorMessage  

Creating a Leaf

leafProperties = dict()
leafProperties[u"____datastorename____"] = u"Data Store_COPY"
leafProperties[u"____content.size____"] = Decimal("0")
leafProperties[u"____dataformat____"] = u"TEXT"
leafProperties[u"____contentcreationdatetime____"] = datetime.datetime.today()
leafProperties[u"____contentmodificationdatetime____"] = datetime.datetime.today()

try:
    item_support.createLeaf("/User1/Projekt_Test/test.txt", leafProperties)
except ItemSupportError, error:
    print error.errorMessage  

Creating a Link

try:
    item_support.createLink("/User2/Projekt_Blub/test.txt", "/User1/Projekt_Test/test.txt")
except ItemSupportError, error:
    print error.errorMessage  

Deleting an Item

try:
    item_support.delete("/User1/Projekt_Test/test.txt")
except ItemSupportError, error:
    print error.errorMessage

Copying/Moving of an Item

try:
    item_support.move("/User1/Projekt_Test/test.txt", "/User2/Projekt_Blub/test.txt")
    item_support.copy("/User2/Projekt_Blub/test.txt", "/User1/Projekt_Test/test.txt")
except ItemSupportError, error:
    print error.errorMessage

Reading the Properties of an Item

try:
    itemProperties = property_support.retrieveProperties("/User1/Projekt_Test/test.txt")

    for identifier, value in itemProperties.iteritems():
        print "PropertyIdentifier: %s Value: %s" % (identifier, value)
except ItemSupportError, error:
    print error.errorMessage

Adding Properties to an Item

properties = dict()
properties[u"____keyword____"] = u"testfile"
properties[u"____createdwith____"] = u"Notepad"

try:
    property_support.storeProperties("/User1/Projekt_Test/test.txt", properties)
except ItemSupportError, error:
    print error.errorMessage

Deleting Properties of an Item

propertiesForDeletion = list()
propertiesForDeletion.append(u"____keyword____")
propertiesForDeletion.append(u"____createdwith____")

try:
    property_support.deleteProperties("/User1/Projekt_Test/test.txt", propertiesForDeletion)
except ItemSupportError, error:
    print error.errorMessage

Defining and Adding Domain Properties

First, we define an Author domain object which describes the author by a name, a main address, and a list of additional addresses. The Author class uses further domain objects to express this information. The domain properties are bound to a specific type. The type information is expressed using the existing type classes (e.g., !DateTimeType, !StringType, !NumberType, ...). This allows us to use the existing value restrictions (e.g., minimum values, regular expression, ...) when declaring a domain property.

from datafinder.script_api.properties import DomainObject, DomainProperty, DomainObjectType, ListType, NumberType, StringType

class _City(DomainObject):    
    name = DomainProperty(StringType(), "", "City Name", "Name of the city.")
    zip = DomainProperty(StringType(), "", "ZIP", "The postal code of the city.")


class _Address(DomainObject):
    street = DomainProperty(StringType(), "", "Street", "This is the street name.")
    streetNumber = DomainProperty(NumberType(), 0, "Street Number", "This is the street number.")
    city = DomainProperty(DomainObjectType(_City), _City(), "City", "This is the city.")

    
class Author(DomainObject):
    name = DomainProperty(StringType(), None, "Name", "This is the author name")
    mainAddress = DomainProperty(DomainObjectType(_Address), _Address(), "Addresses", "The main address.")
    addresses = DomainProperty(ListType([DomainObjectType(_Address)]), 
                               None, "Addresses", "These are additional addresses.")

    def __init__(self, name="", mainAddress=None, addresses=None):
        domain.DomainObject.__init__(self)
        self.name = name
        if not mainAddress is None:
            self.mainAddress = mainAddress
        if not addresses is None:
            self.addresses = addresses
            
    @name.setValidate
    def _validateName(self):
        if self.name is None or len(self.name) ## 0:
            raise ValueError("Name should not be empty.")

Then, we bind the domain object to the property name author. Afterwards, we can add an author instance to an item and retrieve it again.

property_support.registerPropertyDefinition("author", DomainObjectType(Author))

author = Author("Me")
property_support.storeProperties("/Project", {"author": author})
retrievedAuthor = property_support.properties("/Project")["author"]
assert retrievedAuthor ## author

Embeding Script Extensions into the User Client

One of the major use cases of the script API is to extend the functionality of the user client. For that reason a user client specific additional script API exists which allows you to access certain GUI state securely. The functionality provided is also intended to remain stable along minor releases.

For detailed information see the EpyDoc documentation.

User client specfic script example

The following example demonstrates how a collection of the File System View is accessed and creates a single file below it.

import logging

from datafinder.gui.user import script_api # User client specific API
from datafinder.script_api.error import ItemSupportError
from datafinder.script_api.repository import setWorkingRepository
from datafinder.script_api.item.item_support import createLeaf


_log = logging.getLogger("script") # Ensures that logging output is sent to the Script Output Window

umr = script_api.unmanagedRepositoryDescription() # Represents local file system

if not umr is None:
    setWorkingRepository(umr)
    _log.info(script_api.currentSelection())  # Logs the cureent selected items of the table view
    _log.info(script_api.currentCollection()) # Logs the currently active collection

    _log.info("Creating test file test.txt..")
    script_api.lock(["/"]) # Locks the collection /

    try:
        createLeaf("/test.txt", dict())
    except ItemSupportError, error:
        _log.error(error.message)
    finally:
        script_api.unlock(["/"]) # Unlocks the collection /
        script_api.selectItem("/test.txt") # Selects the newly created file 
else:
    _log.error("Cannot access unmanaged repository.")

More examples can be found in the source code repository.

Script Extension Types

In context of the user client two different types of script extensions exist, simple scripts (.py files) and script collections (.tar files). Simple scripts extensions serve most purposes well, but if you want to add a library or configuration dialog to a script you have to use script collection.

Simple Scripts

Simple scripts are just plain python .py files with additional meta data. The meta data helps to display and identify the scripts. Meta data values have to be specified in the comment header of the script as follows:

# @title: Hello World!
# @description: Tries to create file.  
# @datatypes: File,Directory
# @dataformats: PDF,TEXT
# @icon: The_Icon_Name
# @version: 0.0.1

The following comment tags are supported:

Tag Name Description
@title Displayed title of the script.
@description Displayed usage description of the script.
@datatypes List of comma-separated data type names the script is associated with.
@dataformats List of comma-separated data format names the script is associated with.
@icon Name of the displayed icon.
@version 3-digit version number.
@automatic Enables automatic execution of the script on start of the application

If no data type or data format names are specified the script generally usable. If at least one data type or data format is specified the script can only be used when a collection with the specific data type or a file with the specific data format is highlighted.

The available data format names are listed below:

Data Format Descriptive Name Associated File Suffixes Data Format Name
MS Word doc WORD
MS Excel xls EXCEL
MS Powerpoint ppt POWERPOINT
MS Visio vsd VISIO
PDF pdf PDF
XML xml XML
HTML html HTML
Python py, pyc, pyd PYTHON
Binary bin BINARY
Text log, java, cpp, js, php, csv, ini, rtf TEXT
Zip zip, tar, 7z, bz2, rar ARCHIVE
Audio mpeg, x-waf, midi, ogg, wma AUDIO
Video mpeg, x-msvideo, quicktime, xvid VIDEO
Image jpeg, tiff, gif, png,eps, bmp IMAGE

Script Collections

If you have more then one script which depends on the same functionalities the best option is to create a script collection instead of duplicating the code in every script. Furthermore, script collections allow you to create your own configuration dialogs that integrate seamlessly into the user client. Script collections are simple tar files containing simple scripts with an additional file called SCRIPTS. This file contains a list of all available scripts in the tar file and could look as follows:

report1.py
report2.py

When loading such a tar archive only the simple scripts declared in file SCRIPTS are parsed. The content of such a tar archive named my_extension.tar could look as follows:

- report
    |--- common.py
    |--- report1.py
    |--- report2.py
    |--- SCRIPTS
    |--- PreferencesPage.py

In conjunction with the content of the file SCRIPTS listed above this means the following:

  • The script collection adds the simple scripts report1.py and report2.py to the user client.
  • Both report scripts can import functionalities from common.py using import statement import my_extension.common.
  • A preference page is added to the user client by directly executing script PreferencesPage.py.

You can automatically package a script collection using the build target package_script_extension.