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.
In practice…
from trackteroid import (
Query,
Component,
ComponentLocation,
TypedContext
)
print(
Query(TypedContext).get_all(
limit=1, projections=[Component.name, ComponentLocation.resource_identifier]
)[Shot] \
.filter(lambda shot: shot.Component.name[0] == "main") \
.ComponentLocation.resource_identifier[0] \
or "Not existing"
)
print(
Query(TypedContext).get_all(
limit=10, projections=[Component.name, ComponentLocation.resource_identifier]
)[Shot] \
.filter(lambda shot: shot.Component.name[0] == "main") \
.ComponentLocation.resource_identifier \
or "Still not existing, even for multiple results"
)
The provided code demonstrates some of the capabilities of Trackteroid in handling complex scenarios without the need for explicit loops or excessive conditional statements. Although the code may seem complex at first glance, the following explanations will break it down step by step.
In the first part of the code, a single TypedContext sample is retrieved using the Query class. The limit parameter is set to 1 to fetch only one sample. The projections parameter is used to specify the desired attributes (Component.name and ComponentLocation.resource_identifier) to be included in the result.
Next, the retrieved TypedContext sample is filtered using the [Shot] filter. This filter selects only the subtypes of TypedContext that match the Shot entity and ensures that only Shot entities are considered in the subsequent operations. Since we are limiting the result of the TypedContext query to only one entity, there is a possibility that the retrieved entity may not be a Shot. It could be of a different entity type, such as AssetBuild, Sequence, or Folder. Following the filter, the Shot sample is further filtered using the filter method. In this case, the filter condition checks if the Component name of any of the Shot’s AssetVersions is equal to “main”. Finally, the resource_identifier attribute of a single ComponentLocation is accessed. As we are anticipating only one result, the value is accessed using the [0] index. If a value is present, it is utilized; otherwise, the fallback string “Not existing” is used.
The second part of the code follows a similar structure, but this time the limit parameter is set to 10 to retrieve 10 potential TypedContext samples.
No need to worry if you haven’t fully grasped the concepts yet. Subsequent sections will provide further clarification.
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 []
)