Package: invenio-rdm-migrator
DataCite-based data model for Invenio.DevelopmentInstallMake sure that you havelibpq-devinstalled in your system. Seepsycopg installation instructionsfor more information.Choose a version of search and database, then run:pip install -e .Tests./run-tests.shHow to run itTo run the migration you need:A running InvenioRDM instance.If your data contains references to other records (e.g. vocabularies),
then it is also required to run the setup step.invenio-cli services setup --force --no-demo-dataInstall Invenio-RDM-Migrator, any other dependencies must be handled
in the Pipfile of your instance.$pipinstallinvenio-rdm-migratorCreate/edit the configuration file on your instance, for examplestreams.yaml:data_dir:/path/to/datatmp_dir:/path/to/tmpstate_dir:/path/to/statelog_dir:/path/to/logsdb_uri:postgresql+psycopg2://inveniordm:inveniordm@localhost:5432/inveniordmold_secret_key:CHANGE_MEnew_secret_key:CHANGE_MErecords:extract:filename:/path/to/records.jsonYou will need to create a small python script
putting together the different blocks of the ETL. You can find an eample
atmy-site/site/my_site/migrator/__main__.py.frominvenio_rdm_migrator.streamsimportStreamDefinitionfrominvenio_rdm_migrator.streams.recordsimportRDMRecordCopyLoadif__name__=="__main__":RecordStreamDefinition=StreamDefinition(name="records",extract_cls=JSONLExtract,transform_cls=ZenodoToRDMRecordTransform,load_cls=RDMRecordCopyLoad,)runner=Runner(stream_definitions=[RecordStreamDefinition,],config_filepath="path/to/your/streams.yaml",)runner.run()Finally, you can execute the above code. Since it is in the__main__file
of the python package, you can run it as a module:$python-mmy_site.migratorOnce the migration has completed, in your instance you can reindex the data.
Following the records example above, it would look like:$invenio-clipyshellIn [1]: from invenio_access.permissions import system_identity
In [2]: from invenio_rdm_records.proxies import current_rdm_records_service
In [3]: current_rdm_records_service.rebuild_index(identity=system_identity)ETL {Extract/Transform/Load} architectureThere are four packages in this moduleextract,transform,load, andstreams. The first three correspond to the three steps of an ETL process.
Thestreamspackage contains the logic to run the process and different
stream-specific implementations of ETL classes (e.g.records).ExtractThe extract is the first part of the data processing stream. It’s
functionality is quite simple: return an iterator (e.g. of records), where each
yielded value is a dictionary. Note that the data in this step is _transformed_
in format (e.g. JSON, XML), not in content. For example, the implementation ofXMLExtractwould look as follows:classXMLExtract(Extract):...defrun(self):withopen("file.xml")asfile:forentryinfile:yieldxml.loads(entry)TransformThe transformer is in charge of modifying the content to suit, in this case,
the InvenioRDM data model (e.g. for records) so it can be imported in the DB.
It will loop through the entries (i.e. the iterator returned by the extract
class), transform and yield (e.g. the record). Diving more in the example of
a record:To transform something to an RDM record, you need to implementstreams/records/transform.py:RDMRecordTransform. For each record it will
yield what is considered a semantically “full” record: the record itself,
its parent, its draft in case it exists and the files related them.{"record":self._record(entry),"draft":self._draft(entry),"parent":self._parent(entry),}This means that you will need to implement the functions for each key. Note
that, only_recordand_parentshould return content, the others can returnNone.Some of these functions can themselves use atransform/base:Entrytransformer. An _entry_ transformer is an extra layer of abstraction, to
provide an interface with the methods needed to generate valid data for part of
theTransformclass. In the record example, you can implementtransform.base:RDMRecordEntry, which can be used in theRDMRecordTransform._recordfunction mentioned in the code snippet above. Note
that implementing this interface will produce valid _data_ for a record.
However, there is no abc for _metadata_. It is an open question how much we
should define these interfaces and avoid duplicating the already existing
Marshmallow schemas of InvenioRDM.At this point you might be wondering “Why not Marshmallow then?”. The answer is
“separation of responsibilities, performance and simplicity”. The later lays
with the fact that most of the data transformation is custom, so we would end
up with a schema full ofMethodfields, which does not differ much from what
we have but would have an impact on performance (Marshmallow is slow…).
Regarding the responsibilities part, validating (mostly referential, like
vocabularies) can only be done on (or after) _load_ where RDM instance knowledge/appctx
is available.Note that no validation, not even structural, is done in this step.LoadThe final step to have the records available in the RDM instance is to load
them. There are two types of loading _bulk_ or _transactions_.BulkBulk loading will insert data in the database table by table usingCOPY. Since
the order of the tables is not guaranteed it is necessary to drop foreign keys before
loading. They can be restored afterwards. In addition, dropping indices would increase
performance since they will only be calculated once, when they are restored after loading.Bulk loading is done using theload.postgresql.bulk:PostgreSQLCopyLoadclass, which will
carry out 2 steps:Prepare the data, writing one DB row per line in a csv file:$/path/to/data/tables1668697280.943311|
    | - pidstore_pid.csv
    | - rdm_parents_metadata.csv
    | - rdm_records_metadata.csv
    | - rdm_versions_state.csvPerform the actual loading, usingCOPY. Inserting all rows at once is more
