Quickstart

Eager to get started? This page give some introduction to the core concepts of Trackteroid. Follow Installation and install Trackteroid first. Ideally you should also have a basic understanding of the Ftrack Python API.

The provided examples assume that you have properly configured the API access for Ftrack accordingly.

Accessing Data From FTrack

from trackteroid import (
    Query,
    AssetVersion
)

version_collection = Query(AssetVersion).get_first()
print(version_collection)

# output: EntityCollection[AssetVersion]{1}

The Query

To retrieve data from the Ftrack server, you need to perform queries using the Query class. This class serves as the entry point for accessing data and should be initialized with the desired entity type: Query(<Entity Type>).

In the previous example, we wanted to retrieve any AssetVersion but only fetched the first result from the server. Any get_ prefixed method on the Query instance we do refer to as terminators. These terminators are responsible for executing the resolved query instruction, sending it to the Ftrack server, and fetching the corresponding data.

The result is returned as a collection, specifically an instance of either EntityCollection or EmptyCollection, depending on the outcome. When printed, the collection is represented as:

EntityCollection[<Entity Type>]{<Number of Results>} or EmptyCollection[<Entity Type>]

If you wish to preview the resolved query without sending it to the server, you can simply print the collection or call str on it.

from trackteroid import AssetVersion

print(Query(AssetVersion))
# output: select id, asset.name, version from AssetVersion

Projections

When accessing data from the resulting collection, it is important to project (specify) the attributes that you will need to access later. A resolved query typically takes the form:

select <projections> from <entity type> where <criteria>

Looking back at the previous example, you can observe that the attributes id, asset.name, and version were included in the resolved query instruction. This was done because these attributes are commonly accessed and can be predefined for certain entity types.

By projecting the necessary attributes in the query, you ensure that the resulting collection includes the specific data you require.

from trackteroid import AssetVersion

print(AssetVersion.projections)
# output: ['id', 'asset.name', 'version']

In contrast to the Ftrack Python API, the default Session within Trackteroid disables the auto-population feature. This means that the Session will not automatically fetch missing data when accessing attributes on your collections. Instead, the data is fetched only for the attributes that were explicitly projected in the query. This behavior provides a more controlled and optimized approach to data retrieval. By avoiding unnecessary data fetching, disabled auto-population minimizes server requests and might improve performance significantly.

When working with Trackteroid, it is important to be aware of this behavior and ensure that you project all the attributes you need in your queries.

print(Query(AssetVersion).get_first().id)
# output: [u'00001180-b7e7-43cf-b0e5-a2df0cefe669']

print(Query(AssetVersion).get_first().comment)
# output: [Symbol(NOT_SET)]

print(Query(AssetVersion).get_first(projections=["comment"]).comment)
# output: [u'Hello World']

When attempting to access the comment attribute without projecting it, the output contains Symbol(NOT_SET), indicating that the data for the comment attribute was not fetched. However, by modifying the query to include the comment attribute in the projections list (projections=[“comment”]) and accessing it, the output becomes [u’Hello World’], providing the retrieved value of the comment.

from trackteroid import (
    Query,
    AssetVersion
)

print(
    Query(AssetVersion).get_first(
        projections=["comment", "asset.parent.project.name"]
    ).asset.parent.project.name
)
# output: [u'DummyProject']

Knowing these relationships and constructing written queries can be challenging, leading to long and complex queries. However, Trackteroid provides a shorter and easier alternative for many relationships.

from trackteroid import (
    Query,
    AssetVersion,
    Project
)

version_collection = Query(AssetVersion).get_first(projections=[Project.name])
# Performing query: "select asset.name, task.project.name, id, version from AssetVersion"

# The abbreviation is not only working for projections, 
# but also via attribute access on the resulting collection
print(
    version_collection.task.project.name,
    version_collection.Project.name
)
# output: ([u'DummyProject'], [u'DummyProject'])

This concise and intuitive approach simplifies querying and attribute retrieval for complex relationships.

Filtering

