Skip to content

pipeline

TVPAINT_CHUNK_LENGTH = 500 module-attribute

TVPaint's Metadata

Metadata are stored to TVPaint's workfile.

Workfile works similar to .ini file but has few limitation. Most important limitation is that value under key has limited length. Due to this limitation each metadata section/key stores number of "subkeys" that are related to the section.

Example: Metadata key "instances" may have stored value "2". In that case it is expected that there are also keys ["instances0", "instances1"].

Workfile data looks like:

[avalon]
instances0=[{{__dq__}id{__dq__}: {__dq__}ayon.create.instance{__dq__...
instances1=...more data...
instances=2

TVPaintHost

Bases: HostBase, IWorkfileHost, ILoadHost, IPublishHost

Source code in client/ayon_tvpaint/api/pipeline.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
    name = "tvpaint"

    def install(self):
        """Install TVPaint-specific functionality."""

        log.info("AYON - Installing TVPaint integration")

        # Create workdir folder if does not exist yet
        workdir = os.getenv("AYON_WORKDIR")
        if not os.path.exists(workdir):
            os.makedirs(workdir)

        plugins_dir = os.path.join(TVPAINT_ROOT_DIR, "plugins")
        publish_dir = os.path.join(plugins_dir, "publish")
        load_dir = os.path.join(plugins_dir, "load")
        create_dir = os.path.join(plugins_dir, "create")

        pyblish.api.register_host("tvpaint")
        pyblish.api.register_plugin_path(publish_dir)
        register_loader_plugin_path(load_dir)
        register_creator_plugin_path(create_dir)

        register_event_callback("application.launched", self.initial_launch)
        register_event_callback("application.exit", self.application_exit)
        register_event_callback(
            "workfile.open.after",
            self._on_workfile_open_after
        )

    def get_current_project_name(self):
        """
        Returns:
            Union[str, None]: Current project name.
        """

        return self.get_current_context().get("project_name")

    def get_current_folder_path(self):
        """
        Returns:
            Union[str, None]: Current folder path.
        """

        return self.get_current_context().get("folder_path")

    def get_current_task_name(self):
        """
        Returns:
            Union[str, None]: Current task name.
        """

        return self.get_current_context().get("task_name")

    def get_current_context(self):
        context = get_current_workfile_context()
        if not context:
            return get_global_context()

        if "project_name" in context:
            if "asset_name" in context:
                context["folder_path"] = context["asset_name"]
            return context
        # This is legacy way how context was stored
        return {
            "project_name": context.get("project"),
            "folder_path": context.get("asset"),
            "task_name": context.get("task")
        }

    # --- Create ---
    def get_context_data(self):
        return get_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, {})

    def update_context_data(self, data, changes):
        return write_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, data)

    def list_instances(self):
        """List all created instances from current workfile."""
        return list_instances()

    def write_instances(self, data):
        return write_instances(data)

    # --- Workfile ---
    def open_workfile(self, filepath):
        george_script = "tv_LoadProject '\"'\"{}\"'\"'".format(
            filepath.replace("\\", "/")
        )
        return execute_george_through_file(george_script)

    def save_workfile(self, filepath=None):
        if not filepath:
            filepath = self.get_current_workfile()
        context = get_global_context()
        save_current_workfile_context(context)

        # Execute george script to save workfile.
        george_script = "tv_SaveProject {}".format(filepath.replace("\\", "/"))
        return execute_george(george_script)

    def work_root(self, session):
        return session["AYON_WORKDIR"]

    def get_current_workfile(self):
        # TVPaint returns a '\' character when no scene is currently opened
        current_workfile = execute_george("tv_GetProjectName")
        if current_workfile == '\\':
            return None
        return current_workfile

    def workfile_has_unsaved_changes(self):
        return None

    def get_workfile_extensions(self):
        return [".tvpp"]

    # --- Load ---
    def get_containers(self):
        return get_containers()

    def initial_launch(self):
        # Setup project context
        # - if was used e.g. template the context might be invalid.
        if not self.get_current_workfile():
            return

        log.info("Setting up context...")
        global_context = get_global_context()
        project_name = global_context.get("project_name")
        if not project_name:
            return

        save_current_workfile_context(global_context)
        # TODO fix 'set_context_settings'
        return

        folder_path = global_context.get("folder_path")
        task_name = global_context.get("task_name")

        if not folder_path:
            return

        folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
        if folder_entity and task_name:
            task_entity = ayon_api.get_task_by_name(
                project_name,
                folder_id=folder_entity["id"],
                task_name=task_name)
            context_entity = task_entity
        else:
            log.warning(
                "Falling back to setting context settings using folder entity "
                "because no task was found.")
            context_entity = folder_entity

        set_context_settings(context_entity)

    def application_exit(self):
        """Logic related to TimerManager.

        Todo:
            This should be handled out of TVPaint integration logic.
        """

        data = get_current_project_settings()
        stop_timer = data["tvpaint"]["stop_timer_on_application_exit"]

        if not stop_timer:
            return

        # Stop application timer.
        webserver_url = os.environ.get("AYON_WEBSERVER_URL")
        rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url)
        requests.post(rest_api_url)

    def _on_workfile_open_after(self):
        # Make sure opened workfile has stored correct context
        global_context = get_global_context()
        save_current_workfile_context(global_context)