efficient than performing oneINSERTper row.Internally what is happening is that thepreparefunction makes use ofTableGeneratorimplementations and then yields the list of csv files.
So theloadonly iterates through the filenames, not the actual entries.ATableGeneratorwill, for each value in the iterator, yield one
or more rows (lines to be written to the a csv file). For example for a record
it will yield: recid, DOI and OAI (PersistentIdentifiers), record and parent
metadata, etc. which will be written to the respective CSV file.TransactionsAnother option is to migrate transactions. For example, once you have done the initial
part of it in bulk, you can migrate the changes that were persisted while the bulk
migration happened. That can be achieved by migrating transactions. A transaction is a
group of operations, which can be understod as SQL statement and thus have two values:
the operation type (created, update, delete) and its data represented as a database model.Transaction loading is done using theload.postgresql.transactions:PostgreSQLExecuteLoadclass, which will carry out 2 similar steps to the one above:Prepare the data, storing in memory a series ofOperations.Perform the actual loading by adding or removing from the session, or updating the
corresponding object. Each operation is flushed to the database to avoid foreing key
violations. However, each transaction is atomic, meaning that an error in one of the
operations will cause the full transaction to fail as a group.Internally, the load will use an instance ofload.postgresql.transactions.generators.group:TxGeneratorto prepare the
operations. This class contains a mapping between table names andload.postgresql.transactions.generators.row:RowGenerators, which will return a list of
operations with the data as database model in theobjattribute.Note that theTxGeneratoris tightly coupled to thetransform.transactions.Txsince it expects the dictionaries to have a
specific structure:{
    "tx_id": the actual transaction id, useful for debug and error handling
    "action": this information refers to the semantic meaning of the group
                   for example: record metadata update or file upload
    "operations": [
        {
            "op": c (create), u (update), d (delete)
            "table": the name of the table in the source system (e.g. Zenodo)
            "data": the transformed data, this can use any `Transform` implementation
        }
    ]
}StateDuring a migration run, there is a need to share information across different streams
or different generators on the same stream. For example, the records stream needs to
access the UUID to slug map that was populated on the communities stream; or the
drafts generator needs to know which parent records have been created on the records
generator to keep the version state consistent.All this information is persisted to a SQLite database. This state database is kept
in memory during each stream processing, and it is persisted to disk if the stream
finishes without errors. The state will be saved with the name of the stream
(e.g.records.db) to avoid overwriting a previous state. Therefore, a migration can be
restarted from any stream.There are two ways to add more information to the state:Full entities, for example record or users, require their own DB table. Those must be
defined atstate.py:State._initialize_db. In addition, to abstract the access to that
table, a state entity is required. It needs to be initialized in theRunner.py:Runnerconstructor and added the thestate_entitiesdictionary.Independent value, for example the maximum value of generated primary keys. Those can be
stored in theglobal_state. This state has two columns: key and value; adding
information to it would look like{key: name_of_the_value, value: actual_value}.NotesUsing python generatorsUsing generators instead of lists, allows us to iterate through the data
only once and perform the E-T-L steps on them. Instead of loop for E, loop
for T, loop for L. In addition, this allows us to have the csv files open
during the writing and closing them at the end (open/close is an expensive
op when done 3M times).ChangesVersion 4.4.1Fix default value for nullable model fields.Version 4.4.0Add GitHub stream.Addverified_atandblocked_atfor usesr models.Handle parent DOIs for records.Add media files to records and drafts.Adddeletion_statusto record models.Switch toorjsonfor JSON dumping/loading.Add multi-processing for transform.Refactor state to also use Python dict for caching.Version 4.3.0Add community basic CRUD actions.Add DB session fixtures.Version 4.2.0RenameFileUploadActiontoDraftFileUploadAction.Version 4.1.0Add file upload action.Add draft edit action.Version 4.0.0Namespace actions by load and transform.Version 3.1.0AddDatetimeMixinto transform timestamps into iso formatted date strings.AddJSONLoadMixinto load dictionaries from strings.Version 3.0.0Operationinstances have split the model and the data into two attributes.Add user actions.PostgreSQLTxresolve_referencesfunction has now a default behaviour (pass).Add nullable configuration to draft and user related models.Minor bug fixes.Version 2.0.0Make state globally available.Refactor transactions into actions. Create transaction and load data classes.Removed empty kafka extract module.Improved error handling and created specialized classes.Movedict_setto utils.Remove Python 3.8 from test matrix.Version 1.0.0Initial public release.