To ensure optimal performance and avoid fetching unnecessary data, it’s recommended to narrow down the query results directly using Query criteria. Criteria methods in Trackteroid follow a by_ and not_by_ name prefix convention and can be chained together. While different entity types may have different criteria methods available, many share common ones. By utilizing criteria methods, you can specify filtering conditions directly in the query construction process, reducing the amount of data retrieved. This approach helps improve code performance and efficiency.

from trackteroid import (
    Query,
    AssetVersion
)

print(Query(AssetVersion).by_id("00001180-b7e7-43cf-b0e5-a2df0cefe669").get_all())
# output: EntityCollection[AssetVersion]{1}

# while you can technically also do 
# `Query(AssetVersion).by_id("00001180-b7e7-43cf-b0e5-a2df0cefe669").by_id("00001fd9-c8b8-4d84-8a8d-2c8fbbed46a0").get_all()`
print(
    Query(AssetVersion).by_id(
        "00001180-b7e7-43cf-b0e5-a2df0cefe669", 
        "00001fd9-c8b8-4d84-8a8d-2c8fbbed46a0"
    ).get_all()
)
# output: EntityCollection[AssetVersion]{2}

# get all AssetVersions with version number 1 or 2 of an Asset called 'SomeAsset'
print(Query(AssetVersion).by_name("SomeAsset").by_version(1, 2)).get_all()
# output: EntityCollection[AssetVersion]{2}

# get all AssetVersions with version number 1 or 2 of any Asset that is NOT called 'SomeAsset'
print(Query(AssetVersion).not_by_name("SomeAsset").by_version(1, 2)).get_all()
# output: EntityCollection[AssetVersion]{10}

Moreover, the query mechanism allows for pattern-based filtering using the % placeholder, which denotes “zero or more of any character”. This feature enhances the flexibility and sophistication of your filtering options within queries.

from trackteroid import (
    Query,
    Asset
)

print(Query(Asset).by_name("%Asset").get_all().name)
# output: [u'SomeAsset', u'SomeAsset', u'SomeAsset']

print(Query(Asset).by_name("Some%").get_all().name)
# output: [u'SomeAsset', u'SomeAsset', u'SomeAsset', u'SomeCharacter', u'SomeScene']

print(Query(Asset).by_name("%Asset%").get_all().name)
# output: [u'SomeAsset', u'SomeAsset', u'SomeAsset', u'AnAssetClone']

Frequently, criteria in the query mechanism involve searching for direct properties of an entity, such as id, name, or metadata. By default, those criteria are associated with the entity type specified in the Query, representing the desired results. However, criteria can also offer the flexibility to define a target, allowing you to specify the entity type for which you want to reference its property instead.

from trackteroid import (
    Query,
    Asset,
    Project
)
print(Query(Asset).by_name("SomeAsset").get_all())
# output: EntityCollection[Asset]{3}
print(Query(Asset).by_name(Project, "DummyProject", "DummyProject2").get_all())
# output: EntityCollection[Asset]{10}
print(Query(Asset).by_name("SomeAsset").by_name(Project, "DummyProject", "DummyProject2").get_all())
# output: EntityCollection[Asset]{2}

For criteria that support target specification, you have the option to provide exactly one target as the first positional argument. This target defines the relationship for the property used within the criterion.

Limiting and Ordering

The get_all terminator supports limiting and ordering results.

from trackteroid import (
    Query,
    AssetVersion
)

# get all AssetVersions ordered descending by their version number across all Assets
print(Query(AssetVersion).get_all(limit=8, order="descending", order_by="version").version)
# output: [55, 43, 42, 22, 10, 10, 8, 7]

Defining Relationships

One of the main objectives of Trackteroid is to minimize the need for in-depth knowledge of the underlying database structure when working with queries and resulting collections. This goal is accomplished through two distinct approaches.

Firstly, it automatically derives relationships whenever possible by dynamically inspecting the schema of the current session. This capability allows for seamless handling of relationships without requiring explicit configuration.

However, Ftrack’s dynamic nature means that certain entity types may require configuring relationships to align with specific requirements. Trackteroid provides the flexibility to describe and represent contextual relationships for such cases, enabling customization and adaptation to meet individual needs by implementing a resolver.