application_exit()

Logic related to TimerManager.

Todo

This should be handled out of TVPaint integration logic.

Source code in client/ayon_tvpaint/api/pipeline.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def application_exit(self):
    """Logic related to TimerManager.

    Todo:
        This should be handled out of TVPaint integration logic.
    """

    data = get_current_project_settings()
    stop_timer = data["tvpaint"]["stop_timer_on_application_exit"]

    if not stop_timer:
        return

    # Stop application timer.
    webserver_url = os.environ.get("AYON_WEBSERVER_URL")
    rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url)
    requests.post(rest_api_url)

get_current_folder_path()

Returns:

Type Description

Union[str, None]: Current folder path.

Source code in client/ayon_tvpaint/api/pipeline.py
100
101
102
103
104
105
106
def get_current_folder_path(self):
    """
    Returns:
        Union[str, None]: Current folder path.
    """

    return self.get_current_context().get("folder_path")

get_current_project_name()

Returns:

Type Description

Union[str, None]: Current project name.

Source code in client/ayon_tvpaint/api/pipeline.py
92
93
94
95
96
97
98
def get_current_project_name(self):
    """
    Returns:
        Union[str, None]: Current project name.
    """

    return self.get_current_context().get("project_name")

get_current_task_name()

Returns:

Type Description

Union[str, None]: Current task name.

Source code in client/ayon_tvpaint/api/pipeline.py
108
109
110
111
112
113
114
def get_current_task_name(self):
    """
    Returns:
        Union[str, None]: Current task name.
    """

    return self.get_current_context().get("task_name")

install()

Install TVPaint-specific functionality.

Source code in client/ayon_tvpaint/api/pipeline.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def install(self):
    """Install TVPaint-specific functionality."""

    log.info("AYON - Installing TVPaint integration")

    # Create workdir folder if does not exist yet
    workdir = os.getenv("AYON_WORKDIR")
    if not os.path.exists(workdir):
        os.makedirs(workdir)

    plugins_dir = os.path.join(TVPAINT_ROOT_DIR, "plugins")
    publish_dir = os.path.join(plugins_dir, "publish")
    load_dir = os.path.join(plugins_dir, "load")
    create_dir = os.path.join(plugins_dir, "create")

    pyblish.api.register_host("tvpaint")
    pyblish.api.register_plugin_path(publish_dir)
    register_loader_plugin_path(load_dir)
    register_creator_plugin_path(create_dir)

    register_event_callback("application.launched", self.initial_launch)
    register_event_callback("application.exit", self.application_exit)
    register_event_callback(
        "workfile.open.after",
        self._on_workfile_open_after
    )

