"""High-level functions for interacting with COALA IP entities"""
import attr
from collections import namedtuple
from coalaip.exceptions import (
EntityNotYetPersistedError,
IncompatiblePluginError,
ModelDataError,
)
from coalaip.entities import Copyright, Right, Manifestation, Work
from coalaip.plugin import AbstractPlugin
RegistrationResult = namedtuple('RegistrationResult',
['copyright', 'manifestation', 'work'])
[docs]@attr.s(frozen=True, repr=False)
class CoalaIp:
"""High-level, plugin-bound COALA IP functions.
Instantiated with an subclass implementing the ledger plugin
interface (:class:`~.AbstractPlugin`) that will automatically be
bound to all top-level functions:
- :func:`generate_user`
- :func:`register_manifestation`
- :func:`derive_right`
- :func:`transfer_right`
Attributes:
plugin (Plugin): Bound persistence layer plugin.
"""
plugin = attr.ib(validator=attr.validators.instance_of(AbstractPlugin))
def __repr__(self):
return 'CoalaIp bound to plugin: {}'.format(self.plugin)
[docs] def generate_user(self, *args, **kwargs):
"""Generate a new user for the backing persistence layer.
Args:
*args: Argument list passed to the plugin's
``generate_user()``
**kwargs: Keyword arguments passed to the plugin's
``generate_user()``
Returns:
A representation of a user, based on the persistence layer
plugin
Raises:
:exc:`~.PersistenceError`: If a user couldn't be generated
on the persistence layer
"""
return self.plugin.generate_user(*args, **kwargs)
[docs] def register_work(self, work_data, *, copyright_holder, **kwargs):
"""Register a work"""
work = Work.from_data(work_data, plugin=self.plugin)
work.create(copyright_holder, **kwargs)
return work
# TODO: could probably have a 'safe' check to make sure the entities are actually created
[docs] def register_manifestation(self, manifestation_data, *, copyright_holder,
existing_work=None, work_data=None,
create_work=True, create_copyright=True,
**kwargs):
"""Register a Manifestation and automatically assign its
corresponding Copyright to the given :attr:`user`.
Unless specified (see :attr:`existing_work`), also registers a
new Work for the Manifestation.
Args:
manifestation_data (dict): Model data for the
:class:`.Manifestation`.
See :class:`~.Manifestation` for requirements.
If ``manifestationOfWork`` is provided in the dict, the
:attr:`existing_work` and :attr:`work_data` parameters are
ignored and no Work is registered.
copyright_holder (any, keyword): The user to hold the
corresponding Copyright of the registered Manifestation;
must be specified in the format required by the
persistence layer
existing_work (:class:`~.Work`, keyword, optional): An
already persisted Work that the Manifestation is derived
from.
Must be using the same plugin that :class:`CoalaIp` was
instantiated with.
If specified, the :attr:`work_data` parameter is ignored
and no Work is registered.
work_data (dict, keyword, optional): Model data for the Work
that will automatically generated for the Manifestation
if no :attr:`existing_work` was specified.
See :class:`~.Work` for requirements.
If not specified, the Work will be created using only
the name of the Manifestation.
create_work (bool, keyword, optional): To allow for the creation
of a Manifestation without attaching a Work. Default is True.
create_copyright (bool, keyword, optional): To allow for the
creation of a Manifestation without attaching a Copyright.
Default is True.
**kwargs: Keyword arguments passed through to each model's
:meth:`~.Entity.create` (e.g. ``data_format``).
Returns:
:class:`~.RegistrationResult`: A :obj:`namedtuple`
containing the Coypright of the registered Manifestation,
the registered Manifestation, and the Work as named fields::
(
'copyright': (:class:`~.Copyright`),
'manifestation': (:class:`~.Manifestation`),
'work': (:class:`~.Work`),
)
If ``manifestationOfWork`` was provided in
:attr:`manifestation_data`, None will be returned for the
Work; otherwise, the given :attr:`existing_work` or
automatically created Work will be returned.
Raises:
:exc:`~.ModelDataError`: If the :attr:`manifestation_data`
or :attr:`work_data` contain invalid or are missing
required properties.
:class:`~.IncompatiblePluginError`: If the
:attr:`existing_work` is not using a compatible plugin
:exc:`~.EntityNotYetPersistedError`: If the
:attr:`existing_work` is not associated with an id on the
persistence layer (:attr:`~.Entity.persist_id`) yet
:exc:`~.EntityCreationError`: If the manifestation, its
copyright, or the automatically created work (if no
existing work is given) fail to be created on the
persistence layer
:exc:`~.PersistenceError`: If any other error occurred with
the persistence layer
"""
# TODO: in the future, we may want to consider blocking (or asyncing) until
# we confirm that an entity has actually been created
work = None
manifestation_copyright = None
if not manifestation_data.get('manifestationOfWork') and create_work:
if existing_work is None:
if work_data is None:
work_data = {'name': manifestation_data.get('name')}
work = Work.from_data(work_data, plugin=self.plugin)
work.create(copyright_holder, **kwargs)
else:
if not isinstance(existing_work, Work):
raise TypeError(
("'existing_work' argument to "
"'register_manifestation()' must be a Work. Given an "
"instance of '{}'".format(type(existing_work))))
elif existing_work.persist_id is None:
raise EntityNotYetPersistedError(
("Work given as 'existing_work' to "
"'register_manifestation()' must be already created "
'on the backing persistence layer.'))
elif existing_work.plugin != self.plugin:
raise IncompatiblePluginError([
self.plugin,
existing_work.plugin,
])
work = existing_work
manifestation_data['manifestationOfWork'] = work.persist_id
manifestation = Manifestation.from_data(manifestation_data,
plugin=self.plugin)
manifestation.create(copyright_holder, **kwargs)
if create_copyright:
copyright_data = {'rightsOf': manifestation.persist_id}
manifestation_copyright = Copyright.from_data(copyright_data,
plugin=self.plugin)
manifestation_copyright.create(copyright_holder, **kwargs)
return RegistrationResult(manifestation_copyright, manifestation, work)
[docs] def derive_right(self, right_data, *, current_holder, source_right=None,
right_entity_cls=Right, **kwargs):
"""Derive a new Right from an existing :attr:`source_right` (a
:class:`~.Right` or subclass) for the :attr:`current_holder` of
the :attr:`source_right`. The newly registered Right can then be
transferred to other Parties.
Args:
right_data (dict): Model data for the :attr:`right_entity_cls`.
See the given :attr:`right_entity_cls` for requirements.
If ``source`` is provided in the dict, the
:attr:`source_right` parameter is ignored.
current_holder (any, keyword): The current holder of the
:attr:`source_right`; must be specified in the format
required by the persistence layer
source_right (:class:`~.Right`, keyword, optional): An
already persisted Right that the new Right is allowed by.
Must be using the same plugin that :class:`CoalaIp` was
instantiated with.
Ignored if ``source`` is provided in :attr:`right_data`.
right_entity_cls (subclass of :class:`~.Right`, keyword, optional):
The class that must be instantiated for the newly
derived right.
Defaults to :class:`~.Right`.
**kwargs: Keyword arguments passed through to the
:attr:`right_entity_cls`'s ``create`` method (e.g.
:meth:`~.Entity.create`'s ``data_format``)
Returns:
A registered :attr:`right_entity_cls` Right (by default a
:class:`~.Right`)
Raises:
:exc:`~.ModelDataError`: If the :attr:`right_data`
contains invalid or is missing required properties.
:exc:`~.EntityNotYetPersistedError`: If the
:attr:`source_right` is not associated with an id on the
persistence layer (:attr:`~.Entity.persist_id`) yet
:exc:`~.EntityCreationError`: If the Right fails to be
created on the persistence layer
:exc:`~.PersistenceError`: If any other error occurred with
the persistence layer
"""
if right_data.get('source'):
# Try to load the given `source` as either a Copyright or Right
try:
try:
source_right = Copyright.from_persist_id(
right_data['source'], plugin=self.plugin,
force_load=True)
except ModelDataError:
source_right = Right.from_persist_id(
right_data['source'], plugin=self.plugin,
force_load=True)
except ModelDataError as ex:
raise ModelDataError(
("Entity loaded for 'source' ('{source}') given in "
"'right_data' was not a Right or Copyright").format(
source=right_data['source'])) from ex
else:
if source_right is None:
raise ValueError(("'source_right' argument to 'derive_right() "
"must be provided if 'source' is not "
"given as part of 'right_data'"))
elif not isinstance(source_right, Right):
raise TypeError(("'source_right' argument to 'derive_right()' "
'must be a Right (or subclass). Given an '
"instance of '{}'".format(type(source_right))))
elif source_right.persist_id is None:
raise EntityNotYetPersistedError(
("Right given as 'source_right' to 'derive_right()' must "
'be already created on the backing persistence layer.')
)
elif source_right.plugin != self.plugin:
raise IncompatiblePluginError([
self.plugin,
source_right.plugin,
])
right_data['source'] = source_right.persist_id
if not self.plugin.is_same_user(source_right.current_owner,
current_holder):
raise ModelDataError(
("The given source Right (either as a 'source' property of "
"'right_data' or as 'source_right') is not currently held by "
"the given 'current_holder'"))
right = right_entity_cls.from_data(right_data, plugin=self.plugin)
right.create(current_holder, **kwargs)
return right
[docs] def transfer_right(self, right, rights_assignment_data=None, *,
current_holder, to, **kwargs):
"""Transfer a Right to another user.
Args:
right (:class:`~.Right`): An already persisted Right to
transfer
rights_assignment_data (dict, optional): Model data for the
generated :class:`~.RightsAssignment` that will be
associated with the transfer
current_holder (any, keyword): The current holder of the
:attr:`right`; must be specified in the format
required by the persistence layer
to (any, keyword): The new holder of the right; must be
specified in the format required by the persistence
layer.
If the specified user format includes private
information (e.g. a private key) but is not required by
the persistence layer to identify a transfer recipient,
then this information may be omitted in this argument.
**kwargs: keyword arguments passed through to the
:attr:`right`'s ``transfer`` method (e.g.
:meth:`~.Right.transfer`'s ``rights_assignment_format``)
Returns:
:class:`~.RightsAssignment`: the RightsAssignment entity
associated with this transfer
Raises:
:exc:`~.EntityNotYetPersistedError`: If the :attr:`right`
has not been persisted yet
:exc:`~.EntityNotFoundError`: If the :attr:`right` was not
found on the persistence layer
:exc:`~.EntityTransferError`: If the :attr:`right` fails to
be transferred on the persistence layer
:exc:`~.PersistenceError`: If any other error occurred with
the persistence layer
"""
if not isinstance(right, Right):
raise TypeError(("'right' argument to 'transfer_right()' must be "
'a Right (or subclass). Given '
"'{}'".format(right)))
elif right.persist_id is None:
raise EntityNotYetPersistedError(
("Right given as 'right' to 'transfer_right()' must be "
'already created on the backing persistence layer.')
)
elif right.plugin != self.plugin:
raise IncompatiblePluginError([self.plugin, right.plugin])
return right.transfer(rights_assignment_data, from_user=current_holder,
to_user=to, **kwargs)