All communication with an Ftrack server is facilitated through a Session object. By default, a Query is constructed using the SESSION singleton and the default schema. Here’s an example:

from trackteroid import (
    AssetVersion,
    Query,
    SCHEMA,
    SESSION,
)

# same as Query(AssetVersion)
Query(AssetVersion, session=SESSION, schema=SCHEMA.default)

However, you also have the flexibility to initialize your own Session object and provide a different schema. Here’s an example:

from trackteroid import (
    Query,
    SCHEMA,
    AssetVersion
)
from trackteroid.session import Session

my_session = Session()

Query(AssetVersion, session=my_session, schema=SCHEMA.vfx)

Collections

The result of terminated Query is a collection, specifically an instance of either EntityCollection or EmptyCollection, depending on the outcome. When printed, the collection is represented as:

EntityCollection[<Entity Type>]{<Number of Results>} or EmptyCollection[<Entity Type>]

An EntityCollection is a container of wrapped ftrack entity objects with the following definitions:

  • It is an ordered container of entity objects.

  • It is immutable, meaning its contents entities can not be added or removed once created.

  • It is iterable, allowing for easy iteration over the entities no matter if there is a single or multiple entities in it.

  • It only contains unique elements, ensuring there are no duplicate entities.

An EmptyCollection is placeholder for an EntityCollection that doesn’t contain any entities.

  • It is iterable, allowing for iteration even though it doesn’t have any entities.

  • It allows for any attribute access that you would typically perform on an EntityCollection, providing flexibility for operations or checks on the collection itself.

Iterables all the way down!

Regardless of the number of entities it contains, whether it’s multiple, single, or none at all, a collection remains iterable. This holds true even when requesting attributes that result in a primitive data type, such as strings. This consistent behavior allows for uniform usage across different scenarios and helps avoid the need for excessive conditional statements.

Transformation, Fetching and Option Handling

The EntityCollection provides you with a lot of convenience for accessing, filtering and transforming containing data.

Item and Attribute Access

Retrieving items from a collection is straightforward and effortless. These examples illustrate the versatility of the item getter on an EntityCollection.

from trackteroid import (
    Query,
    AssetVersion,
    Shot,
    TypedContext
)

av_collection = Query(AssetVersion).get_all(limit=10)
print(av_collection)
# output: EntityCollection[AssetVersion]{10}

# get a new collection only containing the first item
first_av_collection = av_collection[0]
print(first_av_collection)
# output: EntityCollection[AssetVersion]{1}

# get a new collection via slices
last_av_collection = av_collection[-1]
print(last_av_collection)
# output: EntityCollection[AssetVersion]{1}

range_av_collection = av_collection[2:5]
print(range_av_collection)
# output: EntityCollection[AssetVersion]{3}

# get a new collection via some entity id
last_av_id = last_av_collection.id[0]
last_av_collection_from_id = av_collection[last_av_id]
# output: EntityCollection[AssetVersion]{1}

tc_collection = Query(TypedContext).get_all(limit=100)
print(tc_collection)
# output: EntityCollection[TypedContext]{100}

# get a new collection that only contains `Shot` subtypes
sh_collection = tc_collection[Shot]
print(sh_collection)
# output: EntityCollection[Shot]{8}

Accessing related collections and primitive data is user-friendly. This example demonstrates the seamless navigation through nested collections and the retrieval of primitive data stored in the resource_identifier attribute of associated component_locations.

from trackteroid import (
    Query,
    AssetVersion,
    Shot,
    TypedContext
)

av_collection = Query(AssetVersion).get_all(
    limit=1, 
    projections=[
        ComponentLocation.resource_identifier
    ]
)

print(av_collection.ComponentLocation.resource_identifier)
# expanded attribute access would be like this and the result be the same
print(av_collection.components.component_locations.resource_identifier)
# output: 
# [u'/path/to/some_file1.jpg', u'/path/to/some_file2.mov'] 
# [u'/path/to/some_file1.jpg', u'/path/to/some_file2.mov'] 