list_instances()

List all created instances from current workfile.

Source code in client/ayon_tvpaint/api/pipeline.py
139
140
141
def list_instances(self):
    """List all created instances from current workfile."""
    return list_instances()

containerise(name, namespace, members, context, loader, current_containers=None)

Add new container to metadata.

Parameters:

Name Type Description Default
name str

Container name.

required
namespace str

Container namespace.

required
members list

List of members that were loaded and belongs to the container (layer names).

required
current_containers list

Preloaded containers. Should be used only on update/switch when containers were modified during the process.

None

Returns:

Name Type Description
dict

Container data stored to workfile metadata.

Source code in client/ayon_tvpaint/api/pipeline.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def containerise(
    name, namespace, members, context, loader, current_containers=None
):
    """Add new container to metadata.

    Args:
        name (str): Container name.
        namespace (str): Container namespace.
        members (list): List of members that were loaded and belongs
            to the container (layer names).
        current_containers (list): Preloaded containers. Should be used only
            on update/switch when containers were modified during the process.

    Returns:
        dict: Container data stored to workfile metadata.
    """

    container_data = {
        "members": members,
        "name": name,
        "namespace": namespace,
        "loader": str(loader),
        "representation": context["representation"]["id"]
    }
    if current_containers is None:
        current_containers = get_containers()

    # Add container to containers list
    current_containers.append(container_data)

    # Store data to metadata
    write_workfile_metadata(SECTION_NAME_CONTAINERS, current_containers)

    return container_data

get_current_workfile_context()

Return context in which was workfile saved.

Source code in client/ayon_tvpaint/api/pipeline.py
467
468
469
def get_current_workfile_context():
    """Return context in which was workfile saved."""
    return get_workfile_metadata(SECTION_NAME_CONTEXT, {})

get_workfile_metadata(metadata_key, default=None)

Read and parse metadata for specific key from current project workfile.

Pipeline use function to store loaded and created instances within keys stored in SECTION_NAME_INSTANCES and SECTION_NAME_CONTAINERS constants.

Parameters:

Name Type Description Default
metadata_key str

Key defying which key should read. It is expected value contain json serializable string.

required
Source code in client/ayon_tvpaint/api/pipeline.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
def get_workfile_metadata(metadata_key, default=None):
    """Read and parse metadata for specific key from current project workfile.

    Pipeline use function to store loaded and created instances within keys
    stored in `SECTION_NAME_INSTANCES` and `SECTION_NAME_CONTAINERS`
    constants.

    Args:
        metadata_key (str): Key defying which key should read. It is expected
            value contain json serializable string.
    """
    if default is None:
        default = []

    json_string = get_workfile_metadata_string(metadata_key)
    if json_string:
        try:
            return json.loads(json_string)
        except json.decoder.JSONDecodeError:
            # TODO remove when backwards compatibility of storing metadata
            # will be removed
            print((
                "Fixed invalid metadata in workfile."
                " Not serializable string was: {}"
            ).format(json_string))
            write_workfile_metadata(metadata_key, default)
    return default

get_workfile_metadata_string(metadata_key)

Read metadata for specific key from current project workfile.

Source code in client/ayon_tvpaint/api/pipeline.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def get_workfile_metadata_string(metadata_key):
    """Read metadata for specific key from current project workfile."""
    result = get_workfile_metadata_string_for_keys([metadata_key])
    if not result:
        return None

    stripped_result = result.strip()
    if not stripped_result:
        return None

    # NOTE Backwards compatibility when metadata key did not store range of key
    #   indexes but the value itself
    # NOTE We don't have to care about negative values with `isdecimal` check
    if not stripped_result.isdecimal():
        metadata_string = result
    else:
        keys = []
        for idx in range(int(stripped_result)):
            keys.append("{}{}".format(metadata_key, idx))
        metadata_string = get_workfile_metadata_string_for_keys(keys)

    # Replace quotes plaholders with their values
    metadata_string = (
        metadata_string
        .replace("{__sq__}", "'")
        .replace("{__dq__}", "\"")
    )
    return metadata_string

