Exemple #1
0
    def run(self):
        '''Perform work in thread.'''
        while not self.done.is_set():
            try:
                code, packet_identifier, path, data = self.client._receive_packet(
                )
                self.client._handle_packet(code, packet_identifier, path, data)

            except ftrack_api.exception.EventHubPacketError as error:
                self.logger.debug(L('Ignoring invalid packet: {0}', error))
                continue

            except ftrack_api.exception.EventHubConnectionError:
                self.cancel()

                # Fake a disconnection event in order to trigger reconnection
                # when necessary.
                self.client._handle_packet('0', '', '', '')

                break

            except Exception as error:
                self.logger.debug(L('Aborting processor thread: {0}', error))
                self.cancel()
                break
Exemple #2
0
    def __init__(self, session, data=None, reconstructing=False):
        '''Initialise entity.

        *session* is an instance of :class:`ftrack_api.session.Session` that
        this entity instance is bound to.

        *data* is a mapping of key, value pairs to apply as initial attribute
        values.

        *reconstructing* indicates whether this entity is being reconstructed,
        such as from a query, and therefore should not have any special creation
        logic applied, such as initialising defaults for missing data.

        '''
        super(Entity, self).__init__()
        self.logger = logging.getLogger(__name__ + '.' +
                                        self.__class__.__name__)
        self.session = session
        self._inflated = set()

        if data is None:
            data = {}

        self.logger.debug(
            L('{0} entity from {1!r}.',
              ('Reconstructing' if reconstructing else 'Constructing'), data))

        self._ignore_data_keys = ['__entity_type__']
        if not reconstructing:
            self._construct(data)
        else:
            self._reconstruct(data)
Exemple #3
0
    def load_events(self):
        """Load not processed events sorted by stored date"""
        ago_date = datetime.datetime.now() - datetime.timedelta(days=3)
        result = self.dbcon.delete_many({
            "pype_data.stored": {
                "$lte": ago_date
            },
            "pype_data.is_processed": True
        })

        not_processed_events = self.dbcon.find({
            "pype_data.is_processed": False
        }).sort([("pype_data.stored", pymongo.ASCENDING)])

        found = False
        for event_data in not_processed_events:
            new_event_data = {
                k: v
                for k, v in event_data.items()
                if k not in ["_id", "pype_data"]
            }
            try:
                event = ftrack_api.event.base.Event(**new_event_data)
            except Exception:
                self.logger.exception(
                    L('Failed to convert payload into event: {0}', event_data))
                continue
            found = True
            self._event_queue.put(event)

        return found
Exemple #4
0
    def unsubscribe(self, subscriber_identifier):
        '''Unsubscribe subscriber with *subscriber_identifier*.

        .. note::

            If the server is not reachable then it won't be notified of the
            unsubscription. However, the subscriber will be removed locally
            regardless.

        '''
        subscriber = self.get_subscriber_by_identifier(subscriber_identifier)

        if subscriber is None:
            raise ftrack_api.exception.NotFoundError(
                'Cannot unsubscribe missing subscriber with identifier {0}'.
                format(subscriber_identifier))

        self._subscribers.pop(self._subscribers.index(subscriber))

        # Notify the server if possible.
        unsubscribe_event = ftrack_api.event.base.Event(
            topic='ftrack.meta.unsubscribe',
            data=dict(subscriber=subscriber.metadata))

        try:
            self._publish(unsubscribe_event,
                          callback=functools.partial(self._on_unsubscribed,
                                                     subscriber))
        except ftrack_api.exception.EventHubConnectionError:
            self.logger.debug(
                L(
                    'Failed to notify server to unsubscribe subscriber {0} as '
                    'server not currently reachable.',
                    subscriber.metadata['id']))
Exemple #5
0
    def _receive_packet(self):
        '''Receive and return packet via connection.'''
        try:
            packet = self._connection.recv()
        except Exception as error:
            raise ftrack_api.exception.EventHubConnectionError(
                'Error receiving packet: {0}'.format(error))

        try:
            parts = packet.split(':', 3)
        except AttributeError:
            raise ftrack_api.exception.EventHubPacketError(
                'Received invalid packet {0}'.format(packet))

        code, packet_identifier, path, data = None, None, None, None

        count = len(parts)
        if count == 4:
            code, packet_identifier, path, data = parts
        elif count == 3:
            code, packet_identifier, path = parts
        elif count == 1:
            code = parts[0]
        else:
            raise ftrack_api.exception.EventHubPacketError(
                'Received invalid packet {0}'.format(packet))

        self.logger.debug(L('Received packet: {0}', packet))
        return code, packet_identifier, path, data