You can conveniently access individual attributes within the custom_attributes field by utilizing the custom_ prefix as a shortcut. This allows direct access to specific attributes without the need to explicitly refer to the custom_attributes field and retrieve values by their corresponding keys.

from trackteroid import (
    Query,
    Shot
)

print(
    Query(Shot).get_all(limit=2, projections=["custom_attributes"]).custom_frame_start
)
# output: [1009.0, 1006.0]
print(
    Query(Shot).get_all(limit=2, projections=["custom_attributes"]).custom_frame_end
)
# output: [1055.0, 1015.0]

Transformation Methods

While iterating through loops is a valid approach, leveraging transformations can provide enhanced convenience. The EntityCollection class provides higher-order methods that accept functions as arguments, aligning with the principles of functional programming. The presented example highlights a subset of the transformation methods available.

from pprint import pprint

from trackteroid import (
    Query,
    Asset,
    AssetVersion
)

av_collection = Query(AssetVersion).get_all(limit=5, projections=[Asset.name, "version"])

for i, collection in enumerate(av_collection):
    print(i, collection, collection.id)
# output:
# (0, EntityCollection[AssetVersion]{1}, [u'00001180-b7e7-43cf-b0e5-a2df0cefe669'])
# (1, EntityCollection[AssetVersion]{1}, [u'00001fd9-c8b8-4d84-8a8d-2c8fbbed46a0'])
# (2, EntityCollection[AssetVersion]{1}, [u'00004585-b77e-4638-89dc-33ea4dfe7f73'])
# (3, EntityCollection[AssetVersion]{1}, [u'0000482d-f10c-4d00-b2b3-aec57ec8510f'])
# (4, EntityCollection[AssetVersion]{1}, [u'00004a1a-fdd2-11ec-a538-005056a76761'])

# construct a list of strings that are combining the name of the AssetVersion's Asset and it's version number
print(
    av_collection.map(
        lambda avc: "{}_v{:03d}".format(avc.Asset.name[0], avc.version[0])
    )
)
# output: ['SomeAsset_v001', 'SomeAsset_v002', 'SomeCharacter_v004', 'SomeScene_v010', 'AnAssetClone_v002']

# group together AssetVersions by the name of its Asset
pprint(av_collection.group(lambda avc: avc.Asset.name[0]))
# output: 
# {u'SomeAsset': EntityCollection[AssetVersion]{2},
#  u'SomeCharacter': EntityCollection[AssetVersion]{1},
#  u'SomeScene': EntityCollection[AssetVersion]{1},
#  u'AnAssetClone': EntityCollection[AssetVersion]{1}}

# group together AssetVersions by the name of its Asset and 
# associate all of its version numbers with it
pprint(
    av_collection.group_and_map(lambda avc: avc.Asset.name[0], lambda avc: avc.version)
)
# output:
# {u'SomeAsset': [1, 2],
#  u'SomeCharacter': [4],
#  u'SomeScene': [10],
#  u'AnAssetClone': [2]}

# get a tuple with two items
# the first representing a true matching condition and 
# the second the false matching condition
print(av_collection.partition(lambda avc: avc.version[0] == 2))
# output: 
# (EntityCollection[AssetVersion]{2}, EntityCollection[AssetVersion]{3})

Set Operations

Due to the immutability of collections, it is not possible to directly add or remove entities. However, you can utilize the identical set operations available in Python’s set class to obtain new collections.

from trackteroid import (
    Query,
    AssetVersion
)

collection_a = Query(AssetVersion).get(limit=3)
collection_b = Query(AssetVersion).get(limit=3, offset=1)

print(f"a = {collection_a.version}")
# output: 'a = [2, 10, 2]'
print(f"b = {collection_b.version}")
# output: 'b = [10, 2, 9])'

# union
print(f"a + b = {collection_a.union(collection_b).version}")
# output: 'a + b = [2, 10, 2, 9]'

# difference
print(f"a - b = {collection_a.difference(collection_b).version}")
# output: 'a - b = [2]'
print(f"b - a = ", collection_b.difference(collection_a).version)
# output: 'b - a = [9]'

