Harvest datastream that never stops because of a single record.
The invenio-vocabularies DataStream catches only TransformerError / WriterError -
any other exception raised for a record (e.g. a marshmallow ValidationError from an
import deserializer) stops the whole harvest. Entries with transformation errors are not
passed to the writers either, so the failure would never get to the harvest record.
create_datastream therefore wraps
- each transformer in CatchingTransformer -
any exception (and errors a transformer reports in
errors without raising, as a
TransformerError) is stored on the entry as exc (not in errors, so the datastream
still hands the entry to the writers), the entry's data is replaced with
{"record": None, "oai_record": <OAI record>} and the following transformers pass it
through. The oai-service writer stores the exception on the harvest record and leaves
the local record as it is;
- each writer in CatchingWriter -
any exception is stored on the entry as
exc.
Bases: BaseTransformer
Store any exception of the wrapped transformer on the entry instead of raising it.
Source code in oarepo_oaipmh_harvester/datastream.py
| class CatchingTransformer(BaseTransformer):
"""Store any exception of the wrapped transformer on the entry instead of raising it."""
def __init__(self, transformer: BaseTransformer) -> None:
"""Wrap the transformer."""
self._transformer = transformer
@override
def apply(self, stream_entry: StreamEntry, *args: Any, **kwargs: Any) -> StreamEntry:
"""Apply the wrapped transformer, unless a previous one has failed."""
if stream_entry.exc is not None:
return stream_entry
oai_record = _oai_record(stream_entry.entry)
try:
result = self._transformer.apply(stream_entry, *args, **kwargs)
except Exception as err: # a single record must not stop the harvest
current_app.logger.exception(
"Error transforming %s", oai_record.header.identifier if oai_record else stream_entry.entry
)
return _failed(stream_entry, oai_record, err)
if result.errors:
# errors reported without raising - the datastream would not hand the entry to
# the writers, so turn them into an exception that is stored on the harvest record
message = "; ".join(str(error) for error in result.errors)
return _failed(result, oai_record, TransformerError(f"{type(self._transformer).__name__}: {message}"))
return result
|
Wrap the transformer.
Source code in oarepo_oaipmh_harvester/datastream.py
| def __init__(self, transformer: BaseTransformer) -> None:
"""Wrap the transformer."""
self._transformer = transformer
|
Apply the wrapped transformer, unless a previous one has failed.
Source code in oarepo_oaipmh_harvester/datastream.py
| @override
def apply(self, stream_entry: StreamEntry, *args: Any, **kwargs: Any) -> StreamEntry:
"""Apply the wrapped transformer, unless a previous one has failed."""
if stream_entry.exc is not None:
return stream_entry
oai_record = _oai_record(stream_entry.entry)
try:
result = self._transformer.apply(stream_entry, *args, **kwargs)
except Exception as err: # a single record must not stop the harvest
current_app.logger.exception(
"Error transforming %s", oai_record.header.identifier if oai_record else stream_entry.entry
)
return _failed(stream_entry, oai_record, err)
if result.errors:
# errors reported without raising - the datastream would not hand the entry to
# the writers, so turn them into an exception that is stored on the harvest record
message = "; ".join(str(error) for error in result.errors)
return _failed(result, oai_record, TransformerError(f"{type(self._transformer).__name__}: {message}"))
return result
|
CatchingWriter
Bases: BaseWriter
Store any exception of the wrapped writer on the entry instead of raising it.
Source code in oarepo_oaipmh_harvester/datastream.py
| class CatchingWriter(BaseWriter):
"""Store any exception of the wrapped writer on the entry instead of raising it."""
def __init__(self, writer: BaseWriter) -> None:
"""Wrap the writer."""
super().__init__()
self._writer = writer
self.is_async = getattr(writer, "is_async", False)
@override
def write(self, stream_entry: StreamEntry, *args: Any, **kwargs: Any) -> StreamEntry:
"""Write the entry with the wrapped writer."""
try:
return self._writer.write(stream_entry, *args, **kwargs)
except Exception as err: # a single record must not stop the harvest
current_app.logger.exception("Error writing %s", stream_entry.entry)
# we can't know the state of the db connection, rollback is a safe bet
db.session.rollback()
stream_entry.exc = err
return stream_entry
@override
def write_many(self, stream_entries: list[StreamEntry], *args: Any, **kwargs: Any) -> list[StreamEntry]:
"""Write the entries one by one, so that a failure affects only its entry."""
return [self.write(stream_entry, *args, **kwargs) for stream_entry in stream_entries]
|
__init__(writer: BaseWriter) -> None
Wrap the writer.
Source code in oarepo_oaipmh_harvester/datastream.py
| def __init__(self, writer: BaseWriter) -> None:
"""Wrap the writer."""
super().__init__()
self._writer = writer
self.is_async = getattr(writer, "is_async", False)
|
write(stream_entry: StreamEntry, *args: Any, **kwargs: Any) -> StreamEntry
Write the entry with the wrapped writer.
Source code in oarepo_oaipmh_harvester/datastream.py
| @override
def write(self, stream_entry: StreamEntry, *args: Any, **kwargs: Any) -> StreamEntry:
"""Write the entry with the wrapped writer."""
try:
return self._writer.write(stream_entry, *args, **kwargs)
except Exception as err: # a single record must not stop the harvest
current_app.logger.exception("Error writing %s", stream_entry.entry)
# we can't know the state of the db connection, rollback is a safe bet
db.session.rollback()
stream_entry.exc = err
return stream_entry
|
write_many(stream_entries: list[StreamEntry], *args: Any, **kwargs: Any) -> list[StreamEntry]
Write the entries one by one, so that a failure affects only its entry.
Source code in oarepo_oaipmh_harvester/datastream.py
| @override
def write_many(self, stream_entries: list[StreamEntry], *args: Any, **kwargs: Any) -> list[StreamEntry]:
"""Write the entries one by one, so that a failure affects only its entry."""
return [self.write(stream_entry, *args, **kwargs) for stream_entry in stream_entries]
|
create_datastream(readers_config: list[dict], transformers_config: list[dict], writers_config: list[dict], **kwargs: Any) -> DataStream
Create a datastream (as DataStreamFactory.create) with error catching transformers and writers.
Source code in oarepo_oaipmh_harvester/datastream.py
| def create_datastream(
readers_config: list[dict],
transformers_config: list[dict],
writers_config: list[dict],
**kwargs: Any,
) -> DataStream:
"""Create a datastream (as `DataStreamFactory.create`) with error catching transformers and writers."""
return DataStream(
readers=[ReaderFactory.create(config) for config in readers_config],
transformers=[CatchingTransformer(TransformerFactory.create(config)) for config in transformers_config],
writers=[CatchingWriter(WriterFactory.create(config)) for config in writers_config],
**kwargs,
)
|