Merged in feature/update_py2_ftrack_api (pull request #449)

Added new ftrack_api_old

Approved-by: Milan Kolar <milan@orbi.tools>
This commit is contained in:
Jakub Trllo 2020-01-27 11:09:16 +00:00 committed by Milan Kolar
commit 3466ab63ea
11 changed files with 358 additions and 64 deletions

View file

@ -1 +1 @@
__version__ = '1.3.3'
__version__ = '1.8.2'

66
pype/vendor/ftrack_api_old/_weakref.py vendored Normal file
View file

@ -0,0 +1,66 @@
"""
Yet another backport of WeakMethod for Python 2.7.
Changes include removing exception chaining and adding args to super() calls.
Copyright (c) 2001-2019 Python Software Foundation.All rights reserved.
Full license available in LICENSE.python.
"""
from weakref import ref
class WeakMethod(ref):
"""
A custom `weakref.ref` subclass which simulates a weak reference to
a bound method, working around the lifetime problem of bound methods.
"""
__slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__"
def __new__(cls, meth, callback=None):
try:
obj = meth.__self__
func = meth.__func__
except AttributeError:
raise TypeError(
"argument should be a bound method, not {}".format(type(meth))
)
def _cb(arg):
# The self-weakref trick is needed to avoid creating a reference
# cycle.
self = self_wr()
if self._alive:
self._alive = False
if callback is not None:
callback(self)
self = ref.__new__(cls, obj, _cb)
self._func_ref = ref(func, _cb)
self._meth_type = type(meth)
self._alive = True
self_wr = ref(self)
return self
def __call__(self):
obj = super(WeakMethod, self).__call__()
func = self._func_ref()
if obj is None or func is None:
return None
return self._meth_type(func, obj)
def __eq__(self, other):
if isinstance(other, WeakMethod):
if not self._alive or not other._alive:
return self is other
return ref.__eq__(self, other) and self._func_ref == other._func_ref
return NotImplemented
def __ne__(self, other):
if isinstance(other, WeakMethod):
if not self._alive or not other._alive:
return self is not other
return ref.__ne__(self, other) or self._func_ref != other._func_ref
return NotImplemented
__hash__ = ref.__hash__

View file

@ -148,7 +148,8 @@ class Attribute(object):
'''A name and value pair persisted remotely.'''
def __init__(
self, name, default_value=ftrack_api_old.symbol.NOT_SET, mutable=True
self, name, default_value=ftrack_api_old.symbol.NOT_SET, mutable=True,
computed=False
):
'''Initialise attribute with *name*.
@ -161,10 +162,14 @@ class Attribute(object):
are :attr:`ftrack_api_old.symbol.NOT_SET`. The exception to this is when the
target value is also :attr:`ftrack_api_old.symbol.NOT_SET`.
If *computed* is set to True the value is a remote side computed value
and should not be long-term cached.
'''
super(Attribute, self).__init__()
self._name = name
self._mutable = mutable
self._computed = computed
self.default_value = default_value
self._local_key = 'local'
@ -205,6 +210,11 @@ class Attribute(object):
'''Return whether attribute is mutable.'''
return self._mutable
@property
def computed(self):
'''Return whether attribute is computed.'''
return self._computed
def get_value(self, entity):
'''Return current value for *entity*.

View file

@ -49,9 +49,11 @@ class Factory(object):
# Build attributes for class.
attributes = ftrack_api_old.attribute.Attributes()
immutable = schema.get('immutable', [])
immutable_properties = schema.get('immutable', [])
computed_properties = schema.get('computed', [])
for name, fragment in schema.get('properties', {}).items():
mutable = name not in immutable
mutable = name not in immutable_properties
computed = name in computed_properties
default = fragment.get('default', ftrack_api_old.symbol.NOT_SET)
if default == '{uid}':
@ -62,7 +64,8 @@ class Factory(object):
if data_type is not ftrack_api_old.symbol.NOT_SET:
if data_type in (
'string', 'boolean', 'integer', 'number', 'variable'
'string', 'boolean', 'integer', 'number', 'variable',
'object'
):
# Basic scalar attribute.
if data_type == 'number':
@ -74,7 +77,7 @@ class Factory(object):
data_type = 'datetime'
attribute = self.create_scalar_attribute(
class_name, name, mutable, default, data_type
class_name, name, mutable, computed, default, data_type
)
if attribute:
attributes.add(attribute)
@ -139,11 +142,12 @@ class Factory(object):
return cls
def create_scalar_attribute(
self, class_name, name, mutable, default, data_type
self, class_name, name, mutable, computed, default, data_type
):
'''Return appropriate scalar attribute instance.'''
return ftrack_api_old.attribute.ScalarAttribute(
name, data_type=data_type, default_value=default, mutable=mutable
name, data_type=data_type, default_value=default, mutable=mutable,
computed=computed
)
def create_reference_attribute(self, class_name, name, mutable, reference):

View file

@ -526,7 +526,8 @@ class Location(ftrack_api_old.entity.base.Entity):
for index, resource_identifier in enumerate(resource_identifiers):
resource_identifiers[index] = (
self.resource_identifier_transformer.decode(
resource_identifier
resource_identifier,
context={'component': components[index]}
)
)

View file

@ -1,6 +1,8 @@
# :coding: utf-8
# :copyright: Copyright (c) 2015 ftrack
import warnings
import ftrack_api_old.entity.base
@ -33,26 +35,52 @@ class Note(ftrack_api_old.entity.base.Entity):
class CreateNoteMixin(object):
'''Mixin to add create_note method on entity class.'''
def create_note(self, content, author, recipients=None, category=None):
def create_note(
self, content, author, recipients=None, category=None, labels=None
):
'''Create note with *content*, *author*.
Note category can be set by including *category* and *recipients*
can be specified as a list of user or group instances.
NoteLabels can be set by including *labels*.
Note category can be set by including *category*.
*recipients* can be specified as a list of user or group instances.
'''
note_label_support = 'NoteLabel' in self.session.types
if not labels:
labels = []
if labels and not note_label_support:
raise ValueError(
'NoteLabel is not supported by the current server version.'
)
if category and labels:
raise ValueError(
'Both category and labels cannot be set at the same time.'
)
if not recipients:
recipients = []
category_id = None
if category:
category_id = category['id']
data = {
'content': content,
'author': author,
'category_id': category_id
'author': author
}
if category:
if note_label_support:
labels = [category]
warnings.warn(
'category argument will be removed in an upcoming version, '
'please use labels instead.',
PendingDeprecationWarning
)
else:
data['category_id'] = category['id']
note = self.session.create('Note', data)
self['notes'].append(note)
@ -65,4 +93,13 @@ class CreateNoteMixin(object):
note['recipients'].append(recipient)
for label in labels:
self.session.create(
'NoteLabelLink',
{
'label_id': label['id'],
'note_id': note['id']
}
)
return note

View file

@ -3,14 +3,15 @@
from operator import eq, ne, ge, le, gt, lt
from pyparsing import (ParserElement, Group, Word, CaselessKeyword, Forward,
from pyparsing import (Group, Word, CaselessKeyword, Forward,
FollowedBy, Suppress, oneOf, OneOrMore, Optional,
alphanums, quotedString, removeQuotes)
import ftrack_api_old.exception
# Optimise parsing using packrat memoisation feature.
ParserElement.enablePackrat()
# Do not enable packrat since it is not thread-safe and will result in parsing
# exceptions in a multi threaded environment.
# ParserElement.enablePackrat()
class Parser(object):

View file

@ -14,6 +14,7 @@ import operator
import functools
import json
import socket
import warnings
import requests
import requests.exceptions
@ -40,9 +41,20 @@ ServerDetails = collections.namedtuple('ServerDetails', [
])
class EventHub(object):
'''Manage routing of events.'''
_future_signature_warning = (
'When constructing your Session object you did not explicitly define '
'auto_connect_event_hub as True even though you appear to be publishing '
'and / or subscribing to asynchronous events. In version version 2.0 of '
'the ftrack-python-api the default behavior will change from True '
'to False. Please make sure to update your tools. You can read more at '
'http://ftrack-python-api.rtd.ftrack.com/en/stable/release/migration.html'
)
def __init__(self, server_url, api_user, api_key):
'''Initialise hub, connecting to ftrack *server_url*.
@ -76,6 +88,8 @@ class EventHub(object):
self._auto_reconnect_attempts = 30
self._auto_reconnect_delay = 10
self._deprecation_warning_auto_connect = False
# Mapping of Socket.IO codes to meaning.
self._code_name_mapping = {
'0': 'disconnect',
@ -134,6 +148,9 @@ class EventHub(object):
connected or connection fails.
'''
self._deprecation_warning_auto_connect = False
if self.connected:
raise ftrack_api_old.exception.EventHubConnectionError(
'Already connected.'
@ -164,17 +181,26 @@ class EventHub(object):
# https://docs.python.org/2/library/socket.html#socket.socket.setblocking
self._connection = websocket.create_connection(url, timeout=60)
except Exception:
except Exception as error:
error_message = (
'Failed to connect to event server at {server_url} with '
'error: "{error}".'
)
error_details = {
'error': unicode(error),
'server_url': self.get_server_url()
}
self.logger.debug(
L(
'Error connecting to event server at {0}.',
self.get_server_url()
error_message, **error_details
),
exc_info=1
)
raise ftrack_api_old.exception.EventHubConnectionError(
'Failed to connect to event server at {0}.'
.format(self.get_server_url())
error_message,
details=error_details
)
# Start background processing thread.
@ -543,6 +569,11 @@ class EventHub(object):
event will be caught by this method and ignored.
'''
if self._deprecation_warning_auto_connect and not synchronous:
warnings.warn(
self._future_signature_warning, FutureWarning
)
try:
return self._publish(
event, synchronous=synchronous, on_reply=on_reply
@ -700,18 +731,23 @@ class EventHub(object):
# Automatically publish a non None response as a reply when not in
# synchronous mode.
if not synchronous and response is not None:
try:
self.publish_reply(
event, data=response, source=subscriber.metadata
if not synchronous:
if self._deprecation_warning_auto_connect:
warnings.warn(
self._future_signature_warning, FutureWarning
)
except Exception:
self.logger.exception(L(
'Error publishing response {0} from subscriber {1} '
'for event {2}.', response, subscriber, event
))
if response is not None:
try:
self.publish_reply(
event, data=response, source=subscriber.metadata
)
except Exception:
self.logger.exception(L(
'Error publishing response {0} from subscriber {1} '
'for event {2}.', response, subscriber, event
))
# Check whether to continue processing topic event.
if event.is_stopped():
@ -881,6 +917,7 @@ class EventHub(object):
if code_name == 'connect':
self.logger.debug('Connected to event server.')
event = ftrack_api_old.event.base.Event('ftrack.meta.connected')
self._prepare_event(event)
self._event_queue.put(event)
elif code_name == 'disconnect':
@ -901,6 +938,7 @@ class EventHub(object):
if not self.connected:
event = ftrack_api_old.event.base.Event('ftrack.meta.disconnected')
self._prepare_event(event)
self._event_queue.put(event)
elif code_name == 'heartbeat':

View file

@ -1,6 +1,23 @@
# :coding: utf-8
# :copyright: Copyright (c) 2016 ftrack
import functools
import warnings
def deprecation_warning(message):
def decorator(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
warnings.warn(
message,
PendingDeprecationWarning
)
return function(*args, **kwargs)
return wrapper
return decorator
class LazyLogMessage(object):
'''A log message that can be evaluated lazily for improved performance.

View file

@ -16,6 +16,7 @@ import hashlib
import tempfile
import threading
import atexit
import warnings
import requests
import requests.auth
@ -42,8 +43,14 @@ import ftrack_api_old.structure.origin
import ftrack_api_old.structure.entity_id
import ftrack_api_old.accessor.server
import ftrack_api_old._centralized_storage_scenario
import ftrack_api_old.logging
from ftrack_api_old.logging import LazyLogMessage as L
try:
from weakref import WeakMethod
except ImportError:
from ftrack_api_old._weakref import WeakMethod
class SessionAuthentication(requests.auth.AuthBase):
'''Attach ftrack session authentication information to requests.'''
@ -69,7 +76,7 @@ class Session(object):
def __init__(
self, server_url=None, api_key=None, api_user=None, auto_populate=True,
plugin_paths=None, cache=None, cache_key_maker=None,
auto_connect_event_hub=True, schema_cache_path=None,
auto_connect_event_hub=None, schema_cache_path=None,
plugin_arguments=None
):
'''Initialise session.
@ -233,7 +240,8 @@ class Session(object):
self._api_key
)
if auto_connect_event_hub:
self._auto_connect_event_hub_thread = None
if auto_connect_event_hub in (None, True):
# Connect to event hub in background thread so as not to block main
# session usage waiting for event hub connection.
self._auto_connect_event_hub_thread = threading.Thread(
@ -242,8 +250,14 @@ class Session(object):
self._auto_connect_event_hub_thread.daemon = True
self._auto_connect_event_hub_thread.start()
# To help with migration from auto_connect_event_hub default changing
# from True to False.
self._event_hub._deprecation_warning_auto_connect = (
auto_connect_event_hub is None
)
# Register to auto-close session on exit.
atexit.register(self.close)
atexit.register(WeakMethod(self.close))
self._plugin_paths = plugin_paths
if self._plugin_paths is None:
@ -271,6 +285,15 @@ class Session(object):
ftrack_api_old._centralized_storage_scenario.register(self)
self._configure_locations()
self.event_hub.publish(
ftrack_api_old.event.base.Event(
topic='ftrack.api.session.ready',
data=dict(
session=self
)
),
synchronous=True
)
def __enter__(self):
'''Return session as context manager.'''
@ -389,7 +412,8 @@ class Session(object):
try:
self.event_hub.disconnect()
self._auto_connect_event_hub_thread.join()
if self._auto_connect_event_hub_thread:
self._auto_connect_event_hub_thread.join()
except ftrack_api_old.exception.EventHubConnectionError:
pass
@ -428,6 +452,16 @@ class Session(object):
# Re-configure certain session aspects that may be dependant on cache.
self._configure_locations()
self.event_hub.publish(
ftrack_api_old.event.base.Event(
topic='ftrack.api.session.reset',
data=dict(
session=self
)
),
synchronous=True
)
def auto_populating(self, auto_populate):
'''Temporarily set auto populate to *auto_populate*.
@ -508,7 +542,7 @@ class Session(object):
'entity_key': entity.get('id')
})
result = self._call(
result = self.call(
[payload]
)
@ -790,12 +824,13 @@ class Session(object):
}]
# TODO: When should this execute? How to handle background=True?
results = self._call(batch)
results = self.call(batch)
# Merge entities into local cache and return merged entities.
data = []
merged = dict()
for entity in results[0]['data']:
data.append(self.merge(entity))
data.append(self._merge_recursive(entity, merged))
return data, results[0]['metadata']
@ -856,6 +891,48 @@ class Session(object):
else:
return value
def _merge_recursive(self, entity, merged=None):
'''Merge *entity* and all its attributes recursivly.'''
log_debug = self.logger.isEnabledFor(logging.DEBUG)
if merged is None:
merged = {}
attached = self.merge(entity, merged)
for attribute in entity.attributes:
# Remote attributes.
remote_value = attribute.get_remote_value(entity)
if isinstance(
remote_value,
(
ftrack_api_old.entity.base.Entity,
ftrack_api_old.collection.Collection,
ftrack_api_old.collection.MappedCollectionProxy
)
):
log_debug and self.logger.debug(
'Merging remote value for attribute {0}.'.format(attribute)
)
if isinstance(remote_value, ftrack_api_old.entity.base.Entity):
self._merge_recursive(remote_value, merged=merged)
elif isinstance(
remote_value, ftrack_api_old.collection.Collection
):
for entry in remote_value:
self._merge_recursive(entry, merged=merged)
elif isinstance(
remote_value, ftrack_api_old.collection.MappedCollectionProxy
):
for entry in remote_value.collection:
self._merge_recursive(entry, merged=merged)
return attached
def _merge_entity(self, entity, merged=None):
'''Merge *entity* into session returning merged entity.
@ -1185,7 +1262,7 @@ class Session(object):
# Process batch.
if batch:
result = self._call(batch)
result = self.call(batch)
# Clear recorded operations.
self.recorded_operations.clear()
@ -1260,7 +1337,7 @@ class Session(object):
def _fetch_server_information(self):
'''Return server information.'''
result = self._call([{'action': 'query_server_information'}])
result = self.call([{'action': 'query_server_information'}])
return result[0]
def _discover_plugins(self, plugin_arguments=None):
@ -1362,7 +1439,7 @@ class Session(object):
'Loading schemas from server due to hash not matching.'
'Local: {0!r} != Server: {1!r}', local_schema_hash, server_hash
))
schemas = self._call([{'action': 'query_schemas'}])[0]
schemas = self.call([{'action': 'query_schemas'}])[0]
if schema_cache_path:
try:
@ -1525,8 +1602,24 @@ class Session(object):
synchronous=True
)
@ftrack_api_old.logging.deprecation_warning(
'Session._call is now available as public method Session.call. The '
'private method will be removed in version 2.0.'
)
def _call(self, data):
'''Make request to server with *data*.'''
'''Make request to server with *data* batch describing the actions.
.. note::
This private method is now available as public method
:meth:`entity_reference`. This alias remains for backwards
compatibility, but will be removed in version 2.0.
'''
return self.call(data)
def call(self, data):
'''Make request to server with *data* batch describing the actions.'''
url = self._server_url + '/api'
headers = {
'content-type': 'application/json',
@ -1553,7 +1646,7 @@ class Session(object):
'Server reported error in unexpected format. Raw error was: {0}'
.format(response.text)
)
self.logger.error(error_message)
self.logger.exception(error_message)
raise ftrack_api_old.exception.ServerError(error_message)
else:
@ -1562,7 +1655,7 @@ class Session(object):
error_message = 'Server reported error: {0}({1})'.format(
result['exception'], result['content']
)
self.logger.error(error_message)
self.logger.exception(error_message)
raise ftrack_api_old.exception.ServerError(error_message)
return result
@ -1620,12 +1713,12 @@ class Session(object):
if "entity_data" in data:
for key, value in data["entity_data"].items():
if isinstance(value, ftrack_api_old.entity.base.Entity):
data["entity_data"][key] = self._entity_reference(value)
data["entity_data"][key] = self.entity_reference(value)
return data
if isinstance(item, ftrack_api_old.entity.base.Entity):
data = self._entity_reference(item)
data = self.entity_reference(item)
with self.auto_populating(True):
@ -1646,14 +1739,15 @@ class Session(object):
value = attribute.get_local_value(item)
elif entity_attribute_strategy == 'persisted_only':
value = attribute.get_remote_value(item)
if not attribute.computed:
value = attribute.get_remote_value(item)
if value is not ftrack_api_old.symbol.NOT_SET:
if isinstance(
attribute, ftrack_api_old.attribute.ReferenceAttribute
):
if isinstance(value, ftrack_api_old.entity.base.Entity):
value = self._entity_reference(value)
value = self.entity_reference(value)
data[attribute.name] = value
@ -1668,14 +1762,14 @@ class Session(object):
if isinstance(item, ftrack_api_old.collection.Collection):
data = []
for entity in item:
data.append(self._entity_reference(entity))
data.append(self.entity_reference(entity))
return data
raise TypeError('{0!r} is not JSON serializable'.format(item))
def _entity_reference(self, entity):
'''Return reference to *entity*.
def entity_reference(self, entity):
'''Return entity reference that uniquely identifies *entity*.
Return a mapping containing the __entity_type__ of the entity along with
the key, value pairs that make up it's primary key.
@ -1689,6 +1783,26 @@ class Session(object):
return reference
@ftrack_api_old.logging.deprecation_warning(
'Session._entity_reference is now available as public method '
'Session.entity_reference. The private method will be removed '
'in version 2.0.'
)
def _entity_reference(self, entity):
'''Return entity reference that uniquely identifies *entity*.
Return a mapping containing the __entity_type__ of the entity along
with the key, value pairs that make up it's primary key.
.. note::
This private method is now available as public method
:meth:`entity_reference`. This alias remains for backwards
compatibility, but will be removed in version 2.0.
'''
return self.entity_reference(entity)
def decode(self, string):
'''Return decoded JSON *string* as Python object.'''
with self.operation_recording(False):
@ -2016,6 +2130,10 @@ class Session(object):
return availabilities
@ftrack_api_old.logging.deprecation_warning(
'Session.delayed_job has been deprecated in favour of session.call. '
'Please refer to the release notes for more information.'
)
def delayed_job(self, job_type):
'''Execute a delayed job on the server, a `ftrack.entity.job.Job` is returned.
@ -2033,7 +2151,7 @@ class Session(object):
}
try:
result = self._call(
result = self.call(
[operation]
)[0]
@ -2070,7 +2188,7 @@ class Session(object):
)
try:
result = self._call([operation])
result = self.call([operation])
except ftrack_api_old.exception.ServerError as error:
# Raise informative error if the action is not supported.
@ -2172,7 +2290,7 @@ class Session(object):
}
try:
result = self._call([operation])
result = self.call([operation])
except ftrack_api_old.exception.ServerError as error:
# Raise informative error if the action is not supported.
@ -2212,7 +2330,7 @@ class Session(object):
}
try:
result = self._call([operation])
result = self.call([operation])
except ftrack_api_old.exception.ServerError as error:
# Raise informative error if the action is not supported.
@ -2258,7 +2376,7 @@ class Session(object):
)
try:
self._call(operations)
self.call(operations)
except ftrack_api_old.exception.ServerError as error:
# Raise informative error if the action is not supported.
@ -2306,7 +2424,7 @@ class Session(object):
)
try:
self._call(operations)
self.call(operations)
except ftrack_api_old.exception.ServerError as error:
# Raise informative error if the action is not supported.
if 'Invalid action u\'send_review_session_invite\'' in error.message:

View file

@ -1,6 +1,8 @@
# :coding: utf-8
# :copyright: Copyright (c) 2014 ftrack
import os
class Symbol(object):
'''A constant symbol.'''
@ -68,8 +70,8 @@ CONNECT_LOCATION_ID = '07b82a97-8cf9-11e3-9383-20c9d081909b'
#: Identifier of builtin server location.
SERVER_LOCATION_ID = '3a372bde-05bc-11e4-8908-20c9d081909b'
#: Chunk size used when working with data.
CHUNK_SIZE = 8192
#: Chunk size used when working with data, default to 1Mb.
CHUNK_SIZE = int(os.getenv('FTRACK_API_FILE_CHUNK_SIZE', 0)) or 1024*1024
#: Symbol representing syncing users with ldap
JOB_SYNC_USERS_LDAP = Symbol('SYNC_USERS_LDAP')