class DOIDataCitePIDProvider(BasePIDProvider): """DataCite Provider class. Note that DataCite is only contacted when a DOI is reserved or registered, or any action posterior to it. Its creation happens only at PIDStore level. """ name = "datacite" def __init__(self, client, pid_type="doi", default_status=PIDStatus.NEW, generate_suffix_func=None, generate_id_func=None, **kwargs): """Constructor.""" self.client = client self.api_client = DataCiteRESTClient(client.username, client.password, client.prefix, client.test_mode) super().__init__(self.api_client, pid_type, default_status) self.generate_suffix = generate_suffix_func or \ DOIDataCitePIDProvider._generate_suffix self.generate_id = generate_id_func or self._generate_id @staticmethod def _generate_suffix(record, client, **kwargs): """Generate a unique DOI suffix. The content after the slash. """ recid = record.pid.pid_value return f"{client.name}.{recid}" def _generate_id(self, record, **kwargs): """Generate a unique DOI.""" prefix = self.client.prefix suffix = self.generate_suffix(record, self.client, **kwargs) return f"{prefix}/{suffix}" def create(self, record, **kwargs): """Create a new unique DOI PID based on the record ID.""" doi = self.generate_id(record, **kwargs) return super().create(pid_value=doi, object_type="rec", object_uuid=record.id, **kwargs) def reserve(self, pid, record, **kwargs): """Reserve a DOI only in the local system. It does not reserve the DOI in DataCite. :param pid: the PID to reserve. :param record: the record. :returns: `True` if is reserved successfully. """ return super().reserve(pid, record) def register(self, pid, record, url, **kwargs): """Register a DOI via the DataCite API. :param pid: the PID to register. :param record: the record metadata for the DOI. :returns: `True` if is registered successfully. """ super().register(pid, record) # PIDS-FIXME: move to async task, exception handling included try: doc = DataCite43JSONSerializer().dump_one(record) self.api_client.public_doi(metadata=doc, url=url, doi=pid.pid_value) except DataCiteError: pass return True def update(self, pid, record, url, **kwargs): """Update metadata associated with a DOI. This can be called before/after a DOI is registered. :param pid: the PID to register. :param record: the record metadata for the DOI. :returns: `True` if is updated successfully. """ # PIDS-FIXME: Do we want to log when reactivate the DOI # if pid.is_deleted(): try: # PIDS-FIXME: move to async task, exception handling included # Set metadata doc = DataCite43JSONSerializer().dump_one(record) self.api_client.update_doi(metadata=doc, doi=pid.pid_value, url=url) except DataCiteError: pass if pid.is_deleted(): # PIDS-FIXME: Is this correct? pid.sync_status(PIDStatus.REGISTERED) return True def delete(self, pid, record, **kwargs): """Delete/unregister a registered DOI. If the PID has not been reserved then it's deleted only locally. Otherwise, also it's deleted also remotely. :returns: `True` if is deleted successfully. """ # PIDS-FIXME: move to async task, exception handling included try: if pid.is_reserved(): # Delete only works for draft DOIs self.api_client.delete_doi(pid.pid_value) elif pid.is_registered(): self.api_client.hide_doi(pid.pid_value) except DataCiteError: pass return super().delete(pid, record) def validate(self, identifier=None, provider=None, client=None, **kwargs): """Validate the attributes of the identifier.""" super().validate(identifier, provider, client, **kwargs) if identifier: self.api_client.check_doi(identifier) return True
class DOIDataCitePIDProvider(BasePIDProvider): """DataCite Provider class. Note that DataCite is only contacted when a DOI is reserved or registered, or any action posterior to it. Its creation happens only at PIDStore level. """ name = "datacite" def __init__(self, client, pid_type="doi", default_status=PIDStatus.NEW, generate_id_func=None, generate_doi_func=None, **kwargs): """Constructor.""" self.client = client self.api_client = None self.is_api_client_setup = False if client and client.has_credentials(): self.api_client = DataCiteRESTClient(client.username, client.password, client.prefix, client.test_mode) self.is_api_client_setup = True super().__init__(self.api_client, pid_type, default_status) self.generate_id = generate_id_func or \ DOIDataCitePIDProvider._generate_id default_generate_doi = self._generate_doi format_func = current_app.config['RDM_RECORDS_DOI_DATACITE_FORMAT'] if format_func and callable(format_func): default_generate_doi = format_func self.generate_doi = generate_doi_func or default_generate_doi @staticmethod def _log_errors(errors): """Log errors from DataCiteError class.""" # DataCiteError is a tuple with the errors on the first errors = json.loads(errors.args[0])["errors"] for error in errors: field = error["source"] reason = error["title"] current_app.logger.warning(f"Error in {field}: {reason}") @staticmethod def _generate_id(record, **kwargs): """Generate a unique DOI suffix. The content after the slash. """ recid = record.pid.pid_value return f"{recid}" def _generate_doi(self, record, **kwargs): """Generate a unique DOI.""" prefix = self.client.prefix id = self.generate_id(record, **kwargs) format_string = current_app.config['RDM_RECORDS_DOI_DATACITE_FORMAT'] if format_string: return format_string.format(prefix=prefix, id=id) else: return f"{prefix}/datacite.{id}" def create(self, record, **kwargs): """Create a new unique DOI PID based on the record ID.""" doi = self.generate_doi(record, **kwargs) return super().create(record, doi, **kwargs) def reserve(self, pid, record, **kwargs): """Constant True. It does not reserve locally, nor externally. This is to avoid storing many PIDs as cause of reserve/discard, which would then be soft deleted. Therefore we want to pass from status.NEW to status.RESERVED. :param pid: the PID to reserve. :param record: the record. :returns: `True` """ return True def register(self, pid, record, **kwargs): """Register a DOI via the DataCite API. :param pid: the PID to register. :param record: the record metadata for the DOI. :returns: `True` if is registered successfully. """ local_success = super().register(pid, record) if not local_success: return False if self.is_api_client_setup: # PIDS-FIXME: move to async task, exception handling included try: doc = DataCite43JSONSerializer().dump_one(record) url = kwargs["url"] self.api_client.public_doi(metadata=doc, url=url, doi=pid.pid_value) except DataCiteError as e: current_app.logger.warning("DataCite provider error when " f"updating DOI for {pid.pid_value}") self._log_errors(e) return False else: current_app.logger.warning("DataCite client not configured. Cannot" f" register DOI for {pid.pid_value}") return True def update(self, pid, record, url, **kwargs): """Update metadata associated with a DOI. This can be called before/after a DOI is registered. :param pid: the PID to register. :param record: the record metadata for the DOI. :returns: `True` if is updated successfully. """ # PIDS-FIXME: Do we want to log when reactivate the DOI # if pid.is_deleted(): if self.is_api_client_setup: try: # PIDS-FIXME: move to async task, exception handling included # Set metadata doc = DataCite43JSONSerializer().dump_one(record) self.api_client.update_doi(metadata=doc, doi=pid.pid_value, url=url) except DataCiteError as e: current_app.logger.warning("DataCite provider error when " f"updating DOI for {pid.pid_value}") self._log_errors(e) return False else: current_app.logger.warning("DataCite client not configured. Cannot" f" update DOI for {pid.pid_value}") if pid.is_deleted(): return pid.sync_status(PIDStatus.REGISTERED) return True def delete(self, pid, record, **kwargs): """Delete/unregister a registered DOI. If the PID has not been reserved then it's deleted only locally. Otherwise, also it's deleted also remotely. :returns: `True` if is deleted successfully. """ # PIDS-FIXME: move to async task, exception handling included try: if pid.is_reserved(): # Delete only works for draft DOIs if self.is_api_client_setup: self.api_client.delete_doi(pid.pid_value) else: current_app.logger.warning("DataCite client not " "configured. Cannot delete DOI " f"for {pid.pid_value}") elif pid.is_registered(): if self.is_api_client_setup: self.api_client.hide_doi(pid.pid_value) else: current_app.logger.warning("DataCite client not " "configured. Cannot delete DOI " f"for {pid.pid_value}") except DataCiteError as e: current_app.logger.warning("DataCite provider error when deleting " f"DOI for {pid.pid_value}") self._log_errors(e) return False return super().delete(pid, record) def validate(self, record, identifier=None, provider=None, client=None, **kwargs): """Validate the attributes of the identifier. :returns: A tuple (success, errors). The first specifies if the validation was passed successfully. The second one is an array of error messages. """ success, errors = super().validate(record, identifier, provider, client, **kwargs) if identifier and self.is_api_client_setup: # format check try: self.api_client.check_doi(identifier) except ValueError as e: errors.append(str(e)) return (True, []) if not errors else (False, errors)