Exemple #6
0
    def subscribe(self, subscription, callback, subscriber=None, priority=100):
        '''Register *callback* for *subscription*.

        A *subscription* is a string that can specify in detail which events the
        callback should receive. The filtering is applied against each event
        object. Nested references are supported using '.' separators.
        For example, 'topic=foo and data.eventType=Shot' would match the
        following event::

            <Event {'topic': 'foo', 'data': {'eventType': 'Shot'}}>

        The *callback* should accept an instance of
        :class:`ftrack_api.event.base.Event` as its sole argument.

        Callbacks are called in order of *priority*. The lower the priority
        number the sooner it will be called, with 0 being the first. The
        default priority is 100. Note that priority only applies against other
        callbacks registered with this hub and not as a global priority.

        An earlier callback can prevent processing of subsequent callbacks by
        calling :meth:`Event.stop` on the passed `event` before
        returning.

        .. warning::

            Handlers block processing of other received events. For long
            running callbacks it is advisable to delegate the main work to
            another process or thread.

        A *callback* can be attached to *subscriber* information that details
        the subscriber context. A subscriber context will be generated
        automatically if not supplied.

        .. note::

            The subscription will be stored locally, but until the server
            receives notification of the subscription it is possible the
            callback will not be called.

        Return subscriber identifier.

        Raise :exc:`ftrack_api.exception.NotUniqueError` if a subscriber with
        the same identifier already exists.

        '''
        # Add subscriber locally.
        subscriber = self._add_subscriber(subscription, callback, subscriber,
                                          priority)

        # Notify server now if possible.
        try:
            self._notify_server_about_subscriber(subscriber)
        except ftrack_api.exception.EventHubConnectionError:
            self.logger.debug(
                L(
                    'Failed to notify server about new subscriber {0} '
                    'as server not currently reachable.',
                    subscriber.metadata['id']))

        return subscriber.metadata['id']
Exemple #7
0
 def _on_published(self, event, response):
     '''Handle acknowledgement of published event.'''
     if response.get('success', False) is False:
         self.logger.error(
             L(
                 'Server responded with error while publishing event {0}. '
                 'Error was: {1}', event, response.get('message')))
Exemple #8
0
 def create_mapped_collection_attribute(self, class_name, name, mutable,
                                        reference):
     '''Return appropriate mapped collection attribute instance.'''
     self.logger.debug(
         L(
             'Skipping {0}.{1} mapped_array attribute that has '
             'no implementation defined for reference {2}.', class_name,
             name, reference))
Exemple #9
0
    def activate(self, event):
        '''Activate scenario in *event*.'''
        storage_scenario = event['data']['storage_scenario']

        try:
            location_data = storage_scenario['data']
            location_name = location_data['location_name']
            location_id = location_data['location_id']
            mount_points = location_data['accessor']['mount_points']

        except KeyError:
            error_message = ('Unable to read storage scenario data.')
            self.logger.error(L(error_message))
            raise ftrack_api.exception.LocationError(
                'Unable to configure location based on scenario.')

        else:
            location = self.session.create('Location',
                                           data=dict(name=location_name,
                                                     id=location_id),
                                           reconstructing=True)

            if sys.platform == 'darwin':
                prefix = mount_points['osx']
            elif sys.platform == 'linux2':
                prefix = mount_points['linux']
            elif sys.platform == 'win32':
                prefix = mount_points['windows']
            else:
                raise ftrack_api.exception.LocationError((
                    'Unable to find accessor prefix for platform {0}.').format(
                        sys.platform))

            location.accessor = ftrack_api.accessor.disk.DiskAccessor(
                prefix=prefix)
            location.structure = _standard.StandardStructure()
            location.priority = 1
            self.logger.info(
                L(
                    u'Storage scenario activated. Configured {0!r} from '
                    u'{1!r}', location, storage_scenario))
Exemple #10
0
    def _send_packet(self, code, data='', callback=None):
        '''Send packet via connection.'''
        path = ''
        packet_identifier = (self._add_packet_callback(callback)
                             if callback else '')
        packet_parts = (str(code), packet_identifier, path, data)
        packet = ':'.join(packet_parts)

        try:
            self._connection.send(packet)
            self.logger.debug(L(u'Sent packet: {0}', packet))
        except socket.error as error:
            raise ftrack_api.exception.EventHubConnectionError(
                'Failed to send packet: {0}'.format(error))
Exemple #11
0
    def _reconstruct(self, data):
        '''Reconstruct from *data*.'''
        # Data represents remote values.
        for key, value in list(data.items()):
            if key in self._ignore_data_keys:
                continue

            attribute = self.__class__.attributes.get(key)
            if attribute is None:
                self.logger.debug(
                    L(
                        'Cannot populate {0!r} attribute as no such attribute '
                        'found on entity {1!r}.', key, self))
                continue

            attribute.set_remote_value(self, value)
Exemple #12
0
    def reconnect(self, attempts=10, delay=5):
        '''Reconnect to server.

         Make *attempts* number of attempts with *delay* in seconds between each
         attempt.

        .. note::

            All current subscribers will be automatically resubscribed after
            successful reconnection.

        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if fail to
        reconnect.

        '''
        try:
            self.disconnect(unsubscribe=False)
        except ftrack_api.exception.EventHubConnectionError:
            pass

        for attempt in range(attempts):
            self.logger.debug(
                L('Reconnect attempt {0} of {1}', attempt, attempts))

            # Silence logging temporarily to avoid lots of failed connection
            # related information.
            try:
                logging.disable(logging.CRITICAL)

                try:
                    self.connect()
                except ftrack_api.exception.EventHubConnectionError:
                    time.sleep(delay)
                else:
                    break

            finally:
                logging.disable(logging.NOTSET)

        if not self.connected:
            raise ftrack_api.exception.EventHubConnectionError(
                'Failed to reconnect to event server at {0} after {1} attempts.'
                .format(self.get_server_url(), attempts))
