The FilmHUB Python API allows for easy integration into Python (v2.7 & v3.4+) enabled third party applications.
Install using PIP:
pip install filmhub-python-apiDownload from git:
git clone https://github.com/filmhubsoftware/filmhub-python-apiThe Python API requires you to supply your FilmHUB domain, your username (E-mail) and either API key or password when creating a new session:
import filmhub_apisession = filmhub_api.Session(domain='acmefilm',username='john@user.com', api_key='f0a8b9a9-879f-4174-9d08-322eea196efc')or
session = filmhub_api.Session(domain='acmefilm',username='john@user.com', pwd='password')The following environment variables are picked up if not provided:
NOTES
filmhubcli user get_api_key" from your terminal/*nix shell.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”)}If an error occurs, an exception will be raised and the exception message can fetched afterwards by issuing:
print session.get_last_message()If you live on a network that does not have network access, the Python API can utilise a FilmHUB server acting as a proxy (refer to the FilmHUB manual on how to setup such a proxy). Simply supply proxy="<hostname or IP>:<port>" when creating session or set the FILMHUB_PROXY environment variable.
Three permission/clearance levels exists for FilmHUB users, the clearance dictates what data can be read and write through the API:
<root share>/filmhub/<user id (email)>.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",..].
Each entity has its own attributes, such as “id” or “code” (an FilmHUB 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 a job (entity), sending a single file to a user:
jobs = session.create("job",{ "source":"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, stored at relative paths in queue 'lowprio' with metadata:
jobs = session.create("job",{ "code":"thefilm_offline", "queue":"lowprio", "tasks":{ "0":{ "source":"share=lars@edit.com/thefilm/offline.zip", "destination":"lars@edit.com:thefilm/offline.zip" }, "1":{ "source":"share=lars@edit.com/thefilm/reference/moodboard_final.pdf", "destination":"lars@edit.com:thefilm/reference/moodboard_final.pdf" } }, "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":{ "0":{ "source":"share=projects/_REFS/pitch.mob", "destination":"site=london" } }})Create a job from JSON on disk:
job = session.create("job","/tmp/filmhub/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.
The FilmHUB 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)As mentioned, the query syntax is not as evolved as for example SQL. FilmHUB 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)')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 FilmHUB Python API does not support SQL-like atom transaction ending with a commit statement.
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.
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(..) }, .. ]}Failure scenarios:
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()):
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}Failure scenarios (will cause an exception to be thrown, obtain the message by calling 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)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 FilmHUB 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.
NOTE: Functionality described below is planned for v1.2 and might not make it to the final release v1.1 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()):
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()):
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()):
{“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).