# symmetric difference
print(f"(a - b) + (b - a) = {collection_a.symmetric_difference(collection_b).version}")
# output: '(a - b) + (b - a) = [2, 9]'

# intersection
print(f"(a + b) - ((a - b) + (b - a)) = {collection_a.intersection(collection_b).version}")
# output: '(a + b) - ((a - b) + (b - a)) = [10, 2]'

Fetching Attributes

As Trackteroid’s default Sessions disable the auto-polulate feature, it is possible to work with unprojected data. In such cases, you may need to fetch missing attributes when required. This can be accomplished using the fetch_attributes method on your collection.

from trackteroid import (
    Asset,
    Query,
    Task
)

# assuming you receive a collection from somewhere
some_asset_collection = Query(Asset).by_name(Task, "%").get_all(limit=10)
print(some_asset_collection)
# output: EntityCollection[Asset]{10}

print(
    some_asset_collection.
    fetch_attributes(Task.State.name, "versions").
    filter(
        lambda a: a.versions and a.Task.State.name[0] == "Blocked"
    )
    .Task.State.name
)
# output: ['Blocked']

Fallback Concept

The concept of the EmptyCollection shares similarities with the optional type found in various programming languages. It serves as a mechanism to handle the absence of values or empty results.

Similar to optional types in other languages, the EmptyCollection provides a consistent interface and allows for operations and attribute access without the need for explicit checks for empty or null values. It acts as a container that represents the absence of a value or result.

By utilizing the EmptyCollection, developers can write cleaner and more concise code by treating empty results as a valid state without the need for verbose conditional statements. This promotes a more functional programming style, allowing for seamless chaining and composition of operations even in scenarios where the result might be empty.

Just as optional types in different programming languages offer methods or functions to check for presence or provide fallback values , the EmptyCollection provides a simple fallback functionality to handle cases where the collection is empty as it always evaluates to False.

This demonstrates how you can implement a straightforward fallback mechanism using the or operator when retrieving the final data.

from trackteroid import (
    Asset,
    Query
)

asset_collection = Query(Asset).by_name("DOESNT_EXIST").get_all()
print(asset_collection)
# output: EmptyCollection[Asset]

print(not asset_collection)
# output: True

print(asset_collection or "Oh vey... no results found.")
# output: Oh vey... no results found.

This code example showcases how to gracefully handle scenarios where the intermediate steps of querying, filtering, and retrieving data may result in an empty collection. By utilizing the or operator and providing an empty list as a fallback, we ensure that the final result is either the desired data or an empty list, mitigating the risk of errors or unexpected behavior.

from trackteroid import (
    Asset,
    Query
)

print(
    # The Query result could already be empty.
    # This is more likely when using criteria to filter results when querying.
    Query(Asset).get_first(
        projections=[
            "versions.is_published",
            "versions.user.username"
        ]
    ).
    # An asset could have no versions or at least no versions that have been marked as `is_published`.
    versions.filter(
        lambda avc: avc.is_published[0]
    ).
    user.username
    # Finally, we retrieve the username of the user associated with the filtered versions.
    # If at any point we encounter no results, we can gracefully handle it by providing an empty list as a fallback.
    or []
)

Authoring

CRUD (Create, Read, Update, Delete)

Collections provide a user interface for performing CRUD operations, which include creating, reading, updating, and deleting data. The sections below are organized in a logical order to guide you through these operations.

Read

The listed page references will provide you will all the information when it comes to requesting and accessing data from Ftrack and how the data is being exposed on collections.

  • Querying: Learn how to construct queries to retrieve specific data

  • Attribute Access: Understand how to access attributes of items in collections.

Update

Setting Attributes

Data updates are primarily performed by assigning values using the = operator.

from trackteroid import (
    Query,
    AssetVersion,
    ComponentLocation,
    SESSION
)

collection = Query(AssetVersion).get_all(limit=1, projections=[ComponentLocation.resource_identifier])

