Caching

The API makes use of caching in order to provide more efficient retrieval of data by reducing the number of calls to the remote server:

# First call to retrieve user performs a request to the server.
user = session.get('User', 'some-user-id')

# A later call in the same session to retrieve the same user just gets
# the existing instance from the cache without a request to the server.
user = session.get('User', 'some-user-id')

It also seamlessly merges related data together regardless of how it was retrieved:

>>> timelog = user['timelogs'][0]
>>> with session.auto_populating(False):
>>>     print timelog['comment']
NOT_SET
>>> session.query(
...     'select comment from Timelog where id is "{0}"'
...     .format(timelog['id'])
... ).all()
>>> with session.auto_populating(False):
>>>     print timelog['comment']
'Some comment'

By default, each Session is configured with a simple MemoryCache() and the cache is lost as soon as the session expires.

Configuring a session cache

It is possible to configure the cache that a session uses. An example would be a persistent auto-populated cache that survives between sessions:

import os
import ftrack_api.cache

# Specify where the file based cache should be stored.
cache_path = os.path.join(tempfile.gettempdir(), 'ftrack_session_cache.dbm')


# Define a cache maker that returns a file based cache. Note that a
# function is used because the file based cache should use the session's
# encode and decode methods to serialise the entity data to a format that
# can be written to disk (JSON).
def cache_maker(session):
    '''Return cache to use for *session*.'''
    return ftrack_api.cache.SerialisedCache(
        ftrack_api.cache.FileCache(cache_path),
        encode=session.encode,
        decode=session.decode
    )

# Create the session using the cache maker.
session = ftrack_api.Session(cache=cache_maker)

Note

There can be a performance penalty when using a more complex cache setup. For example, serialising data and also writing and reading from disk can be relatively slow operations.

Regardless of the cache specified, the session will always construct a LayeredCache with a MemoryCache at the top level and then your cache at the second level. This is to ensure consistency of instances returned by the session.

You can check (or even modify) at any time what cache configuration a session is using by accessing the cache attribute on a Session:

>>> print session.cache
<ftrack_api.cache.LayeredCache object at 0x0000000002F64400>

Writing a new cache interface

If you have a custom cache backend you should be able to integrate it into the system by writing a cache interface that matches the one defined by ftrack_api.cache.Cache. This typically involves a subclass and overriding the get(), set() and remove() methods.

Managing what gets cached

The cache system is quite flexible when it comes to controlling what should be cached.

Consider you have a layered cache where the bottom layer cache should be persisted between sessions. In this setup you probably don’t want the persisted cache to hold non-persisted values, such as modified entity values or newly created entities not yet committed to the server. However, you might want the top level memory cache to hold onto these values.

Here is one way to set this up. First define a new proxy cache that is selective about what it sets:

import ftrack_api.inspection


class SelectiveCache(ftrack_api.cache.ProxyCache):
    '''Proxy cache that won't cache newly created entities.'''

    def set(self, key, value):
        '''Set *value* for *key*.'''
        if isinstance(value, ftrack_api.entity.base.Entity):
            if (
                ftrack_api.inspection.state(value)
                is ftrack_api.symbol.CREATED
            ):
                return

        super(SelectiveCache, self).set(key, value)

Now use this custom cache to wrap the serialised cache in the setup above:

def cache_maker(session):
    '''Return cache to use for *session*.'''
    return SelectiveCache(
        ftrack_api.cache.SerialisedCache(
            ftrack_api.cache.FileCache(cache_path),
            encode=session.encode,
            decode=session.decode
        )
    )

Now to prevent modified attributes also being persisted, tweak the encode settings for the file cache:

import functools


def cache_maker(session):
    '''Return cache to use for *session*.'''
    return SelectiveCache(
        ftrack_api.cache.SerialisedCache(
            ftrack_api.cache.FileCache(cache_path),
            encode=functools.partial(
                session.encode,
                entity_attribute_strategy='persisted_only'
            ),
            decode=session.decode
        )
    )

And use the updated cache maker for your session:

session = ftrack_api.Session(cache=cache_maker)

Note

For some type of attributes that are computed, long term caching is not recommended and such values will not be encoded with the persisted_only strategy.