diff --git a/docs/api.rst b/docs/api.rst index cd064cf..9256793 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,20 +25,23 @@ API Docs ======== +Extension +--------- + .. automodule:: invenio_pidstore.ext :members: - :undoc-members: + +Models +------ .. automodule:: invenio_pidstore.models :members: - :undoc-members: Resolver -------- .. automodule:: invenio_pidstore.resolver :members: - :undoc-members: Providers @@ -46,36 +49,30 @@ Providers .. automodule:: invenio_pidstore.providers :members: - :undoc-members: .. automodule:: invenio_pidstore.providers.base :members: - :undoc-members: .. automodule:: invenio_pidstore.providers.recordid :members: - :undoc-members: Minters ------- .. automodule:: invenio_pidstore.minters :members: - :undoc-members: Fetchers -------- .. automodule:: invenio_pidstore.fetchers :members: - :undoc-members: Exceptions ---------- .. automodule:: invenio_pidstore.errors :members: - :undoc-members: CLI --- @@ -85,4 +82,3 @@ Detailed usage documentation is available by running .. automodule:: invenio_pidstore.cli :members: - :undoc-members: diff --git a/invenio_pidstore/admin.py b/invenio_pidstore/admin.py index 3c32251..43cb823 100644 --- a/invenio_pidstore/admin.py +++ b/invenio_pidstore/admin.py @@ -24,16 +24,12 @@ from flask import current_app, url_for from flask_admin.contrib.sqla import ModelView from flask_admin.contrib.sqla.filters import FilterEqual +from flask_babelex import gettext as _ from markupsafe import Markup from .models import PersistentIdentifier, PIDStatus -def _(x): - """Identity function for string extraction.""" - return x - - def object_formatter(v, c, m, p): """Format object view link.""" endpoint = current_app.config['PIDSTORE_OBJECT_ENDPOINTS'].get( @@ -41,8 +37,8 @@ def object_formatter(v, c, m, p): if endpoint and m.object_uuid: return Markup('{1}'.format( - url_for(endpoint, id=m.object_uuid), - _('View'))) + url_for(endpoint, id=m.object_uuid), + _('View'))) return '' diff --git a/invenio_pidstore/ext.py b/invenio_pidstore/ext.py index ab18942..638bfd9 100644 --- a/invenio_pidstore/ext.py +++ b/invenio_pidstore/ext.py @@ -57,22 +57,36 @@ def __init__(self, app, minters_entry_point_group=None, self.load_fetchers_entry_point_group(fetchers_entry_point_group) def register_minter(self, name, minter): - """Register a minter.""" + """Register a minter. + + :param name: Minter name. + :param minter: The new minter. + """ assert name not in self.minters self.minters[name] = minter def register_fetcher(self, name, fetcher): - """Register a fetcher.""" + """Register a fetcher. + + :param name: Fetcher name. + :param fetcher: The new fetcher. + """ assert name not in self.fetchers self.fetchers[name] = fetcher def load_minters_entry_point_group(self, entry_point_group): - """Load minters from an entry point group.""" + """Load minters from an entry point group. + + :param entry_point_group: The entrypoint group. + """ for ep in pkg_resources.iter_entry_points(group=entry_point_group): self.register_minter(ep.name, ep.load()) def load_fetchers_entry_point_group(self, entry_point_group): - """Load fetchers from an entry point group.""" + """Load fetchers from an entry point group. + + :param entry_point_group: The entrypoint group. + """ for ep in pkg_resources.iter_entry_points(group=entry_point_group): self.register_fetcher(ep.name, ep.load()) @@ -83,7 +97,13 @@ class InvenioPIDStore(object): def __init__(self, app=None, minters_entry_point_group='invenio_pidstore.minters', fetchers_entry_point_group='invenio_pidstore.fetchers'): - """Extension initialization.""" + """Extension initialization. + + :param minters_entry_point_group: The entrypoint for minters. + (Default: `invenio_pidstore.minters`). + :param fetchers_entry_point_group: The entrypoint for fetchers. + (Default: `invenio_pidstore.fetchers`). + """ if app: self._state = self.init_app( app, minters_entry_point_group=minters_entry_point_group, @@ -92,7 +112,22 @@ def __init__(self, app=None, def init_app(self, app, minters_entry_point_group=None, fetchers_entry_point_group=None): - """Flask application initialization.""" + """Flask application initialization. + + Initialize: + + * The CLI commands. + + * Initialize the logger (Default: `app.debug`). + + * Initialize the default admin object link endpoint. + (Default: `{"rec": "recordmetadata.details_view"}` if + `invenio-records` is installed, otherwise `{}`). + + * Register the `pid_exists` template filter. + + * Initialize extension state. + """ # Initialize CLI app.cli.add_command(cmd) diff --git a/invenio_pidstore/fetchers.py b/invenio_pidstore/fetchers.py index 1d92279..7ad0ac1 100644 --- a/invenio_pidstore/fetchers.py +++ b/invenio_pidstore/fetchers.py @@ -22,7 +22,24 @@ # waive the privileges and immunities granted to it by virtue of its status # as an Intergovernmental Organization or submit itself to any jurisdiction. -"""Persistent identifier minters.""" +"""Persistent identifier fetchers. + +A proper fetcher is defined as a function that return a +:data:`invenio_pidstore.fetchers.FetchedPID` instance. + +E.g. + +.. code-block:: python + + def my_fetcher(record_uuid, data): + return FetchedPID( + provider=MyRecordIdProvider, + pid_type=MyRecordIdProvider.pid_type, + pid_value=extract_pid_value(data), + ) + +To see more about providers see :mod:`invenio_pidstore.providers`. +""" from __future__ import absolute_import, print_function @@ -31,10 +48,16 @@ from .providers.recordid import RecordIdProvider FetchedPID = namedtuple('FetchedPID', ['provider', 'pid_type', 'pid_value']) +"""A pid fetcher.""" def recid_fetcher(record_uuid, data): - """Fetch a record's identifiers.""" + """Fetch a record's identifiers. + + :param record_uuid: The record UUID. + :param data: The record metadata. + :returns: A :data:`invenio_pidstore.fetchers.FetchedPID` instance. + """ return FetchedPID( provider=RecordIdProvider, pid_type=RecordIdProvider.pid_type, diff --git a/invenio_pidstore/minters.py b/invenio_pidstore/minters.py index f67d0fa..ff60c69 100644 --- a/invenio_pidstore/minters.py +++ b/invenio_pidstore/minters.py @@ -30,7 +30,29 @@ def recid_minter(record_uuid, data): - """Mint record identifiers.""" + """Mint record identifiers. + + This is a minter specific for records. + With the help of + :class:`invenio_pidstore.providers.recordid.RecordIdProvider`, it creates + the PID instance with `rec` as predefined `object_type`. + + Procedure followed: + + #. If a `control_number` field is already there, a `AssertionError` + exception is raised. + + #. The provider is initialized with the help of + :class:`invenio_pidstore.providers.recordid.RecordIdProvider`. + It's called with default value 'rec' for `object_type` and `record_uuid` + variable for `object_uuid`. + + #. The new `id_value` is stored inside `data` as `control_number` field. + + :param record_uuid: The record UUID. + :param data: The record metadata. + :returns: A fresh `invenio_pidstore.models.PersistentIdentifier` instance. + """ assert 'control_number' not in data provider = RecordIdProvider.create( object_type='rec', object_uuid=record_uuid) diff --git a/invenio_pidstore/models.py b/invenio_pidstore/models.py index 9faed13..44697bc 100644 --- a/invenio_pidstore/models.py +++ b/invenio_pidstore/models.py @@ -145,6 +145,13 @@ def create(cls, pid_type, pid_value, pid_provider=None, :param pid_type: Persistent identifier type. :param pid_value: Persistent identifier value. + :param pid_provider: Persistent identifier provider. + :param status: Current PID status. + (Default: :attr:`invenio_pidstore.models.PIDStatus.NEW`) + :param object_type: The object type is a string that identify its type. + :param object_uuid: The object UUID. + :returns: A :class:`invenio_pidstore.models.PersistentIdentifier` + instance. """ try: with db.session.begin_nested(): @@ -185,7 +192,16 @@ def create(cls, pid_type, pid_value, pid_provider=None, @classmethod def get(cls, pid_type, pid_value, pid_provider=None): - """Get persistent identifier.""" + """Get persistent identifier. + + :param pid_type: Persistent identifier type. + :param pid_value: Persistent identifier value. + :param pid_provider: Persistent identifier provider. + :raises: :exc:`invenio_pidstore.errors.PIDDoesNotExistError` if no + PID is found. + :returns: A :class:`invenio_pidstore.models.PersistentIdentifier` + instance. + """ try: args = dict(pid_type=pid_type, pid_value=six.text_type(pid_value)) if pid_provider: @@ -196,7 +212,16 @@ def get(cls, pid_type, pid_value, pid_provider=None): @classmethod def get_by_object(cls, pid_type, object_type, object_uuid): - """Get a persistent identifier for a given object.""" + """Get a persistent identifier for a given object. + + :param pid_type: Persistent identifier type. + :param object_type: The object type is a string that identify its type. + :param object_uuid: The object UUID. + :raises: :exc:`invenio_pidstore.errors.PIDDoesNotExistError` if no + PID is found. + :returns: A :class:`invenio_pidstore.models.PersistentIdentifier` + instance. + """ try: return cls.query.filter_by( pid_type=pid_type, @@ -210,11 +235,19 @@ def get_by_object(cls, pid_type, object_type, object_uuid): # Assigned object methods # def has_object(self): - """Determine if this PID has an assigned object.""" + """Determine if this PID has an assigned object. + + :returns: `True` if the PID has a object assigned. + """ return bool(self.object_type and self.object_uuid) def get_assigned_object(self, object_type=None): - """Return an assigned object.""" + """Return the current assigned object UUID. + + :param object_type: If it's specified, returns only if the PID + object_type is the same, otherwise returns None. + :returns: The object UUID. + """ if object_type is not None: if self.object_type == object_type: return self.object_uuid @@ -228,6 +261,15 @@ def assign(self, object_type, object_uuid, overwrite=False): Note, the persistent identifier must first have been reserved. Also, if an existing object is already assigned to the pid, it will raise an exception unless overwrite=True. + + :param object_type: The object type is a string that identify its type. + :param object_uuid: The object UUID. + :param overwrite: Force PID overwrites in case was previously assigned. + :raises: :exc:`invenio_pidstore.errors.PIDInvalidAction` if the PID was + previously deleted. + :raises: :exc:`invenio_pidstore.errors.PIDObjectAlreadyAssigned` if + the PID was previously assigned with a different type/uuid. + :returns: `True` if the PID is successfully assigned. """ if self.is_deleted(): raise PIDInvalidAction( @@ -262,7 +304,13 @@ def assign(self, object_type, object_uuid, overwrite=False): return True def unassign(self): - """Unassign the registered object.""" + """Unassign the registered object. + + Note: + Only registered PIDs can be redirected so we set it back to registered. + + :returns: `True` if the PID is successfully unassigned. + """ if self.object_uuid is None and self.object_type is None: return True @@ -285,14 +333,26 @@ def unassign(self): return True def get_redirect(self): - """Get redirected persistent identifier.""" + """Get redirected persistent identifier. + + :returns: The :class:`invenio_pidstore.models.PersistentIdentifier` + instance. + """ return Redirect.query.get(self.object_uuid).pid # # Status methods. # def redirect(self, pid): - """Redirect persistent identifier to another persistent identifier.""" + """Redirect persistent identifier to another persistent identifier. + + :param pid: The :class:`invenio_pidstore.models.PersistentIdentifier` + where redirect the PID. + :raises: :exc:`invenio_pidstore.errors.PIDInvalidAction` if the PID is + not registered or is not already redirecting to another PID. + :raises: :exc:`invenio_pidstore.errors.PIDDoesNotExistError`. + :returns: `True` if the PID is successfully redirect. + """ if not (self.is_registered() or self.is_redirected()): raise PIDInvalidAction("Persistent identifier is not registered.") @@ -324,6 +384,10 @@ def reserve(self): Note, the reserve method may be called multiple times, even if it was already reserved. + + :raises: :exc:`invenio_pidstore.errors.PIDInvalidAction` if the PID is + not new or is not already reserved a PID. + :returns: `True` if the PID is successfully reserved. """ if not (self.is_new() or self.is_reserved()): raise PIDInvalidAction( @@ -340,7 +404,13 @@ def reserve(self): return True def register(self): - """Register the persistent identifier with the provider.""" + """Register the persistent identifier with the provider. + + :raises: :exc:`invenio_pidstore.errors.PIDInvalidAction` if the PID is + not already registered or is deleted or is a redirection to another + PID. + :returns: `True` if the PID is successfully register. + """ if self.is_registered() or self.is_deleted() or self.is_redirected(): raise PIDInvalidAction( "Persistent identifier has already been registered" @@ -357,7 +427,14 @@ def register(self): return True def delete(self): - """Delete the persistent identifier.""" + """Delete the persistent identifier. + + If the persistent identifier haven't been registered yet, it is + removed from the database. + Otherwise, it's marked as + :data:`invenio_pidstore.models.PIDStatus.DELETED`. + :returns: `True` if the PID is successfully removed. + """ removed = False try: with db.session.begin_nested(): @@ -384,6 +461,9 @@ def sync_status(self, status): Used when the provider uses an external service, which might have been modified outside of our system. + + :param status: The new status to set. + :returns: `True` if the PID is successfully sync. """ if self.status == status: return True @@ -405,19 +485,31 @@ def is_redirected(self): return self.status == PIDStatus.REDIRECTED def is_registered(self): - """Return true if the persistent identifier has been registered.""" + """Return true if the persistent identifier has been registered. + + :returns: A :class:`invenio_pidstore.models.PIDStatus` status. + """ return self.status == PIDStatus.REGISTERED def is_deleted(self): - """Return true if the persistent identifier has been deleted.""" + """Return true if the persistent identifier has been deleted. + + :returns: A boolean value. + """ return self.status == PIDStatus.DELETED def is_new(self): - """Return true if the PIDhas not yet been registered or reserved.""" + """Return true if the PIDhas not yet been registered or reserved. + + :returns: A boolean value. + """ return self.status == PIDStatus.NEW def is_reserved(self): - """Return true if the PID has not yet been reserved.""" + """Return true if the PID has not yet been reserved. + + :returns: A boolean value. + """ return self.status == PIDStatus.RESERVED def __repr__(self): @@ -430,7 +522,19 @@ def __repr__(self): class Redirect(db.Model, Timestamp): - """Redirect for a persistent identifier.""" + """Redirect for a persistent identifier. + + You can redirect a PID to another one. + + E.g. + + .. code-block:: python + + pid1 = PersistentIdentifier.get(pid_type="recid", pid_value="1") + pid2 = PersistentIdentifier.get(pid_type="recid", pid_value="2") + pid1.redirect(pid=pid2) + assert pid2.pid_value == pid.get_redirect().pid_value + """ __tablename__ = 'pidstore_redirect' id = db.Column(UUIDType, default=uuid.uuid4, primary_key=True) diff --git a/invenio_pidstore/providers/base.py b/invenio_pidstore/providers/base.py index ce9b935..5534a39 100644 --- a/invenio_pidstore/providers/base.py +++ b/invenio_pidstore/providers/base.py @@ -44,7 +44,17 @@ class BaseProvider(object): @classmethod def create(cls, pid_type=None, pid_value=None, object_type=None, object_uuid=None, status=None, **kwargs): - """Create a new instance for the given type and pid.""" + """Create a new instance for the given type and pid. + + :param pid_type: Persistent identifier type. + :param pid_value: Persistent identifier value. + :param status: Current PID status. + (Default: :attr:`invenio_pidstore.models.PIDStatus.NEW`) + :param object_type: The object type is a string that identify its type. + :param object_uuid: The object UUID. + :returns: A :class:`invenio_pidstore.providers.base.BaseProvider` + instance. + """ assert pid_value assert pid_type or cls.pid_type @@ -60,14 +70,25 @@ def create(cls, pid_type=None, pid_value=None, object_type=None, @classmethod def get(cls, pid_value, pid_type=None, **kwargs): - """Get a persistent identifier for this provider.""" + """Get a persistent identifier for this provider. + + :param pid_type: Persistent identifier type. (Default: configured + :attr:`invenio_pidstore.providers.base.BaseProvider.pid_type`) + :param pid_value: Persistent identifier value. + :returns: A :class:`invenio_pidstore.providers.base.BaseProvider` + instance. + """ return cls( PersistentIdentifier.get(pid_type or cls.pid_type, pid_value, pid_provider=cls.pid_provider), **kwargs) def __init__(self, pid): - """Initialize provider using persistent identifier.""" + """Initialize provider using persistent identifier. + + :param pid: A :class:`invenio_pidstore.models.PersistentIdentifier` + instance. + """ self.pid = pid assert pid.pid_provider == self.pid_provider @@ -76,19 +97,29 @@ def reserve(self): This might or might not be useful depending on the service of the provider. + + See: :meth:`invenio_pidstore.models.PersistentIdentifier.reserve`. """ return self.pid.reserve() def register(self): - """Register a the persistent identifier.""" + """Register a the persistent identifier. + + See: :meth:`invenio_pidstore.models.PersistentIdentifier.register`. + """ return self.pid.register() def update(self): """Update information about the persistent identifier.""" + pass def delete(self): - """Delete a persistent identifier.""" + """Delete a persistent identifier. + + See: :meth:`invenio_pidstore.models.PersistentIdentifier.delete`. + """ return self.pid.delete() def sync_status(self): """Synchronize PIDstatus with remote service provider.""" + pass diff --git a/invenio_pidstore/providers/datacite.py b/invenio_pidstore/providers/datacite.py index dc28ce3..9648e58 100644 --- a/invenio_pidstore/providers/datacite.py +++ b/invenio_pidstore/providers/datacite.py @@ -39,17 +39,48 @@ class DataCiteProvider(BaseProvider): """DOI provider using DataCite API.""" pid_type = 'doi' + """Default persistent identifier type.""" + pid_provider = 'datacite' + """Persistent identifier provider name.""" + default_status = PIDStatus.NEW + """Default status for newly created PIDs by this provider.""" @classmethod def create(cls, pid_value, **kwargs): - """Create a new record identifier.""" + """Create a new record identifier. + + For more information about parameters, + see :meth:`invenio_pidstore.providers.BaseProvider.create`. + + :param pid_value: Persistent identifier value. + :returns: A :class:`invenio_pidstore.providers.DataCiteProvider` + instance. + """ return super(DataCiteProvider, cls).create( pid_value=pid_value, **kwargs) def __init__(self, pid, client=None): - """Initialize provider.""" + """Initialize provider. + + To use the default client, just configure the following variables: + + * `PIDSTORE_DATACITE_USERNAME` as username. + + * `PIDSTORE_DATACITE_PASSWORD` as password. + + * `PIDSTORE_DATACITE_DOI_PREFIX` as DOI prefix. + + * `PIDSTORE_DATACITE_TESTMODE` to `True` if it configured in test mode. + + * `PIDSTORE_DATACITE_URL` as DataCite URL. + + :param pid: A :class:`invenio_pidstore.models.PersistentIdentifier` + instance. + :param client: A client to access to DataCite. + (Default: :class:`datacite.DataCiteMDSClient` instance) + """ super(DataCiteProvider, self).__init__(pid) if client is not None: self.api = client @@ -63,7 +94,11 @@ def __init__(self, pid, client=None): url=current_app.config.get('PIDSTORE_DATACITE_URL')) def reserve(self, doc): - """Reserve a DOI (amounts to upload metadata, but not to mint).""" + """Reserve a DOI (amounts to upload metadata, but not to mint). + + :param doc: Set metadata for DOI. + :returns: `True` if is reserved successfully. + """ # Only registered PIDs can be updated. try: self.pid.reserve() @@ -77,7 +112,11 @@ def reserve(self, doc): return True def register(self, url, doc): - """Register a DOI via the DataCite API.""" + """Register a DOI via the DataCite API. + + :param doc: Set metadata for DOI. + :returns: `True` if is registered successfully. + """ try: self.pid.register() # Set metadata for DOI @@ -96,6 +135,9 @@ def update(self, url, doc): """Update metadata associated with a DOI. This can be called before/after a DOI is registered. + + :param doc: Set metadata for DOI. + :returns: `True` if is updated successfully. """ if self.pid.is_deleted(): logger.info("Reactivate in DataCite", @@ -117,7 +159,13 @@ def update(self, url, doc): return True def delete(self): - """Delete a registered DOI.""" + """Delete a registered DOI. + + If the PID is new then it's deleted only locally. + Otherwise, also it's deleted also remotely. + + :returns: `True` if is deleted successfully. + """ try: if self.pid.is_new(): self.pid.delete() @@ -133,7 +181,10 @@ def delete(self): return True def sync_status(self): - """Synchronize DOI status DataCite MDS.""" + """Synchronize DOI status DataCite MDS. + + :returns: `True` if is sync successfully. + """ status = None try: diff --git a/invenio_pidstore/providers/recordid.py b/invenio_pidstore/providers/recordid.py index cc548f4..809c833 100644 --- a/invenio_pidstore/providers/recordid.py +++ b/invenio_pidstore/providers/recordid.py @@ -44,11 +44,23 @@ class RecordIdProvider(BaseProvider): """ default_status = PIDStatus.RESERVED - """Record IDs are by default registered immediately.""" + """Record IDs are by default registered immediately. + + Default: :attr:`invenio_pidstore.models.PIDStatus.RESERVED` + """ @classmethod def create(cls, object_type=None, object_uuid=None, **kwargs): - """Create a new record identifier.""" + """Create a new record identifier. + + Note: if the object_type and object_uuid values are passed, then the + PID status will be automatically setted to + :attr:`invenio_pidstore.models.PIDStatus.REGISTERED`. + + :param object_type: The object type. + :param object_uuid: The object identifier. + :param kwargs: You can specify the pid_value + """ # Request next integer in recid sequence. assert 'pid_value' not in kwargs kwargs['pid_value'] = str(RecordIdentifier.next()) diff --git a/invenio_pidstore/resolver.py b/invenio_pidstore/resolver.py index 111d6f1..3dfc060 100644 --- a/invenio_pidstore/resolver.py +++ b/invenio_pidstore/resolver.py @@ -26,7 +26,6 @@ from __future__ import absolute_import, print_function -from flask import current_app from sqlalchemy.orm.exc import NoResultFound from .errors import PIDDeletedError, PIDMissingObjectError, \ @@ -57,6 +56,7 @@ def resolve(self, pid_value): """Resolve a persistent identifier to an internal object. :param pid_value: Persistent identifier. + :returns: A tuple containing (pid, object). """ pid = PersistentIdentifier.get(self.pid_type, pid_value)