Blender Kitsu: Set Custom Thumbnail during Playblast #77

Merged
Nick Alberelli merged 12 commits from feature/custom-playblast-thumbnails into main 2023-06-15 21:26:54 +02:00
18 changed files with 1044 additions and 141 deletions
Showing only changes of commit 1fec23b693 - Show all commits

View File

@ -2,11 +2,16 @@ from . import client as raw
from . import cache from . import cache
from . import helpers from . import helpers
try:
from . import events
except ImportError:
pass
from . import asset from . import asset
from . import casting from . import casting
from . import context from . import context
from . import entity
from . import edit from . import edit
from . import entity
from . import files from . import files
from . import project from . import project
from . import person from . import person
@ -16,7 +21,11 @@ from . import task
from . import user from . import user
from . import playlist from . import playlist
from .exception import AuthFailedException, ParameterException from .exception import (
AuthFailedException,
ParameterException,
NotAuthenticatedException,
)
from .__version__ import __version__ from .__version__ import __version__
@ -28,13 +37,30 @@ def set_host(url, client=raw.default_client):
raw.set_host(url, client=client) raw.set_host(url, client=client)
def log_in(email, password, client=raw.default_client): def log_in(
email,
password,
totp=None,
email_otp=None,
fido_authentication_response=None,
recovery_code=None,
client=raw.default_client,
):
tokens = {} tokens = {}
try: try:
tokens = raw.post( tokens = raw.post(
"auth/login", {"email": email, "password": password}, client=client "auth/login",
{
"email": email,
"password": password,
"totp": totp,
"email_otp": email_otp,
"fido_authentication_response": fido_authentication_response,
"recovery_code": recovery_code,
},
client=client,
) )
except ParameterException: except (NotAuthenticatedException, ParameterException):
pass pass
if not tokens or ( if not tokens or (
@ -46,6 +72,10 @@ def log_in(email, password, client=raw.default_client):
return tokens return tokens
def send_email_otp(email, client=raw.default_client):
return raw.get("auth/email-otp", params={"email": email}, client=client)
def log_out(client=raw.default_client): def log_out(client=raw.default_client):
tokens = {} tokens = {}
try: try:
@ -56,6 +86,24 @@ def log_out(client=raw.default_client):
return tokens return tokens
def refresh_token(client=raw.default_client):
headers = {"User-Agent": "CGWire Gazu %s" % __version__}
if "refresh_token" in client.tokens:
headers["Authorization"] = "Bearer %s" % client.tokens["refresh_token"]
response = client.session.get(
raw.get_full_url("auth/refresh-token", client=client),
headers=headers,
)
raw.check_status(response, "auth/refresh-token")
tokens = response.json()
client.tokens["access_token"] = tokens["access_token"]
return tokens
def get_event_host(client=raw.default_client): def get_event_host(client=raw.default_client):
return raw.get_event_host(client=client) return raw.get_event_host(client=client)

View File

@ -1 +1 @@
__version__ = "0.8.30" __version__ = "0.9.3"

View File

@ -124,7 +124,7 @@ def get_asset_by_name(project, name, asset_type=None, client=default):
def get_asset(asset_id, client=default): def get_asset(asset_id, client=default):
""" """
Args: Args:
asset_id (str): Id of claimed asset. asset_id (str): ID of claimed asset.
Returns: Returns:
dict: Asset matching given ID. dict: Asset matching given ID.
@ -270,6 +270,7 @@ def all_asset_types_for_project(project, client=default):
Returns: Returns:
list: Asset types from assets listed in given project. list: Asset types from assets listed in given project.
""" """
project = normalize_model_parameter(project)
path = "projects/%s/asset-types" % project["id"] path = "projects/%s/asset-types" % project["id"]
return sort_by_name(raw.fetch_all(path, client=client)) return sort_by_name(raw.fetch_all(path, client=client))
@ -291,7 +292,7 @@ def all_asset_types_for_shot(shot, client=default):
def get_asset_type(asset_type_id, client=default): def get_asset_type(asset_type_id, client=default):
""" """
Args: Args:
asset_type_id (str/): Id of claimed asset type. asset_type_id (str/): ID of claimed asset type.
Returns: Returns:
dict: Asset Type matching given ID. dict: Asset Type matching given ID.
@ -358,7 +359,7 @@ def remove_asset_type(asset_type, client=default):
def get_asset_instance(asset_instance_id, client=default): def get_asset_instance(asset_instance_id, client=default):
""" """
Args: Args:
asset_instance_id (str): Id of claimed asset instance. asset_instance_id (str): ID of claimed asset instance.
Returns: Returns:
dict: Asset Instance matching given ID. dict: Asset Instance matching given ID.

View File

@ -181,7 +181,6 @@ def cache(function, maxsize=300, expire=120):
@wraps(function) @wraps(function)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if is_cache_enabled(state): if is_cache_enabled(state):
key = get_cache_key(args, kwargs) key = get_cache_key(args, kwargs)

View File

@ -3,14 +3,10 @@ import functools
import json import json
import shutil import shutil
import urllib import urllib
import os
from .encoder import CustomJSONEncoder from .encoder import CustomJSONEncoder
if sys.version_info[0] == 3:
from json import JSONDecodeError
else:
JSONDecodeError = ValueError
from .__version__ import __version__ from .__version__ import __version__
from .exception import ( from .exception import (
@ -24,6 +20,13 @@ from .exception import (
UploadFailedException, UploadFailedException,
) )
if sys.version_info[0] == 3:
from json import JSONDecodeError
else:
JSONDecodeError = ValueError
DEBUG = os.getenv("GAZU_DEBUG", "false").lower() == "true"
class KitsuClient(object): class KitsuClient(object):
def __init__(self, host, ssl_verify=True, cert=None): def __init__(self, host, ssl_verify=True, cert=None):
@ -47,10 +50,7 @@ try:
requests.models.complexjson.dumps = functools.partial( requests.models.complexjson.dumps = functools.partial(
json.dumps, cls=CustomJSONEncoder json.dumps, cls=CustomJSONEncoder
) )
# Set host to "" otherwise requests.Session() takes a long time during Blender startup host = "http://gazu.change.serverhost/api"
# Whyever that is.
# host = "http://gazu.change.serverhost/api"
host = ""
default_client = create_client(host) default_client = create_client(host)
except Exception: except Exception:
print("Warning, running in setup mode!") print("Warning, running in setup mode!")
@ -77,9 +77,9 @@ def host_is_valid(client=default_client):
if not host_is_up(client): if not host_is_up(client):
return False return False
try: try:
post("auth/login", {"email": "", "password": ""}) post("auth/login", {"email": ""})
except Exception as exc: except Exception as exc:
return type(exc) == ParameterException return isinstance(exc, (NotAuthenticatedException, ParameterException))
def get_host(client=default_client): def get_host(client=default_client):
@ -196,6 +196,8 @@ def get(path, json_response=True, params=None, client=default_client):
Returns: Returns:
The request result. The request result.
""" """
if DEBUG:
print("GET", get_full_url(path, client))
path = build_path_with_params(path, params) path = build_path_with_params(path, params)
response = client.session.get( response = client.session.get(
get_full_url(path, client=client), get_full_url(path, client=client),
@ -216,6 +218,10 @@ def post(path, data, client=default_client):
Returns: Returns:
The request result. The request result.
""" """
if DEBUG:
print("POST", get_full_url(path, client))
if not "password" in data:
print("Body:", data)
response = client.session.post( response = client.session.post(
get_full_url(path, client), get_full_url(path, client),
json=data, json=data,
@ -237,6 +243,9 @@ def put(path, data, client=default_client):
Returns: Returns:
The request result. The request result.
""" """
if DEBUG:
print("PUT", get_full_url(path, client))
print("Body:", data)
response = client.session.put( response = client.session.put(
get_full_url(path, client), get_full_url(path, client),
json=data, json=data,
@ -253,6 +262,8 @@ def delete(path, params=None, client=default_client):
Returns: Returns:
The request result. The request result.
""" """
if DEBUG:
print("DELETE", get_full_url(path, client))
path = build_path_with_params(path, params) path = build_path_with_params(path, params)
response = client.session.delete( response = client.session.delete(

View File

@ -1,59 +1,147 @@
from blender_kitsu import gazu
from . import client as raw from . import client as raw
from .sorting import sort_by_name
from .cache import cache from .cache import cache
from .helpers import normalize_model_parameter from .helpers import normalize_model_parameter
default = raw.default_client default = raw.default_client
@cache
def get_all_edits(relations=False, client=default):
"""
Retrieve all edit entries.
"""
params = {}
if relations:
params = {"relations": "true"}
path = "edits/all"
edits = raw.fetch_all(path, params, client=client)
return sort_by_name(edits)
@cache @cache
def get_edit(edit_id, relations=False, client=default): def get_edit(edit_id, client=default):
"""
Retrieve all edit entries.
"""
edit_entry = normalize_model_parameter(edit_id)
params = {}
if relations:
params = {"relations": "true"}
path = f"edits/{edit_entry['id']}"
edit_entry = raw.fetch_all(path, params, client=client)
return edit_entry
@cache
def get_all_edits_with_tasks(relations=False, client=default):
"""
Retrieve all edit entries.
"""
params = {}
if relations:
params = {"relations": "true"}
path = "edits/with-tasks"
edits_with_tasks = raw.fetch_all(path, params, client=client)
return sort_by_name(edits_with_tasks)
@cache
def get_all_previews_for_edit(edit, client=default):
""" """
Args: Args:
edit_id (str): ID of claimed edit.
Returns:
dict: Edit corresponding to given edit ID.
"""
return raw.fetch_one("edits", edit_id, client=client)
@cache
def get_edit_by_name(project, edit_name, client=default):
"""
Args:
project (str / dict): The project dict or the project ID.
edit_name (str): Name of claimed edit.
Returns:
dict: Edit corresponding to given name and sequence.
"""
project = normalize_model_parameter(project)
return raw.fetch_first(
"edits/all",
{"project_id": project["id"], "name": edit_name},
client=client,
)
@cache
def get_edit_url(edit, client=default):
"""
Args:
edit (str / dict): The edit dict or the edit ID.
Returns:
url (str): Web url associated to the given edit
"""
edit = normalize_model_parameter(edit)
edit = get_edit(edit["id"])
path = "{host}/productions/{project_id}/"
if edit["episode_id"] is None:
path += "edits/{edit_id}/"
else:
path += "episodes/{episode_id}/edits/{edit_id}/"
return path.format(
host=raw.get_api_url_from_host(client=client),
project_id=edit["project_id"],
edit_id=edit["id"],
episode_id=edit["episode_id"],
)
def new_edit(
project,
name,
description=None,
data={},
episode=None,
client=default,
):
"""
Create an edit for given project (and episode if given).
Allow to set metadata too.
Args:
project (str / dict): The project dict or the project ID.
name (str): The name of the edit to create.
description (str): The description of the edit to create.
data (dict): Free field to set metadata of any kind.
episode (str / dict): The episode dict or the episode ID. episode (str / dict): The episode dict or the episode ID.
Returns: Returns:
list: Shots which are children of given episode. Created edit.
"""
project = normalize_model_parameter(project)
if episode is not None:
episode = normalize_model_parameter(episode)
data = {"name": name, "data": data, "parent_id": episode["id"]}
if description is not None:
data["description"] = description
edit = get_edit_by_name(project, name, client=client)
if edit is None:
path = "data/projects/%s/edits" % project["id"]
return raw.post(path, data, client=client)
else:
return edit
def remove_edit(edit, force=False, client=default):
"""
Remove given edit from database.
Args:
edit (dict / str): Edit to remove.
""" """
edit = normalize_model_parameter(edit) edit = normalize_model_parameter(edit)
edit_previews = (raw.fetch_all(f"edits/{edit['id']}/preview-files", client=client)) path = "data/edits/%s" % edit["id"]
for key in [key for key in enumerate(edit_previews.keys())]: params = {}
return edit_previews[key[1]] if force:
params = {"force": "true"}
return raw.delete(path, params, client=client)
def update_edit(edit, client=default):
"""
Save given edit data into the API. Metadata are fully replaced by the ones
set on given edit.
Args:
edit (dict): The edit dict to update.
Returns:
dict: Updated edit.
"""
return raw.put("data/entities/%s" % edit["id"], edit, client=client)
def update_edit_data(edit, data={}, client=default):
"""
Update the metadata for the provided edit. Keys that are not provided are
not changed.
Args:
edit (dict / ID): The edit dict or ID to save in database.
data (dict): Free field to set metadata of any kind.
Returns:
dict: Updated edit.
"""
edit = normalize_model_parameter(edit)
current_edit = get_edit(edit["id"], client=client)
updated_edit = {"id": current_edit["id"], "data": current_edit["data"]}
updated_edit["data"].update(data)
return update_edit(updated_edit, client=client)

View File

@ -29,36 +29,39 @@ def all_entity_types(client=default):
def get_entity(entity_id, client=default): def get_entity(entity_id, client=default):
""" """
Args: Args:
id (str, client=default): ID of claimed entity. entity_id (str): ID of claimed entity.
Returns: Returns:
dict: Retrieve entity matching given ID (It can be an entity of any dict: Retrieve entity matching given ID (it can be an entity of any
kind: asset, shot, sequence or episode). kind: asset, shot, sequence or episode).
""" """
return raw.fetch_one("entities", entity_id, client=client) return raw.fetch_one("entities", entity_id, client=client)
@cache @cache
def get_entity_by_name(entity_name, client=default): def get_entity_by_name(entity_name, project=None, client=default):
""" """
Args: Args:
name (str, client=default): The name of the claimed entity. name (str): The name of the claimed entity.
project (str, dict): Project ID or dict.
Returns: Returns:
Retrieve entity matching given name. Retrieve entity matching given name (and project if given).
""" """
return raw.fetch_first("entities", {"name": entity_name}, client=client) params = {"name": entity_name}
if project is not None:
project = normalize_model_parameter(project)
params["project_id"] = project["id"]
return raw.fetch_first("entities", params, client=client)
@cache @cache
def get_entity_type(entity_type_id, client=default): def get_entity_type(entity_type_id, client=default):
""" """
Args: Args:
id (str, client=default): ID of claimed entity type. entity_type_id (str): ID of claimed entity type.
, client=client Returns:
Returns: Retrieve entity type matching given ID (It can be an entity type of any
Retrieve entity type matching given ID (It can be an entity type of any kind).
kind).
""" """
return raw.fetch_one("entity-types", entity_type_id, client=client) return raw.fetch_one("entity-types", entity_type_id, client=client)
@ -77,6 +80,41 @@ def get_entity_type_by_name(entity_type_name, client=default):
) )
@cache
def guess_from_path(project_id, path, sep="/"):
"""
Get list of possible project file tree templates matching a file path
and data ids corresponding to template tokens.
Args:
project_id (str): Project id of given file
file_path (str): Path to a file
sep (str): File path separator, defaults to "/"
Returns:
list: dictionnaries with the corresponding entities and template name.
Example:
.. code-block:: text
[
{
'Asset': '<asset_id>',
'Project': '<project_id>',
'Template': 'asset'
},
{
'Project': '<project_id>',
'Template': 'instance'
},
...
]
"""
return raw.post(
"/data/entities/guess_from_path",
{"project_id": project_id, "file_path": path, "sep": sep},
)
def new_entity_type(name, client=default): def new_entity_type(name, client=default):
""" """
Creates an entity type with the given name. Creates an entity type with the given name.
@ -104,16 +142,3 @@ def remove_entity(entity, force=False, client=default):
if force: if force:
params = {"force": "true"} params = {"force": "true"}
return raw.delete(path, params, client=client) return raw.delete(path, params, client=client)
def update_entity(entity, client=default):
"""
Save given shot data into the API. Metadata are fully replaced by the ones
set on given shot.
Args:
Entity (dict): The shot dict to update.
Returns:
dict: Updated entity.
"""
return raw.put(f"data/entities/{entity['id']}", entity, client=client)

View File

@ -0,0 +1,71 @@
import sys
if sys.version_info[0] == 2:
raise ImportError(
"The events part of Gazu is not available for Python 2.7"
)
from .exception import AuthFailedException
from .client import default_client, get_event_host
from gazu.client import make_auth_header
import socketio
class EventsNamespace(socketio.ClientNamespace):
def on_connect(self):
pass
def on_disconnect(self):
pass
def on_error(self, data):
return connect_error(data)
def init(
client=default_client,
ssl_verify=False,
reconnection=True,
logger=False,
**kwargs
):
"""
Init configuration for SocketIO client.
Returns:
Event client that will be able to set listeners.
"""
params = {"ssl_verify": ssl_verify}
params.update(kwargs)
event_client = socketio.Client(
logger=logger, reconnection=reconnection, **params
)
event_client.on("connect_error", connect_error)
event_client.register_namespace(EventsNamespace("/events"))
event_client.connect(get_event_host(client), make_auth_header())
return event_client
def connect_error(data):
print("The connection failed!")
return data
def add_listener(event_client, event_name, event_handler):
"""
Set a listener that reacts to a given event.
"""
event_client.on(event_name, event_handler, "/events")
return event_client
def run_client(event_client):
"""
Run event client (it blocks current thread). It listens to all events
configured.
"""
try:
print("Listening to Kitsu events...")
event_client.wait()
except TypeError:
raise AuthFailedException
return event_client

View File

@ -79,7 +79,7 @@ class TooBigFileException(Exception):
pass pass
class TaskStatusNotFound(Exception): class TaskStatusNotFoundException(Exception):
""" """
Error raised when a task status is not found. Error raised when a task status is not found.
""" """
@ -91,3 +91,9 @@ class DownloadFileException(Exception):
""" """
Error raised when a file can't be downloaded. Error raised when a file can't be downloaded.
""" """
class TaskMustBeADictException(Exception):
"""
Error raised when a task should be a dict.
"""

View File

@ -1083,7 +1083,7 @@ def download_preview_file(preview_file, file_path, client=default):
file_path (str): Location on hard drive where to save the file. file_path (str): Location on hard drive where to save the file.
""" """
return raw.download( return raw.download(
get_preview_file_url(preview_file), get_preview_file_url(preview_file, client=client),
file_path, file_path,
client=client, client=client,
) )

View File

@ -1,5 +1,5 @@
import os
import sys import sys
import os
import re import re
import datetime import datetime
import shutil import shutil
@ -7,7 +7,7 @@ import requests
import tempfile import tempfile
import mimetypes import mimetypes
from .exception import DownloadFileException from gazu.exception import DownloadFileException
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
import urllib.parse as urlparse import urllib.parse as urlparse

View File

@ -38,15 +38,61 @@ def all_persons(client=default):
@cache @cache
def get_person(id, client=default): def get_time_spents_range(person_id, start_date, end_date, client=default):
"""
Gets the time spents of the current user for the given date range.
Args:
person_id (str): An uuid identifying a person.
start_date (str): The first day of the date range as a date string with
the following format: YYYY-MM-DD
end_date (str): The last day of the date range as a date string with
the following format: YYYY-MM-DD
Returns:
list: All of the person's time spents
"""
date_range = {
"start_date": start_date,
"end_date": end_date,
}
return raw.get(
"/data/persons/{}/time-spents".format(person_id),
params=date_range,
client=client,
)
def get_all_month_time_spents(id, date, client=default):
""" """
Args: Args:
id (str): An uuid identifying a person. id (str): An uuid identifying a person.
date (datetime.date): The date of the month to query.
Returns:
list: All of the person's time spents for the given month.
"""
date = date.strftime("%Y/%m")
return raw.get(
"data/persons/{}/time-spents/month/all/{}".format(id, date),
client=client,
)
@cache
def get_person(id, relations=False, client=default):
"""
Args:
id (str): An uuid identifying a person.
relations (bool): Whether to get the relations for the given person.
Returns: Returns:
dict: Person corresponding to given id. dict: Person corresponding to given id.
""" """
return raw.fetch_one("persons", id, client=client) params = {"id": id}
if relations:
params["relations"] = "true"
return raw.fetch_first("persons", params=params, client=client)
@cache @cache
@ -152,7 +198,7 @@ def new_person(
Returns: Returns:
dict: Created person. dict: Created person.
""" """
person = get_person_by_email(email) person = get_person_by_email(email, client=client)
if person is None: if person is None:
person = raw.post( person = raw.post(
"data/persons/new", "data/persons/new",

View File

@ -256,6 +256,7 @@ def add_metadata_descriptor(
project, project,
name, name,
entity_type, entity_type,
data_type="string",
choices=[], choices=[],
for_client=False, for_client=False,
departments=[], departments=[],
@ -278,6 +279,7 @@ def add_metadata_descriptor(
project = normalize_model_parameter(project) project = normalize_model_parameter(project)
data = { data = {
"name": name, "name": name,
"data_type": data_type,
"choices": choices, "choices": choices,
"for_client": for_client, "for_client": for_client,
"entity_type": entity_type, "entity_type": entity_type,
@ -292,7 +294,27 @@ def add_metadata_descriptor(
def get_metadata_descriptor(project, metadata_descriptor_id, client=default): def get_metadata_descriptor(project, metadata_descriptor_id, client=default):
""" """
Get a metadata descriptor matchind it's ID. Retrieve a the metadata descriptor matching given ID.
Args:
project (dict / ID): The project dict or id.
metadata_descriptor_id (dict / ID): The metadata descriptor dict or id.
Returns:
dict: The metadata descriptor matching the ID.
"""
project = normalize_model_parameter(project)
metadata_descriptor = normalize_model_parameter(metadata_descriptor_id)
return raw.fetch_one(
"projects/%s/metadata-descriptors" % project["id"],
metadata_descriptor["id"],
client=client,
)
def get_metadata_descriptor_by_field_name(project, field_name, client=default):
"""
Get a metadata descriptor matchind given project and name.
Args: Args:
project (dict / ID): The project dict or id. project (dict / ID): The project dict or id.
@ -302,10 +324,12 @@ def get_metadata_descriptor(project, metadata_descriptor_id, client=default):
dict: The metadata descriptor matchind the ID. dict: The metadata descriptor matchind the ID.
""" """
project = normalize_model_parameter(project) project = normalize_model_parameter(project)
metadata_descriptor = normalize_model_parameter(metadata_descriptor_id) return raw.fetch_first(
return raw.fetch_one( "metadata-descriptors",
"projects/%s/metadata-descriptors" % project["id"], params={
metadata_descriptor["id"], "project_id": project["id"],
"field_name": field_name,
},
client=client, client=client,
) )
@ -375,3 +399,46 @@ def remove_metadata_descriptor(
params, params,
client=client, client=client,
) )
def get_team(project, client=default):
"""
Get team for project.
Args:
project (dict / ID): The project dict or id.
"""
project = normalize_model_parameter(project)
return raw.fetch_all("projects/%s/team" % project["id"], client=client)
def add_person_to_team(project, person, client=default):
"""
Add a person to the team project.
Args:
project (dict / ID): The project dict or id.
person (dict / ID): The person dict or id.
"""
project = normalize_model_parameter(project)
person = normalize_model_parameter(person)
data = {"person_id": person["id"]}
return raw.post(
"data/projects/%s/team" % project["id"], data, client=client
)
def remove_person_from_team(project, person, client=default):
"""
Remove a person from the team project.
Args:
project (dict / ID): The project dict or id.
person (dict / ID): The person dict or id.
"""
project = normalize_model_parameter(project)
person = normalize_model_parameter(person)
return raw.delete(
"data/projects/%s/team/%s" % (project["id"], person["id"]),
client=client,
)

View File

@ -14,9 +14,9 @@ def new_scene(project, sequence, name, client=default):
""" """
project = normalize_model_parameter(project) project = normalize_model_parameter(project)
sequence = normalize_model_parameter(sequence) sequence = normalize_model_parameter(sequence)
shot = {"name": name, "sequence_id": sequence["id"]} scene = {"name": name, "sequence_id": sequence["id"]}
return raw.post( return raw.post(
"data/projects/%s/scenes" % project["id"], shot, client=client "data/projects/%s/scenes" % project["id"], scene, client=client
) )

View File

@ -114,7 +114,7 @@ def all_episodes_for_project(project, client=default):
def get_episode(episode_id, client=default): def get_episode(episode_id, client=default):
""" """
Args: Args:
episode_id (str): Id of claimed episode. episode_id (str): ID of claimed episode.
Returns: Returns:
dict: Episode corresponding to given episode ID. dict: Episode corresponding to given episode ID.
@ -205,7 +205,7 @@ def get_sequence_from_shot(shot, client=default):
def get_shot(shot_id, client=default): def get_shot(shot_id, client=default):
""" """
Args: Args:
shot_id (str): Id of claimed shot. shot_id (str): ID of claimed shot.
Returns: Returns:
dict: Shot corresponding to given shot ID. dict: Shot corresponding to given shot ID.

View File

@ -1,4 +1,15 @@
import os
from .helpers import normalize_model_parameter
from . import client as raw from . import client as raw
from . import asset as asset_module
from . import casting as casting_module
from . import person as person_module
from . import project as project_module
from . import files as files_module
from . import shot as shot_module
from . import task as task_module
from .helpers import normalize_model_parameter, validate_date_format from .helpers import normalize_model_parameter, validate_date_format
@ -6,7 +17,12 @@ default = raw.default_client
def get_last_events( def get_last_events(
page_size=20000, project=None, after=None, before=None, client=default page_size=20000,
project=None,
after=None,
before=None,
only_files=False,
client=default,
): ):
""" """
Get last events that occured on the machine. Get last events that occured on the machine.
@ -16,13 +32,13 @@ def get_last_events(
project (dict/id): Get only events related to this project. project (dict/id): Get only events related to this project.
after (dict/id): Get only events occuring after given date. after (dict/id): Get only events occuring after given date.
before (dict/id): Get only events occuring before given date. before (dict/id): Get only events occuring before given date.
only_files (bool): Get only events related to files.
Returns: Returns:
dict: Last events matching criterions. dict: Last events matching criterions.
""" """
path = "/data/events/last" path = "/data/events/last"
params = {"page_size": page_size} params = {"page_size": page_size, "only_files": only_files}
if project is not None: if project is not None:
project = normalize_model_parameter(project) project = normalize_model_parameter(project)
params["project_id"] = project["id"] params["project_id"] = project["id"]
@ -167,6 +183,447 @@ def get_id_map_by_id(source_list, target_list, field="name"):
def is_changed(source_model, target_model): def is_changed(source_model, target_model):
"""
Args:
source_model (dict): Model from the source API.
target_model (dict): Matching model from the target API.
Returns:
bool: True if the source model is older than the target model (based on
`updated_at` field)
"""
source_date = source_model["updated_at"] source_date = source_model["updated_at"]
target_date = target_model["updated_at"] target_date = target_model["updated_at"]
return source_date > target_date return source_date > target_date
def get_sync_department_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source departments ids with target department ids
"""
departments_source = person_module.all_departments(client=source_client)
departments_target = person_module.all_departments(client=target_client)
return get_id_map_by_id(departments_source, departments_target)
def get_sync_asset_type_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source asset type ids with target asset type ids
"""
asset_types_source = asset_module.all_asset_types(client=source_client)
asset_types_target = asset_module.all_asset_types(client=target_client)
return get_id_map_by_id(asset_types_source, asset_types_target)
def get_sync_project_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source project ids with target project ids
"""
projects_source = project_module.all_projects(client=source_client)
projects_target = project_module.all_projects(client=target_client)
return get_id_map_by_id(projects_source, projects_target)
def get_sync_task_type_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source task type ids with target task type ids
"""
task_types_source = task_module.all_task_types(client=source_client)
task_types_target = task_module.all_task_types(client=target_client)
return get_id_map_by_id(task_types_source, task_types_target)
def get_sync_task_status_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source task status ids with target task status
ids
"""
task_statuses_source = task_module.all_task_statuses(client=source_client)
task_statuses_target = task_module.all_task_statuses(client=target_client)
return get_id_map_by_id(task_statuses_source, task_statuses_target)
def get_sync_person_id_map(source_client, target_client):
"""
Args:
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: A dict matching source person ids with target person ids
"""
persons_source = person_module.all_persons(client=source_client)
persons_target = person_module.all_persons(client=target_client)
return get_id_map_by_id(persons_source, persons_target, field="email")
def push_assets(project_source, project_target, client_source, client_target):
"""
Copy assets from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed assets
"""
asset_types_map = get_sync_asset_type_id_map(client_source, client_target)
task_types_map = get_sync_task_type_id_map(client_source, client_target)
assets = asset_module.all_assets_for_project(
project_source, client=client_source
)
for asset in assets:
asset["entity_type_id"] = asset_types_map[asset["entity_type_id"]]
if asset["ready_for"] is not None:
asset["ready_for"] = task_types_map[asset["ready_for"]]
asset["project_id"] = project_target["id"]
return import_entities(assets, client=client_target)
def push_episodes(
project_source, project_target, client_source, client_target
):
"""
Copy episodes from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Args:
project_source (dict): The project to get episodes from
project_target (dict): The project to push episodes to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed episodes
"""
episodes = shot_module.all_episodes_for_project(
project_source, client=client_source
)
for episode in episodes:
episode["project_id"] = project_target["id"]
return import_entities(episodes, client=client_target)
def push_sequences(
project_source, project_target, client_source, client_target
):
"""
Copy sequences from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Args:
project_source (dict): The project to get sequences from
project_target (dict): The project to push sequences to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed sequences
"""
sequences = shot_module.all_sequences_for_project(
project_source, client=client_source
)
for sequence in sequences:
sequence["project_id"] = project_target["id"]
return import_entities(sequences, client=client_target)
def push_shots(project_source, project_target, client_source, client_target):
"""
Copy shots from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Args:
project_source (dict): The project to get shots from
project_target (dict): The project to push shots to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed shots
"""
shots = shot_module.all_shots_for_project(
project_source, client=client_source
)
for shot in shots:
shot["project_id"] = project_target["id"]
return import_entities(shots, client=client_target)
def push_entity_links(
project_source, project_target, client_source, client_target
):
"""
Copy assets from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed entity links
"""
links = casting_module.all_entity_links_for_project(
project_source, client=client_source
)
return import_entity_links(links, client=client_target)
def push_project_entities(
project_source, project_target, client_source, client_target
):
"""
Copy assets, episodes, sequences, shots and entity links from source to
target and preserve audit fields (`id`, `created_at`, and `updated_at`)
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
dict: Pushed data
"""
assets = push_assets(project_source, project_target)
episodes = []
if project_source["production_type"] == "tvshow":
episodes = push_episodes(project_source, project_target)
sequences = push_sequences(project_source, project_target)
shots = push_shots(project_source, project_target)
entity_links = push_entity_links(project_source, project_target)
return {
"assets": assets,
"episodes": episodes,
"sequences": sequences,
"shots": shots,
"entity_links": entity_links,
}
def push_tasks(
project_source,
project_target,
default_status,
client_source,
client_target,
):
"""
Copy tasks from source to target and preserve audit fields (`id`,
`created_at`, and `updated_at`)
Attachments and previews are created too.
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Pushed entity links
"""
default_status_id = normalize_model_parameter(default_status)["id"]
task_type_map = get_sync_task_type_id_map(client_source, client_target)
task_status_map = get_sync_task_status_id_map(client_source, client_target)
person_map = get_sync_person_id_map(client_source, client_target)
tasks = task_module.all_tasks_for_project(
project_source, client=client_source
)
for task in tasks:
task["task_type_id"] = task_type_map[task["task_type_id"]]
task["task_status_id"] = default_status_id
task["assigner_id"] = person_map[task["assigner_id"]]
task["project_id"] = project_target["id"]
task["assignees"] = [
person_map[person_id] for person_id in task["assignees"]
]
return import_tasks(tasks, client=client_target)
def push_tasks_comments(project_source, client_source, client_target):
"""
Create a new comment into target api for each comment in source project
but preserve only `created_at` field.
Attachments and previews are created too.
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Created comments
"""
task_status_map = get_sync_task_status_id_map(client_source, client_target)
person_map = get_sync_person_id_map(client_source, client_target)
tasks = task_module.all_tasks_for_project(
project_source, client=client_source
)
for task in tasks:
push_task_comments(
task_status_map, person_map, task, client_source, client_target
)
return tasks
def push_task_comments(
task_status_map, person_map, task, client_source, client_target
):
"""
Create a new comment into target api for each comment in source task
but preserve only `created_at` field.
Attachments and previews are created too.
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Created comments
"""
comments = task_module.all_comments_for_task(task, client=client_source)
comments.reverse()
comments_target = []
for comment in comments:
comment_target = push_task_comment(
task_status_map,
person_map,
task,
comment,
client_source,
client_target,
)
comments_target.append(comment_target)
return comments_target
def push_task_comment(
task_status_map, person_map, task, comment, client_source, client_target
):
"""
Create a new comment into target api for each comment in source task
but preserve only `created_at` field.
Attachments and previews are created too.
Args:
project_source (dict): The project to get assets from
project_target (dict): The project to push assets to
source_client (KitsuClient): client to get data from source API
target_client (KitsuClient): client to push data to target API
Returns:
list: Created comments
"""
attachments = []
for attachment_id in comment["attachment_files"]:
if type(attachment_id) == dict:
attachment_id = attachment_id["id"]
attachment_file = gazu.files.get_attachment_file(
attachment_id, client=client_source
)
file_path = "/tmp/zou/sync/" + attachment_file["name"]
files_module.download_attachment_file(
attachment_file, file_path, client=client_source
)
attachments.append(file_path)
previews = []
for preview_file in comment["previews"]:
if type(preview_file) is str:
preview_file_id = preview_file
else:
preview_file_id = preview_file["id"]
preview_file = files_module.get_preview_file(
preview_file_id, client=client_source
)
if (
preview_file["original_name"] is not None
and preview_file["extension"] is not None
):
file_path = (
"/tmp/zou/sync/"
+ preview_file["original_name"]
+ "."
+ preview_file["extension"]
)
files_module.download_preview_file(
preview_file, file_path, client=client_source
)
previews.append(
{
"file_path": file_path,
"annotations": preview_file["annotations"],
}
)
task_status = {"id": task_status_map[comment["task_status_id"]]}
author_id = person_map[comment["person_id"]]
person = {"id": author_id}
comment_target = task_module.add_comment(
task,
task_status,
attachments=attachments,
comment=comment["text"],
created_at=comment["created_at"],
person=person,
checklist=comment["checklist"] or [],
client=client_target,
)
for preview in previews:
new_preview_file = task_module.add_preview(
task, comment_target, preview["file_path"], client=client_target
)
files_module.update_preview(
new_preview_file,
{"annotations": preview["annotations"]},
client=client_target,
)
os.remove(preview["file_path"])
for attachment_path in attachments:
os.remove(attachment_path)
return comment
def convert_id_list(ids, model_map):
"""
Args:
ids (list): Ids to convert.
model_map (dict): Map matching ids to another value.c
Returns:
list: Ids converted through given model map.
"""
return [model_map[id] for id in ids]

View File

@ -1,7 +1,10 @@
import string import string
import json import json
from .exception import TaskStatusNotFound from gazu.exception import (
TaskStatusNotFoundException,
TaskMustBeADictException,
)
from . import client as raw from . import client as raw
from .sorting import sort_by_name from .sorting import sort_by_name
@ -147,20 +150,6 @@ def all_tasks_for_episode(episode, relations=False, client=default):
return sort_by_name(tasks) return sort_by_name(tasks)
@cache
def all_tasks_for_edit(edit, relations=False, client=default):
"""
Retrieve all tasks directly linked to given edit.
"""
edit = normalize_model_parameter(edit)
params = {}
if relations:
params = {"relations": "true"}
path = "edits/%s/tasks" % edit["id"]
tasks = raw.fetch_all(path, params, client=client)
return sort_by_name(tasks)
@cache @cache
def all_shot_tasks_for_sequence(sequence, relations=False, client=default): def all_shot_tasks_for_sequence(sequence, relations=False, client=default):
""" """
@ -394,7 +383,7 @@ def get_task_by_name(entity, task_type, name="main", client=default):
def get_task_type(task_type_id, client=default): def get_task_type(task_type_id, client=default):
""" """
Args: Args:
task_type_id (str): Id of claimed task type. task_type_id (str): ID of claimed task type.
Returns: Returns:
dict: Task type matching given ID. dict: Task type matching given ID.
@ -437,11 +426,22 @@ def get_task_by_path(project, file_path, entity_type="shot", client=default):
return raw.post("data/tasks/from-path/", data, client=client) return raw.post("data/tasks/from-path/", data, client=client)
@cache
def get_default_task_status(client=default):
"""
Returns:
dict: The unique task status flagged with `is_default`.
"""
return raw.fetch_first(
"task-status", params={"is_default": True}, client=client
)
@cache @cache
def get_task_status(task_status_id, client=default): def get_task_status(task_status_id, client=default):
""" """
Args: Args:
task_status_id (str): Id of claimed task status. task_status_id (str): ID of claimed task status.
Returns: Returns:
dict: Task status matching given ID. dict: Task status matching given ID.
@ -521,7 +521,7 @@ def remove_task_status(task_status, client=default):
def get_task(task_id, client=default): def get_task(task_id, client=default):
""" """
Args: Args:
task_id (str): Id of claimed task. task_id (str): ID of claimed task.
Returns: Returns:
dict: Task matching given ID. dict: Task matching given ID.
@ -603,7 +603,7 @@ def start_task(task, started_task_status=None, client=default):
"wip", client=client "wip", client=client
) )
if started_task_status is None: if started_task_status is None:
raise TaskStatusNotFound( raise TaskStatusNotFoundException(
( (
"started_task_status is None : 'wip' task status is " "started_task_status is None : 'wip' task status is "
"non-existent. You have to create it or to set an other " "non-existent. You have to create it or to set an other "
@ -647,9 +647,9 @@ def task_to_review(
@cache @cache
def get_time_spent(task, date, client=default): def get_time_spent(task, date=None, client=default):
""" """
Get the time spent by CG artists on a task at a given date. A field contains Get the time spent by CG artists on a task at a given date if given. A field contains
the total time spent. Durations are given in seconds. Date format is the total time spent. Durations are given in seconds. Date format is
YYYY-MM-DD. YYYY-MM-DD.
@ -661,7 +661,9 @@ def get_time_spent(task, date, client=default):
dict: A dict with person ID as key and time spent object as value. dict: A dict with person ID as key and time spent object as value.
""" """
task = normalize_model_parameter(task) task = normalize_model_parameter(task)
path = "actions/tasks/%s/time-spents/%s" % (task["id"], date) path = "actions/tasks/%s/time-spents" % (task["id"])
if date is not None:
path += "/%s" % (date)
return raw.get(path, client=client) return raw.get(path, client=client)
@ -734,7 +736,9 @@ def add_comment(
task_status (str / dict): The task status dict or ID. task_status (str / dict): The task status dict or ID.
comment (str): Comment text comment (str): Comment text
person (str / dict): Comment author person (str / dict): Comment author
date (str): Comment date checklist (list): Comment checklist
attachments (list[file_path]): Attachments file paths
created_at (str): Comment date
Returns: Returns:
dict: Created comment. dict: Created comment.
@ -878,7 +882,9 @@ def add_preview(
task (str / dict): The task dict or the task ID. task (str / dict): The task dict or the task ID.
comment (str / dict): The comment or the comment ID. comment (str / dict): The comment or the comment ID.
preview_file_path (str): Path of the file to upload as preview. preview_file_path (str): Path of the file to upload as preview.
preview_file_path (str): Path of the file to upload as preview.
preview_file_url (str): Url to download the preview file if no path is
given.
Returns: Returns:
dict: Created preview file model. dict: Created preview file model.
""" """
@ -896,19 +902,74 @@ def add_preview(
) )
def set_main_preview(preview_file, client=default): def publish_preview(
task,
task_status,
comment="",
person=None,
checklist=[],
attachments=[],
created_at=None,
client=default,
preview_file_path=None,
preview_file_url=None,
normalize_movie=True,
):
"""
Publish a comment and include given preview for given task and set given
task status.
Args:
task (str / dict): The task dict or the task ID.
task_status (str / dict): The task status dict or ID.
comment (str): Comment text
person (str / dict): Comment author
checklist (list): Comment checklist
attachments (list[file_path]): Attachments file paths
created_at (str): Comment date
preview_file_path (str): Path of the file to upload as preview.
preview_file_url (str): Url to download the preview file if no path is
given.
normalize_movie (bool): Set to false to not do operations on it on the
server side.
Returns:
dict: Created preview file model.
"""
new_comment = add_comment(
task,
task_status,
comment=comment,
person=person,
checklist=checklist,
attachments=[],
created_at=created_at,
client=client,
)
add_preview(
task,
new_comment,
preview_file_path=preview_file_path,
preview_file_url=preview_file_url,
normalize_movie=normalize_movie,
)
return new_comment
def set_main_preview(preview_file, frame_number, client=default):
""" """
Set given preview as thumbnail of given entity. Set given preview as thumbnail of given entity.
Args: Args:
preview_file (str / dict): The preview file dict or ID. preview_file (str / dict): The preview file dict or ID.
frame_number (int): Frame of preview video to set as main preview
Returns: Returns:
dict: Created preview file model. dict: Created preview file model.
""" """
data = {"frame_number": frame_number} if frame_number > 1 else {}
preview_file = normalize_model_parameter(preview_file) preview_file = normalize_model_parameter(preview_file)
path = "actions/preview-files/%s/set-main-preview" % preview_file["id"] path = "actions/preview-files/%s/set-main-preview" % preview_file["id"]
return raw.put(path, {}, client=client) return raw.put(path, data, client=client)
@cache @cache
@ -954,17 +1015,19 @@ def assign_task(task, person, client=default):
return raw.put(path, {"task_ids": task["id"]}, client=client) return raw.put(path, {"task_ids": task["id"]}, client=client)
def new_task_type(name, client=default): def new_task_type(name, color="#000000", client=default):
""" """
Create a new task type with the given name. Create a new task type with the given name.
Args: Args:
name (str): The name of the task type name (str): The name of the task type
color (str): The color of the task type as an hexadecimal string
with # as first character. ex : #00FF00
Returns: Returns:
dict: The created task type dict: The created task type
""" """
data = {"name": name} data = {"name": name, "color": color}
return raw.post("data/task-types", data, client=client) return raw.post("data/task-types", data, client=client)
@ -975,7 +1038,7 @@ def new_task_status(name, short_name, color, client=default):
Args: Args:
name (str): The name of the task status name (str): The name of the task status
short_name (str): The short name of the task status short_name (str): The short name of the task status
color (str): The color of the task status has an hexadecimal string color (str): The color of the task status as an hexadecimal string
with # as first character. ex : #00FF00 with # as first character. ex : #00FF00
Returns: Returns:
@ -1032,12 +1095,13 @@ def update_task_data(task, data={}, client=default):
def get_task_url(task, client=default): def get_task_url(task, client=default):
""" """
Args: Args:
task (str / dict): The task dict or the task ID. task (dict): The task dict.
Returns: Returns:
url (str): Web url associated to the given task url (str): Web url associated to the given task
""" """
task = normalize_model_parameter(task) if not isinstance(task, dict):
raise TaskMustBeADictException
path = "{host}/productions/{project_id}/shots/tasks/{task_id}/" path = "{host}/productions/{project_id}/shots/tasks/{task_id}/"
return path.format( return path.format(
host=raw.get_api_url_from_host(client=client), host=raw.get_api_url_from_host(client=client),

View File

@ -1,5 +1,5 @@
import datetime import datetime
from .exception import NotAuthenticatedException from gazu.exception import NotAuthenticatedException
from . import client as raw from . import client as raw
from .sorting import sort_by_name from .sorting import sort_by_name
@ -247,6 +247,26 @@ def all_done_tasks(client=default):
return raw.fetch_all("user/done-tasks", client=client) return raw.fetch_all("user/done-tasks", client=client)
@cache
def get_timespents_range(start_date, end_date, client=default):
"""
Gets the timespents of the current user for the given date range.
Args:
start_date (str): The first day of the date range as a date string with
the following format: YYYY-MM-DD
end_date (str): The last day of the date range as a date string with
the following format: YYYY-MM-DD
Returns:
list: All of the person's time spents
"""
date_range = {
"start_date": start_date,
"end_date": end_date,
}
return raw.get("/data/user/time-spents", params=date_range, client=client)
def log_desktop_session_log_in(client=default): def log_desktop_session_log_in(client=default):
""" """
Add a log entry to mention that the user logged in his computer. Add a log entry to mention that the user logged in his computer.