Exemple #13
0
    def __delitem__(self, key):
        '''Remove and delete *key*.

        .. note::

            The associated entity will be deleted as well.

        '''
        custom_attribute_value = self._get_entity_by_key(key)

        if custom_attribute_value:
            index = self.collection.index(custom_attribute_value)
            del self.collection[index]

            custom_attribute_value.session.delete(custom_attribute_value)
        else:
            self.logger.warning(
                L(
                    'Cannot delete {0!r} on {1!r}, no custom attribute value set.',
                    key, self.collection.entity))
Exemple #14
0
    def _handle(self, event, synchronous=False):
        '''Handle *event*.

        If *synchronous* is True, do not send any automatic reply events.

        '''
        # Sort by priority, lower is higher.
        # TODO: Use a sorted list to avoid sorting each time in order to improve
        # performance.
        subscribers = sorted(self._subscribers,
                             key=operator.attrgetter('priority'))

        results = []

        target = event.get('target', None)
        target_expression = None
        if target:
            try:
                target_expression = self._expression_parser.parse(target)
            except Exception:
                self.logger.exception(
                    L(
                        'Cannot handle event as failed to parse event target '
                        'information: {0}', event))
                return

        for subscriber in subscribers:
            # Check if event is targeted to the subscriber.
            if (target_expression is not None
                    and not target_expression.match(subscriber.metadata)):
                continue

            # Check if subscriber interested in the event.
            if not subscriber.interested_in(event):
                continue

            response = None

            try:
                response = subscriber.callback(event)
                results.append(response)
            except Exception:
                self.logger.exception(
                    L('Error calling subscriber {0} for event {1}.',
                      subscriber, event))

            # Automatically publish a non None response as a reply when not in
            # synchronous mode.
            if not synchronous:
                if self._deprecation_warning_auto_connect:
                    warnings.warn(self._future_signature_warning,
                                  FutureWarning)

                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():
                self.logger.debug(
                    L(
                        'Subscriber {0} stopped event {1}. Will not process '
                        'subsequent subscriber callbacks for this event.',
                        subscriber, event))
                break

        return results
Exemple #15
0
    def _add_data(self, component, resource_identifier, source):
        '''Manage transfer of *component* data from *source*.

        *resource_identifier* specifies the identifier to use with this
        locations accessor.

        '''
        self.logger.debug(
            L(
                'Adding data for component {0!r} from source {1!r} to location '
                '{2!r} using resource identifier {3!r}.', component,
                resource_identifier, source, self))

        # Read data from source and write to this location.
        if not source.accessor:
            raise ftrack_api.exception.LocationError(
                'No accessor defined for source location {location}.',
                details=dict(location=source))

        if not self.accessor:
            raise ftrack_api.exception.LocationError(
                'No accessor defined for target location {location}.',
                details=dict(location=self))

        is_container = 'members' in list(component.keys())
        if is_container:
            # TODO: Improve this check. Possibly introduce an inspection
            # such as ftrack_api.inspection.is_sequence_component.
            if component.entity_type != 'SequenceComponent':
                self.accessor.make_container(resource_identifier)

        else:
            # Try to make container of component.
            try:
                container = self.accessor.get_container(resource_identifier)

            except ftrack_api.exception.AccessorParentResourceNotFoundError:
                # Container could not be retrieved from
                # resource_identifier. Assume that there is no need to
                # make the container.
                pass

            else:
                # No need for existence check as make_container does not
                # recreate existing containers.
                self.accessor.make_container(container)

            if self.accessor.exists(resource_identifier):
                # Note: There is a race condition here in that the
                # data may be added externally between the check for
                # existence and the actual write which would still
                # result in potential data loss. However, there is no
                # good cross platform, cross accessor solution for this
                # at present.
                raise ftrack_api.exception.LocationError(
                    'Cannot add component as data already exists and '
                    'overwriting could result in data loss. Computed '
                    'target resource identifier was: {0}'.format(
                        resource_identifier))

            # Read and write data.
            source_data = source.accessor.open(
                source.get_resource_identifier(component), 'rb')
            target_data = self.accessor.open(resource_identifier, 'wb')

            # Read/write data in chunks to avoid reading all into memory at the
            # same time.
            chunked_read = functools.partial(source_data.read,
                                             ftrack_api.symbol.CHUNK_SIZE)
            for chunk in iter(chunked_read, b''):
                target_data.write(chunk)

            target_data.close()
            source_data.close()
