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 helpers
try:
from . import events
except ImportError:
pass
from . import asset
from . import casting
from . import context
from . import entity
from . import edit
from . import entity
from . import files
from . import project
from . import person
@ -16,7 +21,11 @@ from . import task
from . import user
from . import playlist
from .exception import AuthFailedException, ParameterException
from .exception import (
AuthFailedException,
ParameterException,
NotAuthenticatedException,
)
from .__version__ import __version__
@ -28,13 +37,30 @@ def set_host(url, client=raw.default_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 = {}
try:
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
if not tokens or (
@ -46,6 +72,10 @@ def log_in(email, password, client=raw.default_client):
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):
tokens = {}
try:
@ -56,6 +86,24 @@ def log_out(client=raw.default_client):
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):
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):
"""
Args:
asset_id (str): Id of claimed asset.
asset_id (str): ID of claimed asset.
Returns:
dict: Asset matching given ID.
@ -270,6 +270,7 @@ def all_asset_types_for_project(project, client=default):
Returns:
list: Asset types from assets listed in given project.
"""
project = normalize_model_parameter(project)
path = "projects/%s/asset-types" % project["id"]
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):
"""
Args:
asset_type_id (str/): Id of claimed asset type.
asset_type_id (str/): ID of claimed asset type.
Returns:
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):
"""
Args:
asset_instance_id (str): Id of claimed asset instance.
asset_instance_id (str): ID of claimed asset instance.
Returns:
dict: Asset Instance matching given ID.

View File

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

View File

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

View File

@ -1,59 +1,147 @@
from blender_kitsu import gazu
from . import client as raw
from .sorting import sort_by_name
from .cache import cache
from .helpers import normalize_model_parameter
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
def get_edit(edit_id, relations=False, 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):
def get_edit(edit_id, client=default):
"""
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.
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_previews = (raw.fetch_all(f"edits/{edit['id']}/preview-files", client=client))
for key in [key for key in enumerate(edit_previews.keys())]:
return edit_previews[key[1]]
path = "data/edits/%s" % edit["id"]
params = {}
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):
"""
Args:
id (str, client=default): ID of claimed entity.
entity_id (str): ID of claimed entity.
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).
"""
return raw.fetch_one("entities", entity_id, client=client)
@cache
def get_entity_by_name(entity_name, client=default):
def get_entity_by_name(entity_name, project=None, client=default):
"""
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:
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
def get_entity_type(entity_type_id, client=default):
"""
Args:
id (str, client=default): ID of claimed entity type.
, client=client
Returns:
Retrieve entity type matching given ID (It can be an entity type of any
kind).
Args:
entity_type_id (str): ID of claimed entity type.
Returns:
Retrieve entity type matching given ID (It can be an entity type of any
kind).
"""
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):
"""
Creates an entity type with the given name.
@ -104,16 +142,3 @@ def remove_entity(entity, force=False, client=default):
if force:
params = {"force": "true"}
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
class TaskStatusNotFound(Exception):
class TaskStatusNotFoundException(Exception):
"""
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.
"""
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.
"""
return raw.download(
get_preview_file_url(preview_file),
get_preview_file_url(preview_file, client=client),
file_path,
client=client,
)

View File

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

View File