print(collection.ComponentLocation.resource_identifier)
# output:
# ['729e05c2-2722-4cc2-97d0-37b4522671d1', 'e26d1b56-c72b-4b9d-8177-6faf47992375', 'ec2203a2-8b5f-11eb-80be-c2ffbce28b68']

collection.ComponentLocation[0].resource_identifier = "new value"
print(collection.ComponentLocation.resource_identifier)
# output:
# ['new value', 'e26d1b56-c72b-4b9d-8177-6faf47992375', 'ec2203a2-8b5f-11eb-80be-c2ffbce28b68']

collection.ComponentLocation.resource_identifier = ["newer value"] * len(collection.ComponentLocation)
print(collection.ComponentLocation.resource_identifier)
# output:
# ['newer value', 'newer value', 'newer value']

# changes are only present in the sessions recorded operation
# in order to send the updates to the Ftrack server you have to commit
collection.commit()

# prove that we updated the Ftrack database accordingly by reconnecting our session instance
SESSION.reconnect()

print(
    Query(AssetVersion).get_all(limit=1, projections=[ComponentLocation.resource_identifier]).
    ComponentLocation.resource_identifier
)
# output: ['newer value', 'newer value', 'newer value']

The provided code example illustrates the process of assigning values to the resource_identifier attribute of ComponentLocation entities associated with an AssetVersion collection.

The code demonstrates two scenarios for updating values: single-value and multi-value assignment. In the case of a single-value assignment, a string value is assigned to ComponentLocation[0].resource_identifier, assuming that we are dealing with a collection containing a single element. This operation is possible when the collection has only one element. On the other hand, in the list assignment scenario, a list of values is assigned to the ComponentLocation.resource_identifier attribute, with each value corresponding to an element in the collection. It is crucial to ensure that the number of elements in the list matches the number of elements in the collection.

Attention

It’s important to note that updates made to the collection are only stored in the local cache until they are committed. The commit() method can be called on any collection and will commit all recorded operations from the underlying session to the Ftrack server. To verify the success of the update in the example, the code reconnects the session and retrieves the updated attribute value by executing a new query.

Tip

The apply method provides a convenient approach when you need to assign a single value or a single-element collection to a collection that has multiple receivers.

Create

The create(**kwargs) method on a collection enables the creation of new entities, providing a new collection that allows for additional operations on the created entities. The required keyword arguments for this method vary depending on the entity type being created.

from trackteroid import (
    Query,
    AssetVersion,
    Note,
    NoteCategory,
    User
)

user = Query(User).by_name("aniela.morin@example.com").get_all(projections=["username"])
category = Query(NoteCategory).by_name("Internal").get_all(projections=["name"])

assetversion = Query(AssetVersion).by_id("6a229598-6596-11ed-a73a-92ba0fc0dc3d").get_one(
    projections=[Note, Note.content, Note.NoteCategory.name]
)

print(
    assetversion.Note.group_and_map(
        lambda note: note.NoteCategory.name[0],
        lambda note: note.content
    )
)
# output:
# {'Client feedback': ['The direction looks good!']}

new_note = assetversion.Note.create(
    author=user,
    content="Thanks!",
    is_todo=False,
    category=category
)
assetversion.Note = assetversion.Note.union(new_note)
print(
    assetversion.Note.group_and_map(
        lambda note: note.NoteCategory.name[0],
        lambda note: note.content
    )
)
# output:
# {'Client feedback': ['The direction looks good!'], 'Internal': ['Thanks!']}

assetversion.commit()

The code example showcases the process of adding a new note to an existing collection of notes within an AssetVersion collection. Since collections are immutable and do not allow entities to be added or removed from an existing collection directly, the create method returns a new Note collection that solely contains the newly created note.

To preserve the existing notes, the code performs a union operation between the original Note collection and the newly created collection. This combined collection is then assigned back, ensuring that both the existing notes and the newly created note are included.

Attention

It’s important to note that creation and updates made to the collection are only stored in the local cache until they are committed. The commit() method can be called on any collection and will commit all recorded operations from the underlying session to the Ftrack server.