Exemple #16
0
    def create(self, schema, bases=None):
        '''Create and return entity class from *schema*.

        *bases* should be a list of bases to give the constructed class. If not
        specified, default to :class:`ftrack_api.entity.base.Entity`.

        '''
        entity_type = schema['id']
        class_name = entity_type

        class_bases = bases
        if class_bases is None:
            class_bases = [ftrack_api.entity.base.Entity]

        class_namespace = dict()

        # Build attributes for class.
        attributes = ftrack_api.attribute.Attributes()
        immutable_properties = schema.get('immutable', [])
        computed_properties = schema.get('computed', [])
        for name, fragment in list(schema.get('properties', {}).items()):
            mutable = name not in immutable_properties
            computed = name in computed_properties

            default = fragment.get('default', ftrack_api.symbol.NOT_SET)
            if default == '{uid}':
                default = lambda instance: str(uuid.uuid4())

            data_type = fragment.get('type', ftrack_api.symbol.NOT_SET)

            if data_type is not ftrack_api.symbol.NOT_SET:

                if data_type in ('string', 'boolean', 'integer', 'number',
                                 'variable', 'object'):
                    # Basic scalar attribute.
                    if data_type == 'number':
                        data_type = 'float'

                    if data_type == 'string':
                        data_format = fragment.get('format')
                        if data_format == 'date-time':
                            data_type = 'datetime'

                    attribute = self.create_scalar_attribute(
                        class_name, name, mutable, computed, default,
                        data_type)
                    if attribute:
                        attributes.add(attribute)

                elif data_type == 'array':
                    attribute = self.create_collection_attribute(
                        class_name, name, mutable)
                    if attribute:
                        attributes.add(attribute)

                elif data_type == 'mapped_array':
                    reference = fragment.get('items', {}).get('$ref')
                    if not reference:
                        self.logger.debug(
                            L(
                                'Skipping {0}.{1} mapped_array attribute that does '
                                'not define a schema reference.', class_name,
                                name))
                        continue

                    attribute = self.create_mapped_collection_attribute(
                        class_name, name, mutable, reference)
                    if attribute:
                        attributes.add(attribute)

                else:
                    self.logger.debug(
                        L(
                            'Skipping {0}.{1} attribute with unrecognised data '
                            'type {2}', class_name, name, data_type))
            else:
                # Reference attribute.
                reference = fragment.get('$ref', ftrack_api.symbol.NOT_SET)
                if reference is ftrack_api.symbol.NOT_SET:
                    self.logger.debug(
                        L(
                            'Skipping {0}.{1} mapped_array attribute that does '
                            'not define a schema reference.', class_name,
                            name))
                    continue

                attribute = self.create_reference_attribute(
                    class_name, name, mutable, reference)
                if attribute:
                    attributes.add(attribute)

        default_projections = schema.get('default_projections', [])

        # Construct class.
        class_namespace['entity_type'] = entity_type
        class_namespace['attributes'] = attributes
        class_namespace['primary_key_attributes'] = schema['primary_key'][:]
        class_namespace['default_projections'] = default_projections

        from future.utils import (native_str)

        cls = type(
            native_str(class_name),  # type doesn't accept unicode.
            tuple(class_bases),
            class_namespace)

        return cls
Exemple #17
0
    def create_mapped_collection_attribute(self, class_name, name, mutable,
                                           reference):
        '''Return appropriate mapped collection attribute instance.'''
        if reference == 'Metadata':

            def create_metadata(proxy, data, reference):
                '''Return metadata for *data*.'''
                entity = proxy.collection.entity
                session = entity.session
                data.update({
                    'parent_id': entity['id'],
                    'parent_type': entity.entity_type
                })
                return session.create(reference, data)

            creator = functools.partial(create_metadata, reference=reference)
            key_attribute = 'key'
            value_attribute = 'value'

            return ftrack_api.attribute.KeyValueMappedCollectionAttribute(
                name, creator, key_attribute, value_attribute, mutable=mutable)

        elif reference == 'CustomAttributeValue':
            return (ftrack_api.attribute.CustomAttributeCollectionAttribute(
                name, mutable=mutable))

        elif reference.endswith('CustomAttributeValue'):

            def creator(proxy, data):
                '''Create a custom attribute based on *proxy* and *data*.

                Raise :py:exc:`KeyError` if related entity is already presisted
                to the server. The proxy represents dense custom attribute
                values and should never create new custom attribute values
                through the proxy if entity exists on the remote.

                If the entity is not persisted the ususal
                <entity_type>CustomAttributeValue items cannot be updated as
                the related entity does not exist on remote and values not in
                the proxy. Instead a <entity_type>CustomAttributeValue will
                be reconstructed and an update operation will be recorded.

                '''
                entity = proxy.collection.entity
                if (ftrack_api.inspection.state(entity)
                        is not ftrack_api.symbol.CREATED):
                    raise KeyError(
                        'Custom attributes must be created explicitly for the '
                        'given entity type before being set.')

                configuration = None
                for candidate in _get_entity_configurations(entity):
                    if candidate['key'] == data['key']:
                        configuration = candidate
                        break

                if configuration is None:
                    raise ValueError(
                        u'No valid custom attribute for data {0!r} was found.'.
                        format(data))

                create_data = dict(list(data.items()))
                create_data['configuration_id'] = configuration['id']
                create_data['entity_id'] = entity['id']

                session = entity.session

                # Create custom attribute by reconstructing it and update the
                # value. This will prevent a create operation to be sent to the
                # remote, as create operations for this entity type is not
                # allowed. Instead an update operation will be recorded.
                value = create_data.pop('value')
                item = session.create(reference,
                                      create_data,
                                      reconstructing=True)

                # Record update operation.
                item['value'] = value

                return item

            key_attribute = 'key'
            value_attribute = 'value'

            return ftrack_api.attribute.KeyValueMappedCollectionAttribute(
                name, creator, key_attribute, value_attribute, mutable=mutable)

        self.logger.debug(
            L(
                'Skipping {0}.{1} mapped_array attribute that has no configuration '
                'for reference {2}.', class_name, name, reference))