get_workfile_metadata_string_for_keys(metadata_keys)

Read metadata for specific keys from current project workfile.

All values from entered keys are stored to single string without separator.

Function is designed to help get all values for one metadata key at once. So order of passed keys matteres.

Parameters:

Name Type Description Default
metadata_keys (list, str)

Metadata keys for which data should be retrieved. Order of keys matters! It is possible to enter only single key as string.

required
Source code in client/ayon_tvpaint/api/pipeline.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def get_workfile_metadata_string_for_keys(metadata_keys):
    """Read metadata for specific keys from current project workfile.

    All values from entered keys are stored to single string without separator.

    Function is designed to help get all values for one metadata key at once.
    So order of passed keys matteres.

    Args:
        metadata_keys (list, str): Metadata keys for which data should be
            retrieved. Order of keys matters! It is possible to enter only
            single key as string.
    """
    # Add ability to pass only single key
    if isinstance(metadata_keys, str):
        metadata_keys = [metadata_keys]

    output_file = tempfile.NamedTemporaryFile(
        mode="w", prefix="a_tvp_", suffix=".txt", delete=False
    )
    output_file.close()
    output_filepath = output_file.name.replace("\\", "/")

    george_script_parts = []
    george_script_parts.append(
        "output_path = \"{}\"".format(output_filepath)
    )
    # Store data for each index of metadata key
    for metadata_key in metadata_keys:
        george_script_parts.append(
            "tv_readprojectstring \"{}\" \"{}\" \"\"".format(
                METADATA_SECTION, metadata_key
            )
        )
        george_script_parts.append(
            "tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' result"
        )

    # Execute the script
    george_script = "\n".join(george_script_parts)
    execute_george_through_file(george_script)

    # Load data from temp file
    with open(output_filepath, "r") as stream:
        file_content = stream.read()

    # Remove `\n` from content
    output_string = file_content.replace("\n", "")

    # Delete temp file
    os.remove(output_filepath)

    return output_string

list_instances()

List all created instances from current workfile.

Source code in client/ayon_tvpaint/api/pipeline.py
477
478
479
def list_instances():
    """List all created instances from current workfile."""
    return get_workfile_metadata(SECTION_NAME_INSTANCES)

save_current_workfile_context(context)

Save context which was used to create a workfile.

Source code in client/ayon_tvpaint/api/pipeline.py
472
473
474
def save_current_workfile_context(context):
    """Save context which was used to create a workfile."""
    return write_workfile_metadata(SECTION_NAME_CONTEXT, context)

set_context_settings(context_entity)

Set workfile settings by folder entity attributes.

Change fps, resolution and frame start/end.

Parameters:

Name Type Description Default
context_entity dict[str, Any]

Task or folder entity.

required
Source code in client/ayon_tvpaint/api/pipeline.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def set_context_settings(context_entity):
    """Set workfile settings by folder entity attributes.

    Change fps, resolution and frame start/end.

    Args:
        context_entity (dict[str, Any]): Task or folder entity.

    """
    # TODO We should fix these issues:
    # - do not use 'tv_resizepage' or find out why it removes layers
    # - mark in/out should respect existing mark in value if is set
    if not context_entity:
        return

    attributes = context_entity["attrib"]

    width = attributes.get("resolutionWidth")
    height = attributes.get("resolutionHeight")
    if width is None or height is None:
        print("Resolution was not found!")
    else:
        execute_george(
            "tv_resizepage {} {} 0".format(width, height)
        )

    framerate = attributes.get("fps")

    if framerate is not None:
        execute_george(
            "tv_framerate {} \"timestretch\"".format(framerate)
        )
    else:
        print("Framerate was not found!")

    frame_start = attributes.get("frameStart")
    frame_end = attributes.get("frameEnd")

    if frame_start is None or frame_end is None:
        print("Frame range was not found!")
        return

    handle_start = attributes.get("handleStart") or 0
    handle_end = attributes.get("handleEnd") or 0

    # Always start from 0 Mark In and set only Mark Out
    mark_in = 0
    mark_out = mark_in + (frame_end - frame_start) + handle_start + handle_end

    execute_george("tv_markin {} set".format(mark_in))
    execute_george("tv_markout {} set".format(mark_out))