Linking

The AssetVersion collection offers a convenient way to link entities to each other using the uses_versions and used_in_versions attribute types. Additionally, collections can be easily linked or unlinked from each other by utilizing the following methods:

  • link_inputs(collection)

  • link_outputs(collection)

  • unlink_inputs(collection)

  • unlink_outputs(collection)

Attention

The linking process involves dedicated *Link types, and using link_inputs and link_outputs will create new link objects with appropriate assignments. Conversely, unlink_inputs and unlink_outputs are used to delete these link objects.

from trackteroid import (
    Query,
    AssetBuild,
    SESSION
)

assetbuild_collection = Query(AssetBuild).get_all(
    limit=3,
    projections=[
        "name",
        "incoming_links.from",
        "incoming_links.from.name",
        "outgoing_links.to"]
)
another_assetbuild_collection = Query(AssetBuild).get_all(
    limit=1,
    offset=3,
    projections=["name"]
)

print(
    f"assetbuilds: {assetbuild_collection.name}\n"
    f"another assetbuild: {another_assetbuild_collection.name}\n"
    f"incoming: {getattr(assetbuild_collection[1].incoming_links, 'from').name or 'nothing linked as input yet'}\n"
    f"outgoing: {getattr(assetbuild_collection[1].outgoing_links, 'to').name or 'nothing linked as output yet'}\n"
)
# output:
# assetbuilds: ['Small Round Wooden Table 01', 'Side Table Tall 01', 'Steel Frame Shelves 02']
# another assetbuild: ['Steel Frame Shelves 01']
# incoming: nothing linked as input yet
# outgoing: nothing linked as output yet

# create links with proper assignment and commit changes to database
assetbuild_collection[1].\
    link_inputs(assetbuild_collection[0]).\
    link_outputs(assetbuild_collection[2]).\
    commit()

SESSION.reconnect()

assetbuild_collection = Query(AssetBuild).get_all(
    limit=3,
    projections=[
        "name",
        "incoming_links.from",
        "incoming_links.from.name",
        "outgoing_links.to"
    ]
)
# verify our changes arrived properly in the database
print(
    f"incoming: {getattr(assetbuild_collection[1].incoming_links, 'from').name or 'nothing linked as input yet'}\n"
    f"outgoing: {getattr(assetbuild_collection[1].outgoing_links, 'to').name or 'nothing linked as output yet'}\n"
)
# # output:
# incoming: ['Small Round Wooden Table 01']
# outgoing: ['Steel Frame Shelves 02']

# extend links
assetbuild_collection[1].link_inputs(another_assetbuild_collection).commit()

SESSION.reconnect()

assetbuild_collection = Query(AssetBuild).get_all(
    limit=3,
    projections=[
        "name",
        "incoming_links.from",
        "incoming_links.from.name",
        "outgoing_links.to"
    ]
)

# verify our changes arrived properly in the database
print(
    f"incoming: {getattr(assetbuild_collection[1].incoming_links, 'from').name or 'nothing linked as input yet'}\n"
)
# output:
# incoming: ['Small Round Wooden Table 01', 'Steel Frame Shelves 01']

Delete

The delete() method available on a collection provides the capability to delete entities associated with that collection.

from trackteroid import (
    Query,
    AssetVersion,
    SESSION
)

_id = "405c569e-8bfa-11eb-ae63-c2ffbce28b68"

Query(AssetVersion).by_id(_id).get_one().delete().commit()

SESSION.reconnect()

# will result in error as the entity doesn't exist anymore
# ftrack_api.exception.NoResultFoundError: 
# No result found for 'select id, asset.name, version from AssetVersion where (id is "405c569e-8bfa-11eb-ae63-c2ffbce28b68")'
#
Query(AssetVersion).by_id(_id).get_one()

Attention

It’s important to note that of a collection is only stored in the local cache until the changes are committed. The commit() method can be called on any collection and will commit all recorded operations from the underlying session to the Ftrack server. To verify the success of the deletion in the example, the code reconnects the session and retrieves the updated attribute value by executing a new query.