ftrack-api integration initialization

This commit is contained in:
Jakub Jezek 2018-10-10 18:34:04 +02:00
parent ff8fe86771
commit 0bdc5d6e71
7 changed files with 1 additions and 796 deletions

View file

@ -2,7 +2,7 @@ import os
from avalon import api as avalon
from pyblish import api as pyblish
# from avalon.tools import workfiles
from ..vendor import ftrack_api
PARENT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.dirname(PARENT_DIR)

View file

@ -1,293 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
# :license: See LICENSE.txt.
import re
from collections import defaultdict
from ._version import __version__
from .collection import Collection
from .error import CollectionError
#: Pattern for matching an index with optional padding.
DIGITS_PATTERN = '(?P<index>(?P<padding>0*)\d+)'
#: Common patterns that can be passed to :py:func:`~clique.assemble`.
PATTERNS = {
'frames': '\.{0}\.\D+\d?$'.format(DIGITS_PATTERN),
'versions': 'v{0}'.format(DIGITS_PATTERN)
}
def assemble(
iterable, patterns=None, minimum_items=2, case_sensitive=True,
assume_padded_when_ambiguous=False
):
'''Assemble items in *iterable* into discreet collections.
*patterns* may be specified as a list of regular expressions to limit
the returned collection possibilities. Use this when interested in
collections that only match specific patterns. Each pattern must contain
the expression from :py:data:`DIGITS_PATTERN` exactly once.
A selection of common expressions are available in :py:data:`PATTERNS`.
.. note::
If a pattern is supplied as a string it will be automatically compiled
to a :py:class:`re.RegexObject` instance for convenience.
When *patterns* is not specified, collections are formed by examining all
possible groupings of the items in *iterable* based around common numerical
components.
*minimum_items* dictates the minimum number of items a collection must have
in order to be included in the result. The default is 2, filtering out
single item collections.
If *case_sensitive* is False, then items will be treated as part of the same
collection when they only differ in casing. To avoid ambiguity, the
resulting collection will always be lowercase. For example, "item.0001.dpx"
and "Item.0002.dpx" would be part of the same collection, "item.%04d.dpx".
.. note::
Any compiled *patterns* will also respect the set case sensitivity.
For certain collections it may be ambiguous whether they are padded or not.
For example, 1000-1010 can be considered either an unpadded collection or a
four padded collection. By default, Clique is conservative and assumes that
the collection is unpadded. To change this behaviour, set
*assume_padded_when_ambiguous* to True and any ambiguous collection will have
a relevant padding set.
.. note::
*assume_padded_when_ambiguous* has no effect on collections that are
unambiguous. For example, 1-100 will always be considered unpadded
regardless of the *assume_padded_when_ambiguous* setting.
Return tuple of two lists (collections, remainder) where 'collections' is a
list of assembled :py:class:`~clique.collection.Collection` instances and
'remainder' is a list of items that did not belong to any collection.
'''
collection_map = defaultdict(set)
collections = []
remainder = []
# Compile patterns.
flags = 0
if not case_sensitive:
flags |= re.IGNORECASE
compiled_patterns = []
if patterns is not None:
if not patterns:
return collections, list(iterable)
for pattern in patterns:
if isinstance(pattern, basestring):
compiled_patterns.append(re.compile(pattern, flags=flags))
else:
compiled_patterns.append(pattern)
else:
compiled_patterns.append(re.compile(DIGITS_PATTERN, flags=flags))
# Process iterable.
for item in iterable:
matched = False
for pattern in compiled_patterns:
for match in pattern.finditer(item):
index = match.group('index')
head = item[:match.start('index')]
tail = item[match.end('index'):]
if not case_sensitive:
head = head.lower()
tail = tail.lower()
padding = match.group('padding')
if padding:
padding = len(index)
else:
padding = 0
key = (head, tail, padding)
collection_map[key].add(int(index))
matched = True
if not matched:
remainder.append(item)
# Form collections.
merge_candidates = []
for (head, tail, padding), indexes in collection_map.items():
collection = Collection(head, tail, padding, indexes)
collections.append(collection)
if collection.padding == 0:
merge_candidates.append(collection)
# Merge together collections that align on padding boundaries. For example,
# 0998-0999 and 1000-1001 can be merged into 0998-1001. Note that only
# indexes within the padding width limit are merged. If a collection is
# entirely merged into another then it will not be included as a separate
# collection in the results.
fully_merged = []
for collection in collections:
if collection.padding == 0:
continue
for candidate in merge_candidates:
if (
candidate.head == collection.head and
candidate.tail == collection.tail
):
merged_index_count = 0
for index in candidate.indexes:
if len(str(abs(index))) == collection.padding:
collection.indexes.add(index)
merged_index_count += 1
if merged_index_count == len(candidate.indexes):
fully_merged.append(candidate)
# Filter out fully merged collections.
collections = [collection for collection in collections
if collection not in fully_merged]
# Filter out collections that do not have at least as many indexes as
# minimum_items. In addition, add any members of a filtered collection,
# which are not members of an unfiltered collection, to the remainder.
filtered = []
remainder_candidates = []
for collection in collections:
if len(collection.indexes) >= minimum_items:
filtered.append(collection)
else:
for member in collection:
remainder_candidates.append(member)
for candidate in remainder_candidates:
# Check if candidate has already been added to remainder to avoid
# duplicate entries.
if candidate in remainder:
continue
has_membership = False
for collection in filtered:
if candidate in collection:
has_membership = True
break
if not has_membership:
remainder.append(candidate)
# Set padding for all ambiguous collections according to the
# assume_padded_when_ambiguous setting.
if assume_padded_when_ambiguous:
for collection in filtered:
if (
not collection.padding and collection.indexes
):
indexes = list(collection.indexes)
first_index_width = len(str(indexes[0]))
last_index_width = len(str(indexes[-1]))
if first_index_width == last_index_width:
collection.padding = first_index_width
return filtered, remainder
def parse(value, pattern='{head}{padding}{tail} [{ranges}]'):
'''Parse *value* into a :py:class:`~clique.collection.Collection`.
Use *pattern* to extract information from *value*. It may make use of the
following keys:
* *head* - Common leading part of the collection.
* *tail* - Common trailing part of the collection.
* *padding* - Padding value in ``%0d`` format.
* *range* - Total range in the form ``start-end``.
* *ranges* - Comma separated ranges of indexes.
* *holes* - Comma separated ranges of missing indexes.
.. note::
*holes* only makes sense if *range* or *ranges* is also present.
'''
# Construct regular expression for given pattern.
expressions = {
'head': '(?P<head>.*)',
'tail': '(?P<tail>.*)',
'padding': '%(?P<padding>\d*)d',
'range': '(?P<range>\d+-\d+)?',
'ranges': '(?P<ranges>[\d ,\-]+)?',
'holes': '(?P<holes>[\d ,\-]+)'
}
pattern_regex = re.escape(pattern)
for key, expression in expressions.items():
pattern_regex = pattern_regex.replace(
'\{{{0}\}}'.format(key),
expression
)
pattern_regex = '^{0}$'.format(pattern_regex)
# Match pattern against value and use results to construct collection.
match = re.search(pattern_regex, value)
if match is None:
raise ValueError('Value did not match pattern.')
groups = match.groupdict()
if 'padding' in groups and groups['padding']:
groups['padding'] = int(groups['padding'])
else:
groups['padding'] = 0
# Create collection and then add indexes.
collection = Collection(
groups.get('head', ''),
groups.get('tail', ''),
groups['padding']
)
if groups.get('range', None) is not None:
start, end = map(int, groups['range'].split('-'))
collection.indexes.update(range(start, end + 1))
if groups.get('ranges', None) is not None:
parts = [part.strip() for part in groups['ranges'].split(',')]
for part in parts:
index_range = list(map(int, part.split('-', 2)))
if len(index_range) > 1:
# Index range.
for index in range(index_range[0], index_range[1] + 1):
collection.indexes.add(index)
else:
# Single index.
collection.indexes.add(index_range[0])
if 'holes' in groups:
parts = [part.strip() for part in groups['holes'].split(',')]
for part in parts:
index_range = map(int, part.split('-', 2))
if len(index_range) > 1:
# Index range.
for index in range(index_range[0], index_range[1] + 1):
collection.indexes.remove(index)
else:
# Single index.
collection.indexes.remove(index_range[0])
return collection