Exemple #18
0
    def connect(self):
        '''Initialise connection to server.

        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if already
        connected or connection fails.

        '''

        self._deprecation_warning_auto_connect = False

        if self.connected:
            raise ftrack_api.exception.EventHubConnectionError(
                'Already connected.')

        # Reset flag tracking whether disconnection was intentional.
        self._intentional_disconnect = False

        try:
            # Connect to socket.io server using websocket transport.
            session = self._get_socket_io_session()

            if 'websocket' not in session.supportedTransports:
                raise ValueError('Server does not support websocket sessions.')

            scheme = 'wss' if self.secure else 'ws'
            url = '{0}://{1}/socket.io/1/websocket/{2}'.format(
                scheme, self.get_network_location(), session.id)

            # timeout is set to 60 seconds to avoid the issue where the socket
            # ends up in a bad state where it is reported as connected but the
            # connection has been closed. The issue happens often when connected
            # to a secure socket and the computer goes to sleep.
            # More information on how the timeout works can be found here:
            # https://docs.python.org/2/library/socket.html#socket.socket.setblocking
            self._connection = websocket.create_connection(url, timeout=60)

        except Exception:
            self.logger.debug(L('Error connecting to event server at {0}.',
                                self.get_server_url()),
                              exc_info=1)
            raise ftrack_api.exception.EventHubConnectionError(
                'Failed to connect to event server at {0}.'.format(
                    self.get_server_url()))

        # Start background processing thread.
        self._processor_thread = _ProcessorThread(self)
        self._processor_thread.start()

        # Subscribe to reply events if not already. Note: Only adding the
        # subscriber locally as the following block will notify server of all
        # existing subscribers, which would cause the server to report a
        # duplicate subscriber error if EventHub.subscribe was called here.
        try:
            self._add_subscriber('topic=ftrack.meta.reply',
                                 self._handle_reply,
                                 subscriber=dict(id=self.id))
        except ftrack_api.exception.NotUniqueError:
            pass

        # Now resubscribe any existing stored subscribers. This can happen when
        # reconnecting automatically for example.
        for subscriber in self._subscribers[:]:
            self._notify_server_about_subscriber(subscriber)
