Accsyn Python API Documentation

rev 1.006, last updated 19.11.24

Summary

The Accsyn Python API allows for easy integration into Python (v2.7 & v3.4+) enabled third party applications. With the API you can for example:

  • Create automated workflows that reacts on an event and then dispatches data (files/metadata) across the globe, in a safe and controlled environment.
  • Fetch and display Accsyn transfer jobs in your own user interface, also enabling control in terms of pause, resume and abort.
  • Handle shares automatically, allowing expose of project dedicated file areas with one or more external users, upon an event in your production systems.

This documentation covers basic API usage, more usage examples are displayed within domain web application beneath Help section @ https://<yourdomain>.accsyn.com.

For more ideas and inspiration, find tutorials at: support.accsyn.com.


How does it work?

The API act as a bridge between your application and the Accsyn HTTPS REST API, simplifying actions such as spawning a new file transfer, query and modify ongoing transfers. Admin tasks can also be performed using the API such as inviting users, creating shares and defining ACLs.


IMPORTANT NOTE: The Python API are not able to send files, it can only tell Accsyn to spawn a transfer between a server (typically identified by a domain, share and path) and a client (typically identified by a user name/E-mail and/or client ID/hostname). This means that the API can work independent of a locally installed file transfer client if the intention is to transfer files to and from the device API is running from.


Obtaining


Install using PIP:

pip install accsyn-python-api


Download from git:

git clone https://github.com/accsyn/accsyn-python-api



Create and test a session


Importing the Python package


The Python API requires you to supply your Accsyn domain, your username (E-mail) and either API key or password when creating a new session:

import accsyn_api



Creating the session


session = accsyn_api.Session(domain='acmefilm',username='john@user.com', api_key='f0a8b9a9-879f-4174-9d08-322eea196efc')

or

session = accsyn_api.Session(domain='acmefilm',username='john@user.com', pwd='password')


The following environment variables are picked up if set by python parent process:

  • "ACCSYN_DOMAIN" => "domain".
  • "ACCSYN_API_USER" => "username".
  • "ACCSYN_API_KEY" => "api_key".


