Importable Elements

This module defines some handy Importable elements.

An Importable is usually composed of two different parts:

  • A natural key used to identify the same element across different systems. This is the only required component for an Importable.
  • An optional set of properties that form the contents. The data in this properties is carried across systems in the process of syncing the elements.

Two elements that are the same and have equal contents are said to be in sync.

For example an element representing an online video can use the value of the streaming URL to be its natural key. The contents of the element can be formed from a view counter and the video title. In this scenario changes on the video title and view counter can be detected and carried across systems thus keeping elements which are the same in sync. Changes to the video URL will make the video element lose any correspondence with elements belonging to other systems.

class importtools.importables.Importable(natural_key, *args, **kwargs)[source]

A default implementation representing an importable element.

This class is intended to be specialized in order to provide the element content and to override its behaviour if needed.

The sync() implementation in this class doesn’t keep track of changed values. For such an implementation see RecordingImportable.

Importable instances are hashable and comparable based on the natural_key value. Because of this the natural_key must also be hashable and should implement equality and less then operators:

>>> i1 = Importable(0)
>>> i2 = Importable(0)
>>> hash(i1) == hash(i2)
True
>>> i1 == i2
True
>>> not i1 < i2
True

Importable elements can access the natural_key value used on instantiation trough the natural_key property:

>>> i = Importable((123, 'abc'))
>>> i.natural_key
(123, 'abc')

Listeners can register to observe an Importable element for changes. Every time the content attributes change with a value that is not equal to the previous one all registered listeners will be notified:

>>> class MockImportable(Importable):
...     _content_attrs = ['a', 'b']
>>> i = MockImportable(0)
>>> notifications = []
>>> i.register(lambda x: notifications.append(x))
>>> i.a = []
>>> i.b = 'b'
>>> i.b = 'bb'
>>> len(notifications)
3
>>> notifications[0] is notifications[1] is notifications[2] is i
True
>>> notifications = []
>>> l = []
>>> i.a = l
>>> len(notifications)
0
>>> i.a is l
True

There is also a shortcut for defining new Importable classes other than using inheritance by setting __content_attrs__ to an iterable of attribute names. This will automatically create a constructor for your class that accepts all values in the list as keyword arguments. It also sets _content_attrs and __slots__ to include this values and generates a __repr__ for you. This method however may not fit all your needs, in that case subclassing Importable is still your best option.

One thing to keep in mind is that it’s not possible to dinamicaly change _content_attrs for instances created from this class because of the __slots__ usage.

>>> class MockImportable(Importable):
...     __content_attrs__ = ['a', 'b']
>>> MockImportable(0)
MockImportable(0)
>>> MockImportable(0, a=1, b=('a', 'b'))
MockImportable(0, a=1, b=('a', 'b'))
>>> i = MockImportable(0, a=1)
>>> i.b = 2
>>> i.a, i.b
(1, 2)
>>> i.update(a=100, b=200)
True
update(**kwargs)[source]

Update multiple content attrtibutes and fire a single notification.

Multiple changes to the element content can be grouped in a single call to update(). This method should return True if at least one element differed from the original values or else False.

>>> class MockImportable(Importable):
...     _content_attrs = ['a', 'b']
>>> i = MockImportable(0)
>>> i.register(lambda x: notifications.append(x))
>>> notifications = []
>>> i.update(a=100, b=200)
True
>>> len(notifications)
1
>>> notifications[0] is i
True
>>> notifications = []
>>> i.update(a=100, b=200)
False
>>> len(notifications)
0

Trying to call update using keywords that are not present in _content_attrs souhld raise ValueError:

>>> i.update(c=1) 
Traceback (most recent call last):
ValueError:
sync(other)[source]

Puts this element in sync with the other.

The default implementation uses _content_attrs to search for the attributes that need to be synced between the elements and it copies the values of each attribute it finds from the other element in this one.

By default the self._content_attrs is an empty list so no synchronization will take place:

>>> class MockImportable(Importable):
...     pass
>>> i1 = MockImportable(0)
>>> i2 = MockImportable(0)
>>> i1.a, i1.b = 'a1', 'b1'
>>> i2.a, i2.b = 'a2', 'b2'
>>> has_changed = i1.sync(i2)
>>> i1.a
'a1'
>>> class MockImportable(Importable):
...     _content_attrs = ['a', 'b', 'x']
>>> i1 = MockImportable(0)
>>> i2 = MockImportable(0)
>>> i1.a, i1.b = 'a1', 'b1'
>>> i2.a, i2.b = 'a2', 'b2'
>>> has_changed = i1.sync(i2)
>>> i1.a, i1.b
('a2', 'b2')

If no synchronization was needed (i.e. the content of the elements were equal) this method should return False, otherwise it should return True:

>>> i1.sync(i2)
False
>>> i1.a = 'a1'
>>> i1.sync(i2)
True

If the sync mutated this element all listeners should be notified. See register():

>>> i1.a = 'a1'
>>> notifications = []
>>> i1.register(lambda x: notifications.append(x))
>>> has_changed = i1.sync(i2)
>>> len(notifications)
1
>>> notifications[0] is i1
True

All attributes that can’t be found in the other element are skipped:

>>> i1._content_attrs = ['a', 'b', 'c']
>>> has_changed = i1.sync(i2)
>>> hasattr(i1, 'c')
False
register(listener)[source]

Register a callable to be notified when sync changes data.

This method should raise an ValueError if listener is not a callable:

>>> i = Importable(0)
>>> i.register(1) 
Traceback (most recent call last):
ValueError:

Same listener can register multiple times:

>>> notifications = []
>>> listener = lambda x: notifications.append(x)
>>> i.register(listener)
>>> i.register(listener)
>>> i._notify()
>>> notifications[0] is notifications[1] is i
True
is_registered(listener)[source]

Check if the listener is already registered.

>>> i = Importable(0)
>>> a = lambda x: None
>>> i.is_registered(a)
False
>>> i.register(a)
>>> i.is_registered(a)
True
_notify()[source]

Sends a notification to all listeners passing this element.

class importtools.importables.RecordingImportable(*args, **kwargs)[source]

Bases: importtools.importables.Importable

Very similar to Importable but tracks changes.

This class records the original values that the attributes had before any change introduced by attribute assignment or call to update and sync.

Just as in Importable case you can define new classes using __content_attrs__ as a shortcut.

>>> class MockImportable(RecordingImportable):
...     __content_attrs__ = ['a', 'b']
>>> MockImportable(0)
MockImportable(0)
>>> MockImportable(0, a=1, b=('a', 'b'))
MockImportable(0, a=1, b=('a', 'b'))
>>> i = MockImportable(0, a=1)
>>> i.b = 2
>>> i.a, i.b
(1, 2)
>>> i.update(a=100, b=200)
True
>>> i.orig.a
1
orig[source]

An object that can be used to access the elements original values.

The object has all the attributes that this element had when it was instantiated or last time when reset() was called.

>>> class MockImportable(RecordingImportable):
...   _content_attrs = ['a']
>>> i = MockImportable(0)
>>> hasattr(i.orig, 'a')
False
>>> i.a = 'a'
>>> i.reset()
>>> i.a
'a'
>>> i.orig.a
'a'
>>> i.a = 'aa'
>>> i.a
'aa'
>>> i.orig.a
'a'
>>> del i.a
>>> i.reset()
>>> hasattr(i.orig, 'a')
False
reset()[source]

Create a snapshot of the current values.

>>> class MockImportable(RecordingImportable):
...   _content_attrs = ['a']
>>> i = MockImportable(0)
>>> hasattr(i.orig, 'a')
False
>>> i.a = 'a'
>>> i.reset()
>>> i.a = 'aa'
>>> i.orig.a
'a'
>>> i.reset()
>>> i.orig.a
'aa'

Previous topic

Import Tutorial

Next topic

DataSet Containers

This Page