Exemple #19
0
    def configure_scenario(self, event):
        '''Configure scenario based on *event* and return form items.'''
        steps = ('select_scenario', 'select_location', 'configure_location',
                 'select_structure', 'select_mount_point', 'confirm_summary',
                 'save_configuration')

        warning_message = ''
        values = event['data'].get('values', {})

        # Calculate previous step and the next.
        previous_step = values.get('step', 'select_scenario')
        next_step = steps[steps.index(previous_step) + 1]
        state = 'configuring'

        self.logger.info(
            L(
                u'Configuring scenario, previous step: {0}, next step: {1}. '
                u'Values {2!r}.', previous_step, next_step, values))

        if 'configuration' in values:
            configuration = values.pop('configuration')
        else:
            configuration = {}

        if values:
            # Update configuration with values from the previous step.
            configuration[previous_step] = values

        if previous_step == 'select_location':
            values = configuration['select_location']
            if values.get('location_id') != 'create_new_location':
                location_exists = self.session.query(
                    'Location where id is "{0}"'.format(
                        values.get('location_id'))).first()
                if not location_exists:
                    next_step = 'select_location'
                    warning_message = (
                        '**The selected location does not exist. Please choose '
                        'one from the dropdown or create a new one.**')

        if next_step == 'select_location':
            try:
                location_id = (
                    self.
                    existing_centralized_storage_configuration['location_id'])
            except (KeyError, TypeError):
                location_id = None

            options = [{
                'label': 'Create new location',
                'value': 'create_new_location'
            }]
            for location in self.session.query(
                    'select name, label, description from Location'):
                if location['name'] not in ('ftrack.origin',
                                            'ftrack.unmanaged',
                                            'ftrack.connect', 'ftrack.server',
                                            'ftrack.review'):
                    options.append({
                        'label':
                        u'{label} ({name})'.format(label=location['label'],
                                                   name=location['name']),
                        'description':
                        location['description'],
                        'value':
                        location['id']
                    })

            warning = ''
            if location_id is not None:
                # If there is already a location configured we must make the
                # user aware that changing the location may be problematic.
                warning = (
                    '\n\n**Be careful if you switch to another location '
                    'for an existing storage scenario. Components that have '
                    'already been published to the previous location will be '
                    'made unavailable for common use.**')
                default_value = location_id
            elif location_id is None and len(options) == 1:
                # No location configured and no existing locations to use.
                default_value = 'create_new_location'
            else:
                # There are existing locations to choose from but non of them
                # are currently active in the centralized storage scenario.
                default_value = None

            items = [{
                'type':
                'label',
                'value':
                ('#Select location#\n'
                 'Choose an already existing location or create a new one '
                 'to represent your centralized storage. {0}'.format(warning))
            }, {
                'type': 'enumerator',
                'label': 'Location',
                'name': 'location_id',
                'value': default_value,
                'data': options
            }]

        default_location_name = 'studio.central-storage-location'
        default_location_label = 'Studio location'
        default_location_description = (
            'The studio central location where all components are '
            'stored.')

        if previous_step == 'configure_location':
            configure_location = configuration.get('configure_location')

            if configure_location:
                try:
                    existing_location = self.session.query(
                        u'Location where name is "{0}"'.format(
                            configure_location.get('location_name'))).first()
                except UnicodeEncodeError:
                    next_step = 'configure_location'
                    warning_message += (
                        '**The location name contains non-ascii characters. '
                        'Please change the name and try again.**')
                    values = configuration['select_location']
                else:
                    if existing_location:
                        next_step = 'configure_location'
                        warning_message += (
                            u'**There is already a location named {0}. '
                            u'Please change the name and try again.**'.format(
                                configure_location.get('location_name')))
                        values = configuration['select_location']

                if (not configure_location.get('location_name')
                        or not configure_location.get('location_label')
                        or not configure_location.get('location_description')):
                    next_step = 'configure_location'
                    warning_message += (
                        '**Location name, label and description cannot '
                        'be empty.**')
                    values = configuration['select_location']

            if next_step == 'configure_location':
                # Populate form with previous configuration.
                default_location_label = configure_location['location_label']
                default_location_name = configure_location['location_name']
                default_location_description = (
                    configure_location['location_description'])

        if next_step == 'configure_location':

            if values.get('location_id') == 'create_new_location':
                # Add options to create a new location.
                items = [{
                    'type':
                    'label',
                    'value':
                    ('#Create location#\n'
                     'Here you will create a new location to be used '
                     'with your new Storage scenario. For your '
                     'convenience we have already filled in some default '
                     'values. If this is the first time you are configuring '
                     'a storage scenario in ftrack we recommend that you '
                     'stick with these settings.')
                }, {
                    'label': 'Label',
                    'name': 'location_label',
                    'value': default_location_label,
                    'type': 'text'
                }, {
                    'label': 'Name',
                    'name': 'location_name',
                    'value': default_location_name,
                    'type': 'text'
                }, {
                    'label': 'Description',
                    'name': 'location_description',
                    'value': default_location_description,
                    'type': 'text'
                }]

            else:
                # The user selected an existing location. Move on to next
                # step.
                next_step = 'select_mount_point'

        if next_step == 'select_structure':
            # There is only one structure to choose from, go to next step.
            next_step = 'select_mount_point'
            # items = [
            #     {
            #         'type': 'label',
            #         'value': (
            #             '#Select structure#\n'
            #             'Select which structure to use with your location. '
            #             'The structure is used to generate the filesystem '
            #             'path for components that are added to this location.'
            #         )
            #     },
            #     {
            #         'type': 'enumerator',
            #         'label': 'Structure',
            #         'name': 'structure_id',
            #         'value': 'standard',
            #         'data': [{
            #             'label': 'Standard',
            #             'value': 'standard',
            #             'description': (
            #                 'The Standard structure uses the names in your '
            #                 'project structure to determine the path.'
            #             )
            #         }]
            #     }
            # ]

        if next_step == 'select_mount_point':
            try:
                mount_points = (
                    self.existing_centralized_storage_configuration['accessor']
                    ['mount_points'])
            except (KeyError, TypeError):
                mount_points = dict()

            items = [{
                'value':
                ('#Mount points#\n'
                 'Set mount points for your centralized storage '
                 'location. For the location to work as expected each '
                 'platform that you intend to use must have the '
                 'corresponding mount point set and the storage must '
                 'be accessible. If not set correctly files will not be '
                 'saved or read.'),
                'type':
                'label'
            }, {
                'type': 'text',
                'label': 'Linux',
                'name': 'linux_mount_point',
                'empty_text': 'E.g. /usr/mnt/MyStorage ...',
                'value': mount_points.get('linux', '')
            }, {
                'type': 'text',
                'label': 'OS X',
                'name': 'osx_mount_point',
                'empty_text': 'E.g. /Volumes/MyStorage ...',
                'value': mount_points.get('osx', '')
            }, {
                'type': 'text',
                'label': 'Windows',
                'name': 'windows_mount_point',
                'empty_text': 'E.g. \\\\MyStorage ...',
                'value': mount_points.get('windows', '')
            }]

        if next_step == 'confirm_summary':
            items = [{
                'type': 'label',
                'value': self._get_confirmation_text(configuration)
            }]
            state = 'confirm'

        if next_step == 'save_configuration':
            mount_points = configuration['select_mount_point']
            select_location = configuration['select_location']

            if select_location['location_id'] == 'create_new_location':
                configure_location = configuration['configure_location']
                location = self.session.create(
                    'Location', {
                        'name': configure_location['location_name'],
                        'label': configure_location['location_label'],
                        'description':
                        (configure_location['location_description'])
                    })

            else:
                location = self.session.query(
                    'Location where id is "{0}"'.format(
                        select_location['location_id'])).one()

            setting_value = json.dumps({
                'scenario': scenario_name,
                'data': {
                    'location_id': location['id'],
                    'location_name': location['name'],
                    'accessor': {
                        'mount_points': {
                            'linux': mount_points['linux_mount_point'],
                            'osx': mount_points['osx_mount_point'],
                            'windows': mount_points['windows_mount_point']
                        }
                    }
                }
            })

            self.storage_scenario['value'] = setting_value
            self.session.commit()

            # Broadcast an event that storage scenario has been configured.
            event = ftrack_api.event.base.Event(
                topic='ftrack.storage-scenario.configure-done')
            self.session.event_hub.publish(event)

            items = [{
                'type':
                'label',
                'value':
                ('#Done!#\n'
                 'Your storage scenario is now configured and ready '
                 'to use. **Note that you may have to restart Connect and '
                 'other applications to start using it.**')
            }]
            state = 'done'

        if warning_message:
            items.insert(0, {'type': 'label', 'value': warning_message})

        items.append({
            'type': 'hidden',
            'value': configuration,
            'name': 'configuration'
        })
        items.append({'type': 'hidden', 'value': next_step, 'name': 'step'})

        return {'items': items, 'state': state}