NOTES

    • Your API key can be obtained online or from desktop app @ Prefs>Setup API environment, or by running "accsyn user get_api_key" from your terminal/*nix shell.
    • Remember to treat the API key as a secret password as it will grant access to all data on your Accsyn shared storage.
    • Session authenticated with password will expire within 10h, make sure to design your application to check for "session_expired:true" being supplied when a error message is supplied. In that case you will need to re-create your session and retry operation.
    • Add verbose=True to session creation if you want to see verbose debugging output.


Testing the session


print session.find_one("User")


Should output your user profile:


{
   "id":"5b0faf03304bfd4810dbd5fc",
   "code”:"john@user.com",
   "modified":datetime("2018-06-04 07:06:41.028619")
}


Error handling


If an error occurs, an exception will be raised and the exception message can fetched afterwards by issuing:

print session.get_last_message()


Network proxy


If you live on a network that does not have network access, the Python API can utilise a SOCKS (v4/v5) proxy of yours or an Accsyn daemon acting as a proxy (refer to the Accsyn admin manual on how to setup such a proxy).

SOCKS proxy

Supply proxy="socks:<hostname or IP>:<port>" when creating session or set the ACCSYN_PROXY environment variable.

Accsyn proxy

Supply proxy="accsyn:<hostname or IP>:<port>" when creating session or set the ACCSYN_PROXY environment variable.


Data types


Roles:


Three built-in "roles" (permission/clearance) levels exists for Accsyn users, the clearance dictates what data can be read and write through the API:

  • Admins; Are allowed to read and write all data – configure Accsyn.
  • Employees; Are allowed full access to jobs and data on all shares.
  • Users; (i.e. remote freelancers/customers) Besides receiving packages, they are only allowed to access share's explicitly given access through ACLs (Access Control Lists). Restricted users automatically is given a share @ default location - <root share>/filmhub/<user id (email)>.


Entities:


Data read from FilmHUB using the API arrives as JSON dictionaries, categorised by entity types:


To retrieve a list of all known entity types:

session.find("entitytypes")


This will return a list of entity types as string, i.e. ["user","organization","job",..].



Attributes:


Each entity has its own attributes, such as “id” or “code” (an Accsyn abbreviation for an unique “name”).


To retrieve a list of known attributes for an entity:

session.find('attributes WHERE entitytype=job')


This will return a list of attributes entities of that entity type can have, i.e. ["id","code","source","dest",...].


Note: Depending the clearance, some attributes might be not visible if accessing the API as an restricted user.



Create an entity


Creating an entity


Create a job (entity), sending a single file from "projects" share to a user:

jobs = session.create("job",{
    "source":"share=projects/thefilm/latest_edit.mov",
    "destination":"lars@edit.com"
})

This will return a list with job(s) created, with its new ID supplied, as would have been returned by a find_one query. If creation failed, an Exception will be thrown.


Create a job sending multiple files from a home share, with metadata and not touching the second file even if it differ in size and date, stored at relative paths in queue 'lowprio' with metadata:

jobs = session.create("job",{
    "code":"thefilm_offline",
    "queue":"lowprio",
    "tasks":[
        {
            "source":"share=lars@edit.com/thefilm/offline.zip",
            "destination":"lars@edit.com:thefilm/offline.zip"
        },
        {
            "source":"share=lars@edit.com/thefilm/reference/moodboard_final.pdf",
            "destination":"lars@edit.com:thefilm/reference/moodboard_final.pdf",
            "settings":{"transfer_ignore_existing":"file"}
        }
    ],
    "metadata":{
        "film":"thefilm"
    }
})


Another example sending a directory to a remote site, with an additional E-mail being sent to john@acmevfx.co.uk:

jobs = session.create("job",{
    "code":"thefilm_pitch",
    "email":"+john@acmevfx.co.uk",
    "tasks":[
        {
            "source":"share=projects/_REFS/pitch.mob",
            "destination":"site=london"
        }
    ]
})


Create a job from JSON on disk:

job = session.create("job","/tmp/accsyn/jobsubmit/A5F1D.json")

Print the results:

print session.str(session.find_one("job where id=%s"%job["id"]))

Note: The "str" API function is a convenience function that formats JSON nicely with indentation (i.e. json.dumps(dict, indent=4)).


Add a file(task) to an existing job:

session.create("task", {"tasks":["/Volumes/projects/creatures_showreel_2018.mov"]}, job["id"]))

Returns {"success":True} if everything goes well, None otherwise.

Add with new destination path:

session.create("task", {"tasks":[{"source":"share=projects/_REF/creatures_showreel_2018.mov","destination":"share=projects/TMP/creatures_showreel_2018_tmp.mov"}]}, job["id"])

Note: Files can only be added to site/remote office push/pull jobs as of current version.


For a complete job JSON syntax reference, please consult the Admin Manual, chapter "Job JSON syntax".


Query one or more entitities


The Accsyn python API accepts queries very similar to SQL queries as input, and return a single JSON dict or a list of JSON dicts as return value.

Note: as it might be similar to SQL, it does not fully support the SQL query syntax. Refer to examples in this reference to find out capabilities and limitations.


Get job named “my_transfer”:

session.find_one('Job WHERE code="thefilm_offline"') 


The job will be returned as a Python dict, None will be returned if no match was found/user have no permission to read the job.


Get a list with all jobs destined “lars@edit.com”, only bringing back attributes "id","code" and "status":

session.find('job WHERE dest=user:lars@edit.com', attributes=['id','code','status'])


The jobs will be returned as a Python list of dictionaries, list will only include job matching query and user have permission to read.

If find/find_one query fails, invalid syntax or other error, an Exception will be thrown.


Get a list of all share allowed access to:

session.find('share')


To print the JSON dict with indentation:

session.str(session.find_one('job WHERE status=failed'), indent=2)


Expressions


As mentioned, the query syntax is not as evolved as for example SQL. Accsyn API currently support AND/OR operations using the following syntax:

session.find('acl WHERE (ident=user:5bfeb0381da7ee4095fa217e AND target=share:ee5b4429-78e2-46ab-96cd-67a0be059e95)')


Updating an entity


Update a job entity, in this case rename a job with named "my_transfer" (ID: 5a7325f8b7ef72f5f9d74bf4) and also pause it:

job = session.find_one('job WHERE code="my_transfer"')
updated_job = session.update_one("job", job["id"], {
    "code":"my_transfer_1",
    "status":"paused"
})

Will return the job dict, as would have been returned by a find_one query. If update failed, an Exception will be thrown.


Note: The Accsyn Python API does not support SQL-like atom transaction ending with a commit statement.


Deleting an entity


To delete a job:

job = session.delete_one("job","5a7325f8b7ef72f5f9d74bf4")


Will return the job dict, as would have been returned by a find_one query. If deletion failed, an Exception will be thrown.


Tasks

A task is a file or directory that should be transferred within a job.

Query tasks

Job tasks are fetched by supplying the job ID query merged with task query:

session.find('task WHERE job.id=5a7325f8b7ef72f5f9d74bf4 AND status=executing')

Updating task(s)

Tasks are always updated in group with values supplied as a list of dicts instead of a single dict:

job = session.find_one('job WHERE code="my_transfer"')
updated_job = session.update_many("task", [{
    "id":"cc5f2afa-9ae4-46e0-9273-82ac802b20ff",
    "status":"onhold"
}], entityid=job["id"])

Will return the updated tasks, as would have been returned by a find query.


Settings

Settings are key="string value" entries on domain(global), queue, share, client or job. Settings are hierarchal which means that settings can overridden, for a example a queue setting can be overridden on a job and so on.

Important note:

Setting values are always on string form, and will required to be converted by you as appropriate.

Get settings

session.find('settings WHERE job.id=5a7325f8b7ef72f5f9d74bf4')

Will return a JSON containing the settings. To retrieve them merged upstream, supply upstream=True argument:

session.find('settings WHERE job.id=5a7325f8b7ef72f5f9d74bf4', upstream=True)



File operations

Besides working with entities, the API supports file operation on share level (admins, employees) or on a specific share (admins, employees and restricted users depending on ACLs).


List files on remote server

To list files on the share “thefilm-DIT”, subfolder TO_ACMEFILM:

session.ls("share=thefilm-DIT/TO_ACMEFILM")

Optional arguments:

  • recursive Do a recursive listing, True or False. Default False, see below.
  • maxdepth (Recursive listing) Limit number of levels to descend, positive integer greater than or equal 1.
  • directories_only Return only directories in listing, True or False. Default is False.
  • files_only Return only files in listing, True or False. Default is False. Note: Providing recursive=True won't have any affect.


If succeeds, this will return a list of dictionaries, one for each file found:

{
    "result": [
        {
            "type":1,
            "filename":"pitch.mov",
            "size":81632,
            "modified":datetime(..)
        },
        ..
    ]
}


  • Type; The type of file; 0 is directory/folder, 1 is a file.
  • Filename; The name of the file.
  • Size; The size of the file.
  • Modified; A Python datetime object holding the date file was last modified.


Failure scenarios:

  • Permission denied; An exception will be thrown.
  • Sahre/Path does not exists; An exception will be thrown, obtain the message by calling session.get_last_message().


By default, it does not dig down into sub directories. Add "recursive=True" to the call in order to have all (visible) files returned:

session.ls("share=thefilm-DIT", recursive=True)

If succeeds, this will return a list of dictionaries, one for each file found, with further descendant files in a list:

{
    "result": [
        {
            "filename":"TO_ACMEFILM",
            ..,
            "files":[
               {"filename":"A001_C001.mov",...},
               {...},
            ]
        },
        ..
    ]
}



Check if a file/directory exist

session.exists("workarea=thefilm-DIT/TO_ACMEFILM/final.mov")


If succeeds, this will return true or false:

{
    "result": True
}


Failure scenarios (will cause an exception to be thrown, obtain the message by calling session.get_last_message()):

  • Permission denied; you do not have read access to folder.
  • Server is down; server at organization is not online to perform the requested operation.
  • Share/Path does not exists;


Get size on disk

Get total size of file or all files beneath a directory:

session.getsize("share=thefilm-DIT/TO_ACMEFILM")

Optional arguments:

  • maxdepth Limit number of levels to descend, positive integer greater than or equal 1.


If succeeds, this will return the size of the file/directory:

{
    "result": 10462211
}



An exception will be thrown if listing fails. Possible reasons include:

  • Permission denied; you do not have read access to folder.
  • Server is down; server at organization is not online to perform the requested operation.
  • Share/Path does not exists;

Note: Obtain the most recent failure message by issuing: session.get_last_message()


Running file operation on multiple target directories in one call

In scenarios were you require speed and need to operate on multiple directories, you can provide a list of target path statements. Giving an example with 'ls' command:

session.ls(["share=thefilm-DIT/source","share=assets/exported"], recursive=True)

If succeeds, this will return a list of a list with dictionaries, one for each path and file found, with further descendant files in a list:

{
    "result": [
        {
            [
                {
                    "path":"share=thefilm-DIT/source",
                    "result": {
                        "filename":"TO_ACMEFILM",
                        ..,
                        "files":[
                        {"filename":"A001_C001.mov",...},
                        {...},
                        ]
                     },
                     ..
                },
                {
                    "path":"share=assets/exported",
                    "result": {
                        ..
                    }
                }
            ]
        }
    ]
}


If you need different arguments for each path statement to override the global, supply as dicts with these set:

session.ls([
   {
      "path":"share=thefilm-DIT/source",
      "recursive":True
   },
   {  
      "path":"share=thefilm-DIT/render",
      "maxdepth": 3
   }
], recursive=False)


Publishing

The Accsyn publish workflow is an extended upload mechanism allowing you to check the files before upload, decide where the should be written and allow user to input metadata. It is described in detail in this tutorial: Tutorial - Publish workflow.

The Python API supports the publish workflow and can be used for building your own client-side tools.

Pre publish

Supply the list of file-/directory names that should be checked at server end, supplying an unique ID and sub files-/directories as necessary. The ID should be a uuid4 and is required for Accsyn to relate each publish entry with each task on job submit later on :

result = session.publish([
   {
      "id":"33d59998-c980-4b82-9f6d-06ce27201d26",
      "filename":"first_file",
      "size":10240,
      "is_dir":false
   },
   {  
      "id":"dda32b0f-36e5-4a5f-9379-fdfabfd482e1",
      "filename":"second_directory",
      "is_dir":true,
      "files":[{
         "filename":"file.0001.dat",
         "size":1000000
      }]
   }
])


Which will return back the same list with additional entries appended by your pre-publish hook:

{
   "files":[{
      "id":"33d59998-c980-4b82-9f6d-06ce27201d26",
      "filename":"first_file",
      "size":10240,
      "is_dir":false,
      "warning":"Will be uploaded but now published",
      "can_upload":true,
   },
   {  
      "id":"dda32b0f-36e5-4a5f-9379-fdfabfd482e1",
      "filename":"second_directory",
      "is_dir":true,
      "files":[{
         "filename":"file.0001.dat",
         "size":1000000
      }],
      "ident":"abcd1234",
      "can_publish":true,
      "info":"Verified publish, please enter comment and time report"
   }],
   "time_report":true,
   "comment":true,
   "guidelines":".."
}

or throw an exception if something went wrong.

Then submit job as you would normally, with this publish data supplied as a hook and corresponding ID:s supplied to tasks aswell:

jobs = session.create("job",{
    "code":"abcd1234_publish",
    "tasks":[
        {
            "id":"33d59998-c980-4b82-9f6d-06ce27201d26",
            "source":"/Volumes/NAS01/first_file",
        },
        {
            "id":"dda32b0f-36e5-4a5f-9379-fdfabfd482e1",
            "source":"/Volumes/NAS01/second_directory",
        }
    ],
    "hooks":[
        {
           "when":"hook-job-publish-server",
           "data":{"files":result}
        }
    ]
})

This will cause hook to be run after files has been transferred to the correct location at server, as supplied by pre publish hooks.


Misc


Retreive your API key:

session.get_api_key()

Will return a string with your API key.

Note: The API KEY should be treated as a secret password and not to be shared with other users as they would gain access to files on your shares. The API KEY can be discarded and re-generated from the prefs section within the Accsyn app.


Check if GUI or server is running:

(since 1.1.4)
session.gui_is_running()
session.server_is_running()

Will return True if GUI/Server is running, False if offline, None if not installed/detected.


Administrating Accsyn using the API

Basic administration can be done using the API, this guide will not cover this as it can be found within the web application/admin portal beneath the Help section at each page @ https://<yourdomain>.accsyn.com.



NOTE: Functionality described below is planned for v1.3 and might not make it to the final release v1.2 if not requested.


Create directory

To create directory “__UPLOAD” at the share “projects”:

session.mkdir("share=projects/__UPLOAD")

Will return ; {"result":true} if successful, otherwise an exception to be thrown, obtain the message by calling session.get_last_message()):

  • Permission denied; You do not have write access to the parent folder.
  • Parent directory does not exist;
  • Directory already exists;


Rename/move a file or directory

Move file “share=thefilm-DIT/TO_ACMEFILM/pitch.mov” to “share=thefilm-DIT/TO_ACMEFILM/QT/pitch.mov”:

session.mv("share=thefilm-DIT/TO_ACMEFILM/pitch.mov","share=thefilm-DIT/TO_ACMEFILM/QT/pitch.mov")

If rename went well {“result”:true} will be returned, otherwise an exception to be thrown, obtain the message by calling session.get_last_message()):

  • Permission denied; You do not have read access to the source file/folder or do not have write access to the destination file/folder.
  • Source file/directory does not exist.
  • Destination parent directory does not exist.


Delete a file or directory

WARNING: Automising file removal through API calls can cause unwanted directories to be deleted, always test/dry run your calls before you put them into production!

Remove the directory “share=thefilm-DIT/TO_ACMEFILM/QT”:

session.rm("share=thefilm-DIT/TO_ACMEFILM/QT")

Will return {“result”:true} if successful, otherwise an exception to be thrown, obtain the message by calling session.get_last_message()):

  • Permission denied; You do not have write access to the file/folder that is to be deleted.
  • If target is a directory and contains files, exception will say: {“message”:”Cannot delete non-empty directory 'share=thefilm-DIT/TO_ACMEFILM/QT'!”}. To have it deleted anyway, supply the force flag: session.delete("share=thefilm-DIT/TO_ACMEFILM/QT",force=True).
  • The removal failed to to locked files or other permission problems on server. Contact domain adminstrator.