View file

@ -1,2 +0,0 @@
__version__ = '1.5.0'

View file

@ -1,385 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
# :license: See LICENSE.txt.
import re
import descriptor
import error
import sorted_set
class Collection(object):
'''Represent group of items that differ only by numerical component.'''
indexes = descriptor.Unsettable('indexes')
def __init__(self, head, tail, padding, indexes=None):
'''Initialise collection.
*head* is the leading common part whilst *tail* is the trailing
common part.
*padding* specifies the "width" of the numerical component. An index
will be padded with zeros to fill this width. A *padding* of zero
implies no padding and width may be any size so long as no leading
zeros are present.
*indexes* can specify a set of numerical indexes to initially populate
the collection with.
.. note::
After instantiation, the ``indexes`` attribute cannot be set to a
new value using assignment::
>>> collection.indexes = [1, 2, 3]
AttributeError: Cannot set attribute defined as unsettable.
Instead, manipulate it directly::
>>> collection.indexes.clear()
>>> collection.indexes.update([1, 2, 3])
'''
super(Collection, self).__init__()
self.__dict__['indexes'] = sorted_set.SortedSet()
self._head = head
self._tail = tail
self.padding = padding
self._update_expression()
if indexes is not None:
self.indexes.update(indexes)
@property
def head(self):
'''Return common leading part.'''
return self._head
@head.setter
def head(self, value):
'''Set common leading part to *value*.'''
self._head = value
self._update_expression()
@property
def tail(self):
'''Return common trailing part.'''
return self._tail
@tail.setter
def tail(self, value):
'''Set common trailing part to *value*.'''
self._tail = value
self._update_expression()
def _update_expression(self):
'''Update internal expression.'''
self._expression = re.compile(
'^{0}(?P<index>(?P<padding>0*)\d+?){1}$'
.format(re.escape(self.head), re.escape(self.tail))
)
def __str__(self):
'''Return string represenation.'''
return self.format()
def __repr__(self):
'''Return representation.'''
return '<{0} "{1}">'.format(self.__class__.__name__, self)
def __iter__(self):
'''Return iterator over items in collection.'''
for index in self.indexes:
formatted_index = '{0:0{1}d}'.format(index, self.padding)
item = '{0}{1}{2}'.format(self.head, formatted_index, self.tail)
yield item
def __contains__(self, item):
'''Return whether *item* is present in collection.'''
match = self.match(item)
if not match:
return False
if not int(match.group('index')) in self.indexes:
return False
return True
def __eq__(self, other):
'''Return whether *other* collection is equal.'''
if not isinstance(other, Collection):
return NotImplemented
return all([
other.head == self.head,
other.tail == self.tail,
other.padding == self.padding,
other.indexes == self.indexes
])
def __ne__(self, other):
'''Return whether *other* collection is not equal.'''
result = self.__eq__(other)
if result is NotImplemented:
return result
return not result
def __gt__(self, other):
'''Return whether *other* collection is greater than.'''
if not isinstance(other, Collection):
return NotImplemented
a = (self.head, self.tail, self.padding, len(self.indexes))
b = (other.head, other.tail, other.padding, len(other.indexes))
return a > b
def __lt__(self, other):
'''Return whether *other* collection is less than.'''
result = self.__gt__(other)
if result is NotImplemented:
return result
return not result
def __ge__(self, other):
'''Return whether *other* collection is greater than or equal.'''
result = self.__eq__(other)
if result is NotImplemented:
return result
if result is False:
result = self.__gt__(other)
return result
def __le__(self, other):
'''Return whether *other* collection is less than or equal.'''
result = self.__eq__(other)
if result is NotImplemented:
return result
if result is False:
result = self.__lt__(other)
return result
def match(self, item):
'''Return whether *item* matches this collection expression.
If a match is successful return data about the match otherwise return
None.
'''
match = self._expression.match(item)
if not match:
return None
index = match.group('index')
padded = False
if match.group('padding'):
padded = True
if self.padding == 0:
if padded:
return None
elif len(index) != self.padding:
return None
return match
def add(self, item):
'''Add *item* to collection.
raise :py:class:`~error.CollectionError` if *item* cannot be
added to the collection.
'''
match = self.match(item)
if match is None:
raise error.CollectionError(
'Item does not match collection expression.'
)
self.indexes.add(int(match.group('index')))
def remove(self, item):
'''Remove *item* from collection.
raise :py:class:`~error.CollectionError` if *item* cannot be
removed from the collection.
'''
match = self.match(item)
if match is None:
raise error.CollectionError(
'Item not present in collection.'
)
index = int(match.group('index'))
try:
self.indexes.remove(index)
except KeyError:
raise error.CollectionError(
'Item not present in collection.'
)
def format(self, pattern='{head}{padding}{tail} [{ranges}]'):
'''Return string representation as specified by *pattern*.
Pattern can be any format accepted by Python's standard format function
and will receive the following keyword arguments as context:
* *head* - Common leading part of the collection.
* *tail* - Common trailing part of the collection.
* *padding* - Padding value in ``%0d`` format.
* *range* - Total range in the form ``start-end``
* *ranges* - Comma separated ranges of indexes.
* *holes* - Comma separated ranges of missing indexes.
'''
data = {}
data['head'] = self.head
data['tail'] = self.tail
if self.padding:
data['padding'] = '%0{0}d'.format(self.padding)
else:
data['padding'] = '%d'
if '{holes}' in pattern:
data['holes'] = self.holes().format('{ranges}')
if '{range}' in pattern or '{ranges}' in pattern:
indexes = list(self.indexes)
indexes_count = len(indexes)
if indexes_count == 0:
data['range'] = ''
elif indexes_count == 1:
data['range'] = '{0}'.format(indexes[0])
else:
data['range'] = '{0}-{1}'.format(
indexes[0], indexes[-1]
)
if '{ranges}' in pattern:
separated = self.separate()
if len(separated) > 1:
ranges = [collection.format('{range}')
for collection in separated]
else:
ranges = [data['range']]
data['ranges'] = ', '.join(ranges)
return pattern.format(**data)
def is_contiguous(self):
'''Return whether entire collection is contiguous.'''
previous = None
for index in self.indexes:
if previous is None:
previous = index
continue
if index != (previous + 1):
return False
previous = index
return True
def holes(self):
'''Return holes in collection.
Return :py:class:`~collection.Collection` of missing indexes.
'''
missing = set([])
previous = None
for index in self.indexes:
if previous is None:
previous = index
continue
if index != (previous + 1):
missing.update(range(previous + 1, index))
previous = index
return Collection(self.head, self.tail, self.padding, indexes=missing)
def is_compatible(self, collection):
'''Return whether *collection* is compatible with this collection.
To be compatible *collection* must have the same head, tail and padding
properties as this collection.
'''
return all([
isinstance(collection, Collection),
collection.head == self.head,
collection.tail == self.tail,
collection.padding == self.padding
])
def merge(self, collection):
'''Merge *collection* into this collection.
If the *collection* is compatible with this collection then update
indexes with all indexes in *collection*.
raise :py:class:`~error.CollectionError` if *collection* is not
compatible with this collection.
'''
if not self.is_compatible(collection):
raise error.CollectionError('Collection is not compatible '
'with this collection.')
self.indexes.update(collection.indexes)
def separate(self):
'''Return contiguous parts of collection as separate collections.
Return as list of :py:class:`~collection.Collection` instances.
'''
collections = []
start = None
end = None
for index in self.indexes:
if start is None:
start = index
end = start
continue
if index != (end + 1):
collections.append(
Collection(self.head, self.tail, self.padding,
indexes=set(range(start, end + 1)))
)
start = index
end = index
if start is None:
collections.append(
Collection(self.head, self.tail, self.padding)
)
else:
collections.append(
Collection(self.head, self.tail, self.padding,
indexes=range(start, end + 1))
)
return collections