@ -38,15 +38,61 @@ def all_persons(client=default):
@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:
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:
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
@ -152,7 +198,7 @@ def new_person(
Returns:
dict: Created person.
"""
person = get_person_by_email(email)
person = get_person_by_email(email, client=client)
if person is None:
person = raw.post(
"data/persons/new",

View File

@ -256,6 +256,7 @@ def add_metadata_descriptor(
project,
name,
entity_type,
data_type="string",
choices=[],
for_client=False,
departments=[],
@ -278,6 +279,7 @@ def add_metadata_descriptor(
project = normalize_model_parameter(project)
data = {
"name": name,
"data_type": data_type,
"choices": choices,
"for_client": for_client,
"entity_type": entity_type,
@ -292,7 +294,27 @@ def add_metadata_descriptor(
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:
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.
"""
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"],
return raw.fetch_first(
"metadata-descriptors",
params={
"project_id": project["id"],
"field_name": field_name,
},
client=client,
)
@ -375,3 +399,46 @@ def remove_metadata_descriptor(
params,
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)
sequence = normalize_model_parameter(sequence)
shot = {"name": name, "sequence_id": sequence["id"]}
scene = {"name": name, "sequence_id": sequence["id"]}
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):
"""
Args:
episode_id (str): Id of claimed episode.
episode_id (str): ID of claimed episode.
Returns:
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):
"""
Args:
shot_id (str): Id of claimed shot.
shot_id (str): ID of claimed shot.
Returns:
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 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
@ -6,7 +17,12 @@ default = raw.default_client
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.
@ -16,13 +32,13 @@ def get_last_events(
project (dict/id): Get only events related to this project.
after (dict/id): Get only events occuring after given date.
before (dict/id): Get only events occuring before given date.
only_files (bool): Get only events related to files.
Returns:
dict: Last events matching criterions.
"""
path = "/data/events/last"
params = {"page_size": page_size}
params = {"page_size": page_size, "only_files": only_files}
if project is not None:
project = normalize_model_parameter(project)
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):
"""
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"]
target_date = target_model["updated_at"]
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 json
from .exception import TaskStatusNotFound
from gazu.exception import (
TaskStatusNotFoundException,
TaskMustBeADictException,
)
from . import client as raw
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)
@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
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):
"""
Args:
task_type_id (str): Id of claimed task type.
task_type_id (str): ID of claimed task type.
Returns:
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)
@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
def get_task_status(task_status_id, client=default):
"""
Args:
task_status_id (str): Id of claimed task status.
task_status_id (str): ID of claimed task status.
Returns:
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):
"""
Args:
task_id (str): Id of claimed task.
task_id (str): ID of claimed task.
Returns:
dict: Task matching given ID.
@ -603,7 +603,7 @@ def start_task(task, started_task_status=None, client=default):
"wip", client=client
)
if started_task_status is None:
raise TaskStatusNotFound(
raise TaskStatusNotFoundException(
(
"started_task_status is None : 'wip' task status is "
"non-existent. You have to create it or to set an other "
@ -647,9 +647,9 @@ def task_to_review(
@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
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.
"""
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)
@ -734,7 +736,9 @@ def add_comment(
task_status (str / dict): The task status dict or ID.
comment (str): Comment text
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:
dict: Created comment.
@ -878,7 +882,9 @@ def add_preview(
task (str / dict): The task dict or the task 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_url (str): Url to download the preview file if no path is
given.
Returns:
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.
Args:
preview_file (str / dict): The preview file dict or ID.
frame_number (int): Frame of preview video to set as main preview
Returns:
dict: Created preview file model.
"""
data = {"frame_number": frame_number} if frame_number > 1 else {}
preview_file = normalize_model_parameter(preview_file)
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
@ -954,17 +1015,19 @@ def assign_task(task, person, client=default):
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.
Args:
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:
dict: The created task type
"""
data = {"name": name}
data = {"name": name, "color": color}
return raw.post("data/task-types", data, client=client)
@ -975,7 +1038,7 @@ def new_task_status(name, short_name, color, client=default):
Args:
name (str): The 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
Returns:
@ -1032,12 +1095,13 @@ def update_task_data(task, data={}, client=default):
def get_task_url(task, client=default):
"""
Args:
task (str / dict): The task dict or the task ID.
task (dict): The task dict.
Returns:
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}/"
return path.format(
host=raw.get_api_url_from_host(client=client),

View File

@ -1,5 +1,5 @@
import datetime
from .exception import NotAuthenticatedException
from gazu.exception import NotAuthenticatedException
from . import client as raw
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)
@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):
"""
Add a log entry to mention that the user logged in his computer.