Exemple #20
0
    def _handle_packet(self, code, packet_identifier, path, data):
        '''Handle packet received from server.'''
        code_name = self._code_name_mapping[code]

        if code_name == 'connect':
            self.logger.debug('Connected to event server.')
            event = ftrack_api.event.base.Event('ftrack.meta.connected')
            self._event_queue.put(event)

        elif code_name == 'disconnect':
            self.logger.debug('Disconnected from event server.')
            if not self._intentional_disconnect:
                self.logger.debug(
                    'Disconnected unexpectedly. Attempting to reconnect.')
                try:
                    self.reconnect(attempts=self._auto_reconnect_attempts,
                                   delay=self._auto_reconnect_delay)
                except ftrack_api.exception.EventHubConnectionError:
                    self.logger.debug('Failed to reconnect automatically.')
                else:
                    self.logger.debug('Reconnected successfully.')

            if not self.connected:
                event = ftrack_api.event.base.Event('ftrack.meta.disconnected')
                self._event_queue.put(event)

        elif code_name == 'heartbeat':
            # Reply with heartbeat.
            self._send_packet(self._code_name_mapping['heartbeat'])

        elif code_name == 'message':
            self.logger.debug(L('Message received: {0}', data))

        elif code_name == 'event':
            payload = self._decode(data)
            args = payload.get('args', [])

            if len(args) == 1:
                event_payload = args[0]
                if isinstance(event_payload, collections.Mapping):
                    try:
                        event = ftrack_api.event.base.Event(**event_payload)
                    except Exception:
                        self.logger.exception(
                            L('Failed to convert payload into event: {0}',
                              event_payload))
                        return

                    self._event_queue.put(event)

        elif code_name == 'acknowledge':
            parts = data.split('+', 1)
            acknowledged_packet_identifier = int(parts[0])
            args = []
            if len(parts) == 2:
                args = self._decode(parts[1])

            try:
                callback = self._pop_packet_callback(
                    acknowledged_packet_identifier)
            except KeyError:
                pass
            else:
                callback(*args)

        elif code_name == 'error':
            self.logger.error(L('Event server reported error: {0}.', data))

        else:
            self.logger.debug(L('{0}: {1}', code_name, data))
Exemple #21
0
 def _on_subscribed(self, subscriber, response):
     '''Handle acknowledgement of subscription.'''
     if response.get('success') is False:
         self.logger.warning(
             L('Server failed to subscribe subscriber {0}: {1}',
               subscriber.metadata['id'], response.get('message')))
Exemple #22
0
 def _on_unsubscribed(self, subscriber, response):
     '''Handle acknowledgement of unsubscribing *subscriber*.'''
     if response.get('success') is not True:
         self.logger.warning(
             L('Server failed to unsubscribe subscriber {0}: {1}',
               subscriber.metadata['id'], response.get('message')))
Exemple #23
0
    def _publish(self, event, synchronous=False, callback=None, on_reply=None):
        '''Publish *event*.

        If *synchronous* is specified as True then this method will wait and
        return a list of results from any called callbacks.

        .. note::

            Currently, if synchronous is True then only locally registered
            callbacks will be called and no event will be sent to the server.
            This may change in future.

        A *callback* can also be specified. This callback will be called once
        the server acknowledges receipt of the sent event. A default callback
        that checks for errors from the server will be used if not specified.

        *on_reply* is an optional callable to call with any reply event that is
        received in response to the published *event*. Note that there is no
        guarantee that a reply will be sent.

        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if not
        currently connected.

        '''
        # Prepare event adding any relevant additional information.
        self._prepare_event(event)

        if synchronous:
            # Bypass emitting event to server and instead call locally
            # registered handlers directly, collecting and returning results.
            return self._handle(event, synchronous=synchronous)

        if not self.connected:
            raise ftrack_api.exception.EventHubConnectionError(
                'Cannot publish event asynchronously as not connected to '
                'server.')

        # Use standard callback if none specified.
        if callback is None:
            callback = functools.partial(self._on_published, event)

        # Emit event to central server for asynchronous processing.
        try:
            # Register on reply callback if specified.
            if on_reply is not None:
                # TODO: Add cleanup process that runs after a set duration to
                # garbage collect old reply callbacks and prevent dictionary
                # growing too large.
                self._reply_callbacks[event['id']] = on_reply

            try:
                self._emit_event_packet(self._event_namespace,
                                        event,
                                        callback=callback)
            except ftrack_api.exception.EventHubConnectionError:
                # Connection may have dropped temporarily. Wait a few moments to
                # see if background thread reconnects automatically.
                time.sleep(15)

                self._emit_event_packet(self._event_namespace,
                                        event,
                                        callback=callback)
            except:
                raise

        except Exception:
            # Failure to send event should not cause caller to fail.
            # TODO: This behaviour is inconsistent with the failing earlier on
            # lack of connection and also with the error handling parameter of
            # EventHub.publish. Consider refactoring.
            self.logger.exception(L('Error sending event {0}.', event))