split_metadata_string(text, chunk_length=None)

Split string by length.

Split text to chunks by entered length. Example: python text = "ABCDEFGHIJKLM" result = split_metadata_string(text, 3) print(result) >>> ['ABC', 'DEF', 'GHI', 'JKL']

Parameters:

Name Type Description Default
text str

Text that will be split into chunks.

required
chunk_length int

Single chunk size. Default chunk_length is set to global variable TVPAINT_CHUNK_LENGTH.

None

Returns:

Name Type Description
list

List of strings with at least one item.

Source code in client/ayon_tvpaint/api/pipeline.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def split_metadata_string(text, chunk_length=None):
    """Split string by length.

    Split text to chunks by entered length.
    Example:
        ```python
        text = "ABCDEFGHIJKLM"
        result = split_metadata_string(text, 3)
        print(result)
        >>> ['ABC', 'DEF', 'GHI', 'JKL']
        ```

    Args:
        text (str): Text that will be split into chunks.
        chunk_length (int): Single chunk size. Default chunk_length is
            set to global variable `TVPAINT_CHUNK_LENGTH`.

    Returns:
        list: List of strings with at least one item.
    """
    if chunk_length is None:
        chunk_length = TVPAINT_CHUNK_LENGTH
    chunks = []
    for idx in range(chunk_length, len(text) + chunk_length, chunk_length):
        start_idx = idx - chunk_length
        chunks.append(text[start_idx:idx])
    return chunks

write_workfile_metadata(metadata_key, value)

Write metadata for specific key into current project workfile.

George script has specific way how to work with quotes which should be solved automatically with this function.

Parameters:

Name Type Description Default
metadata_key str

Key defying under which key value will be stored.

required
value (dict, list, str)

Data to store they must be json serializable.

required
Source code in client/ayon_tvpaint/api/pipeline.py
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
def write_workfile_metadata(metadata_key, value):
    """Write metadata for specific key into current project workfile.

    George script has specific way how to work with quotes which should be
    solved automatically with this function.

    Args:
        metadata_key (str): Key defying under which key value will be stored.
        value (dict,list,str): Data to store they must be json serializable.
    """
    if isinstance(value, (dict, list)):
        value = json.dumps(value)

    if not value:
        value = ""

    # Handle quotes in dumped json string
    # - replace single and double quotes with placeholders
    value = (
        value
        .replace("'", "{__sq__}")
        .replace("\"", "{__dq__}")
    )
    chunks = split_metadata_string(value)
    chunks_len = len(chunks)

    write_template = "tv_writeprojectstring \"{}\" \"{}\" \"{}\""
    george_script_parts = []
    # Add information about chunks length to metadata key itself
    george_script_parts.append(
        write_template.format(METADATA_SECTION, metadata_key, chunks_len)
    )
    # Add chunk values to indexed metadata keys
    for idx, chunk_value in enumerate(chunks):
        sub_key = "{}{}".format(metadata_key, idx)
        george_script_parts.append(
            write_template.format(METADATA_SECTION, sub_key, chunk_value)
        )

    george_script = "\n".join(george_script_parts)

    return execute_george_through_file(george_script)