View file

@ -1,43 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
# :license: See LICENSE.txt.
class Unsettable(object):
'''Prevent standard setting of property.
Example::
>>> class Foo(object):
...
... x = Unsettable('x')
...
... def __init__(self):
... self.__dict__['x'] = True
...
>>> foo = Foo()
>>> print foo.x
True
>>> foo.x = False
AttributeError: Cannot set attribute defined as unsettable.
'''
def __init__(self, label):
'''Initialise descriptor with property *label*.
*label* should match the name of the property being described::
x = Unsettable('x')
'''
self.label = label
super(Unsettable, self).__init__()
def __get__(self, instance, owner):
'''Return value of property for *instance*.'''
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
'''Set *value* for *instance* property.'''
raise AttributeError('Cannot set attribute defined as unsettable.')

View file

@ -1,10 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
# :license: See LICENSE.txt.
'''Custom error classes.'''
class CollectionError(Exception):
'''Raise when a collection error occurs.'''

View file

@ -1,62 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
# :license: See LICENSE.txt.
import collections
import bisect
class SortedSet(collections.MutableSet):
'''Maintain sorted collection of unique items.'''
def __init__(self, iterable=None):
'''Initialise with items from *iterable*.'''
super(SortedSet, self).__init__()
self._members = []
if iterable:
self.update(iterable)
def __str__(self):
'''Return string representation.'''
return str(self._members)
def __repr__(self):
'''Return representation.'''
return '<{0} "{1}">'.format(self.__class__.__name__, self)
def __contains__(self, item):
'''Return whether *item* is present.'''
return self._index(item) >= 0
def __len__(self):
'''Return number of items.'''
return len(self._members)
def __iter__(self):
'''Return iterator over items.'''
return iter(self._members)
def add(self, item):
'''Add *item*.'''
if not item in self:
index = bisect.bisect_right(self._members, item)
self._members.insert(index, item)
def discard(self, item):
'''Remove *item*.'''
index = self._index(item)
if index >= 0:
del self._members[index]
def update(self, iterable):
'''Update items with those from *iterable*.'''
for item in iterable:
self.add(item)
def _index(self, item):
'''Return index of *item* in member list or -1 if not present.'''
index = bisect.bisect_left(self._members, item)
if index != len(self) and self._members[index] == item:
return index
return -1