Exemple #24
0
    def _construct(self, data):
        '''Construct from *data*.'''
        # Suspend operation recording so that all modifications can be applied
        # in single create operation. In addition, recording a modification
        # operation requires a primary key which may not be available yet.

        relational_attributes = dict()

        with self.session.operation_recording(False):
            # Set defaults for any unset local attributes.
            for attribute in self.__class__.attributes:
                if attribute.name not in data:
                    default_value = attribute.default_value
                    if callable(default_value):
                        default_value = default_value(self)

                    attribute.set_local_value(self, default_value)

            # Data represents locally set values.
            for key, value in list(data.items()):
                if key in self._ignore_data_keys:
                    continue

                attribute = self.__class__.attributes.get(key)
                if attribute is None:
                    self.logger.debug(
                        L(
                            'Cannot populate {0!r} attribute as no such '
                            'attribute found on entity {1!r}.', key, self))
                    continue

                if not isinstance(attribute,
                                  ftrack_api.attribute.ScalarAttribute):
                    relational_attributes.setdefault(attribute, value)

                else:
                    attribute.set_local_value(self, value)

        # Record create operation.
        # Note: As this operation is recorded *before* any Session.merge takes
        # place there is the possibility that the operation will hold references
        # to outdated data in entity_data. However, this would be unusual in
        # that it would mean the same new entity was created twice and only one
        # altered. Conversely, if this operation were recorded *after*
        # Session.merge took place, any cache would not be able to determine
        # the status of the entity, which could be important if the cache should
        # not store newly created entities that have not yet been persisted. Out
        # of these two 'evils' this approach is deemed the lesser at this time.
        # A third, more involved, approach to satisfy both might be to record
        # the operation with a PENDING entity_data value and then update with
        # merged values post merge.
        if self.session.record_operations:
            entity_data = {}

            # Lower level API used here to avoid including any empty
            # collections that are automatically generated on access.
            for attribute in self.attributes:
                value = attribute.get_local_value(self)
                if value is not ftrack_api.symbol.NOT_SET:
                    entity_data[attribute.name] = value

            self.session.recorded_operations.push(
                ftrack_api.operation.CreateEntityOperation(
                    self.entity_type, ftrack_api.inspection.primary_key(self),
                    entity_data))

        for attribute, value in list(relational_attributes.items()):
            # Finally we set values for "relational" attributes, we need
            # to do this at the end in order to get the create operations
            # in the correct order as the newly created attributes might
            # contain references to the newly created entity.

            attribute.set_local_value(self, value)
Exemple #25
0
    def connect(self):
        '''Initialise connection to server.

        Raise :exc:`ftrack_api.exception.EventHubConnectionError` if already
        connected or connection fails.

        '''
        # Update tracking flag for connection.
        self._connection_initialised = True

        if self.connected:
            raise ftrack_api.exception.EventHubConnectionError(
                'Already connected.')

        # Reset flag tracking whether disconnection was intentional.
        self._intentional_disconnect = False

        try:
            # Connect to socket.io server using websocket transport.
            session = self._get_socket_io_session()

            if 'websocket' not in session.supportedTransports:
                raise ValueError('Server does not support websocket sessions.')

            scheme = 'wss' if self.secure else 'ws'
            url = '{0}://{1}/socket.io/1/websocket/{2}'.format(
                scheme, self.get_network_location(), session.id)

            # Select highest available protocol for websocket connection.
            ssl_protocols = ['PROTOCOL_TLS', 'PROTOCOL_TLSv1_2']

            available_ssl_protocol = None

            for ssl_protocol in ssl_protocols:
                if hasattr(ssl, ssl_protocol):
                    available_ssl_protocol = getattr(ssl, ssl_protocol)
                    self.logger.debug(
                        'Using protocol {} to connect to websocket.'.format(
                            ssl_protocol))
                    break

            # timeout is set to 60 seconds to avoid the issue where the socket
            # ends up in a bad state where it is reported as connected but the
            # connection has been closed. The issue happens often when connected
            # to a secure socket and the computer goes to sleep.
            # More information on how the timeout works can be found here:
            # https://docs.python.org/2/library/socket.html#socket.socket.setblocking
            self._connection = websocket.create_connection(
                url,
                timeout=60,
                sslopt={"ssl_version": available_ssl_protocol})

        except Exception as error:
            error_message = (
                'Failed to connect to event server at {server_url} with '
                'error: "{error}".')

            error_details = {
                'error': str(error),
                'server_url': self.get_server_url()
            }

            self.logger.debug(L(error_message, **error_details), exc_info=1)
            raise ftrack_api.exception.EventHubConnectionError(
                error_message, details=error_details)

        # Start background processing thread.
        self._processor_thread = _ProcessorThread(self)
        self._processor_thread.start()

        # Subscribe to reply events if not already. Note: Only adding the
        # subscriber locally as the following block will notify server of all
        # existing subscribers, which would cause the server to report a
        # duplicate subscriber error if EventHub.subscribe was called here.
        try:
            self._add_subscriber('topic=ftrack.meta.reply',
                                 self._handle_reply,
                                 subscriber=dict(id=self.id))
        except ftrack_api.exception.NotUniqueError:
            pass

        # Now resubscribe any existing stored subscribers. This can happen when
        # reconnecting automatically for example.
        for subscriber in self._subscribers[:]:
            self._notify_server_about_subscriber(subscriber)