ricgraph 2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ricgraph/ricgraph.py ADDED
@@ -0,0 +1,2788 @@
1
+ # ########################################################################
2
+ #
3
+ # Ricgraph - Research in context graph
4
+ #
5
+ # ########################################################################
6
+ #
7
+ # MIT License
8
+ #
9
+ # Copyright (c) 2023, 2024 Rik D.T. Janssen
10
+ #
11
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ # of this software and associated documentation files (the "Software"), to deal
13
+ # in the Software without restriction, including without limitation the rights
14
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ # copies of the Software, and to permit persons to whom the Software is
16
+ # furnished to do so, subject to the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be included in all
19
+ # copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ # SOFTWARE.
28
+ #
29
+ # ########################################################################
30
+ #
31
+ # This file is the main file for Ricgraph.
32
+ # Original version Rik D.T. Janssen, December 2022.
33
+ # Updated Rik D.T. Janssen, February, March, September to December 2023.
34
+ # Updated Rik D.T. Janssen, February 2024, March 2024.
35
+ #
36
+ # ########################################################################
37
+
38
+
39
+ """Ricgraph - Research in context graph.
40
+
41
+ This is Ricgraph - Research in context graph, a graph structure with nodes and edges
42
+ to represent information (as properties in the nodes)
43
+ and their relations (the edges). You can find the code and documentation on
44
+ https://github.com/UtrechtUniversity/ricgraph/.
45
+
46
+ Ricgraph uses Neo4j as graph database, https://neo4j.com.
47
+ Neo4j only allows directed graphs, so Ricgraph is a directed graph.
48
+ They have several products. Their free products are:
49
+
50
+ - Neo4j Desktop (https://neo4j.com/download-center/#desktop);
51
+
52
+ - Neo4j Community Edition (https://neo4j.com/download-center/#community).
53
+
54
+ Neo4j Desktop includes Bloom, which allows exploring the graph in a more
55
+ intuitive way. Community Edition allows only Cypher queries.
56
+
57
+ Ricgraph can be extended for other brands of graph databases
58
+ by changing minor bits of the code.
59
+
60
+ All nodes have the properties in RICGRAPH_PROPERTIES_STANDARD and
61
+ RICGRAPH_PROPERTIES_HIDDEN (see ricgraph.ini):
62
+
63
+ In RICGRAPH_PROPERTIES_STANDARD:
64
+
65
+ - name: name of the node, e.g. ISNI, ORCID, DOI, etc.
66
+
67
+ - category: category of the node,
68
+ e.g. person, person-root, book, journal article, dataset, etc.
69
+
70
+ - value: value of the node.
71
+
72
+ In RICGRAPH_PROPERTIES_HIDDEN:
73
+
74
+ - _key: key value of the node, not to be modified by the user.
75
+
76
+ - _source: sorted list of sources a record has been harvested from,
77
+ not to be modified by the user
78
+
79
+ - _history: list of history events of the node, not to be modified by the user.
80
+
81
+ Additional properties for nodes can be added by changing entry RICGRAPH_PROPERTIES_ADDITIONAL
82
+ in ricgraph.ini (make sure 'history_event' is last).
83
+ In the default configuration of Ricgraph, the following properties are included:
84
+
85
+ - comment: comment for a node, can be anything that is thought to be useful.
86
+
87
+ - url_main: main URL for a node, pointing to e.g. the corresponding ISNI, ORCID or DOI
88
+ record on the web.
89
+
90
+ - url_other: other URL for a node, pointing to e.g. the originating record in a source system.
91
+
92
+ - year: year of a research output.
93
+
94
+ - source_event: an event to be added to the _source list.
95
+
96
+ - history_event: an event to be added to the _history list.
97
+ """
98
+
99
+
100
+ import os
101
+ import sys
102
+ from datetime import datetime
103
+ import numpy
104
+ import pandas
105
+ import requests
106
+ import csv
107
+ import uuid
108
+ import json
109
+ from typing import Union, Tuple
110
+ from collections import defaultdict
111
+ import configparser
112
+ from neo4j import GraphDatabase, Driver, Result
113
+ from neo4j.graph import Node
114
+
115
+ __author__ = 'Rik D.T. Janssen'
116
+ __copyright__ = 'Copyright (c) 2023, 2024 Rik D.T. Janssen'
117
+ __email__ = ''
118
+ __license__ = 'MIT License'
119
+ __package__ = 'Ricgraph'
120
+ # __version__ is in __init__.py of this module.
121
+
122
+
123
+ # ########################################################################
124
+ # Start of constants section
125
+ # ########################################################################
126
+ RICGRAPH_INI_FILENAME = 'ricgraph.ini'
127
+ RICGRAPH_KEY_SEPARATOR = '|'
128
+
129
+ # Used for some loop iterations, in case no max iteration for such a loop is specified.
130
+ A_LARGE_NUMBER = 9999999999
131
+
132
+ # For the REST API, we need to return an HTTP response status code. These are
133
+ # listed on https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml.
134
+ # For Ricgraph, we only use one HTTP response status code:
135
+ # https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok.
136
+ HTTP_RESPONSE_OK = 200
137
+ # We use these if an item cannot be found. They should be in the 2xx series
138
+ # since we still return a valid HTTP response. These are in the "unassigned" range,
139
+ # which may be freely used.
140
+ HTTP_RESPONSE_NOTHING_FOUND = 250
141
+ HTTP_RESPONSE_INVALID_SEARCH = 251
142
+
143
+ # This dict is used as a cache for node id's. If we have a node id, we can
144
+ # do a direct lookup for a node in O(1), instead of a search in O(log n).
145
+ # The dict has the format: [Ricgraph _key]: [Node element_id].
146
+ nodes_cache_key_element_id = {}
147
+ # Cache size. 'nodes_cache_key_element_id' will be emptied if it has this number of elements.
148
+ MAX_NODES_CACHE_KEY_ID = 15000
149
+
150
+
151
+ # These counters are used to count the number of accesses to the
152
+ # graph database backend.
153
+ graphdb_nr_creates = 0
154
+ graphdb_nr_reads = 0
155
+ graphdb_nr_updates = 0
156
+ graphdb_nr_deletes = 0
157
+
158
+
159
+ # ########################################################################
160
+ # Research output types used in Ricgraph.
161
+ # Harvested sources most often have a type for a research output.
162
+ # In a harvest script for a certain source, you specify a mapping
163
+ # from the names used in the source you harvest, to the values below.
164
+ # This ensures that Ricgraph uses the same wording for types of
165
+ # research outputs harvested from different sources.
166
+ #
167
+ # An example mapping may look like this:
168
+ # mapping = {
169
+ # 'book': ROTYPE_BOOK,
170
+ # 'dataset': ROTYPE_DATASET,
171
+ # 'software': ROTYPE_SOFTWARE
172
+ # }
173
+ #
174
+ # Call the function lookup_resout_type() to do this mapping.
175
+ # If you add a type, also add it to ROTYPE_ALL.
176
+ #
177
+ # This list is inspired by the Strategy Evaluation Protocol 2021-2027
178
+ # https://www.universiteitenvannederland.nl/files/documenten/Domeinen/Onderzoek/SEP_2021-2027.pdf,
179
+ # Appendix E2.
180
+ ROTYPE_ABSTRACT = 'abstract'
181
+ ROTYPE_BOOK = 'book'
182
+ ROTYPE_BOOKCHAPTER = 'book chapter'
183
+ ROTYPE_CONFERENCE_ARTICLE = 'conference article'
184
+ ROTYPE_DATASET = 'data set'
185
+ ROTYPE_DESIGN = 'design'
186
+ ROTYPE_DIGITAL_VISUAL_PRODUCT = 'digital or visual product'
187
+ ROTYPE_EDITORIAL = 'editorial'
188
+ ROTYPE_ENTRY = 'entry for encyclopedia or dictionary'
189
+ ROTYPE_EXHIBITION_PERFORMANCE = 'exhibition or performance'
190
+ ROTYPE_JOURNAL_ARTICLE = 'journal article'
191
+ ROTYPE_LETTER = 'letter to the editor'
192
+ ROTYPE_MEMORANDUM = 'memorandum'
193
+ ROTYPE_METHOD_DESCRIPTION = 'method description'
194
+ ROTYPE_MODEL = 'model'
195
+ ROTYPE_OTHER_CONTRIBUTION = 'other contribution'
196
+ ROTYPE_PATENT = 'patent'
197
+ ROTYPE_PHDTHESIS = 'PhD thesis'
198
+ ROTYPE_POSTER = 'poster'
199
+ ROTYPE_PREPRINT = 'preprint'
200
+ ROTYPE_PRESENTATION = 'presentation'
201
+ ROTYPE_REGISTERED_REPORT = 'registered report'
202
+ ROTYPE_REPORT = 'report'
203
+ ROTYPE_RETRACTION = 'retraction'
204
+ ROTYPE_REVIEW = 'review'
205
+ ROTYPE_SOFTWARE = 'software'
206
+ ROTYPE_THESIS = 'thesis'
207
+ ROTYPE_WEBSITE = 'website or web publication'
208
+
209
+ ROTYPE_ALL = [ROTYPE_ABSTRACT,
210
+ ROTYPE_BOOK,
211
+ ROTYPE_BOOKCHAPTER,
212
+ ROTYPE_CONFERENCE_ARTICLE,
213
+ ROTYPE_DATASET,
214
+ ROTYPE_DESIGN,
215
+ ROTYPE_DIGITAL_VISUAL_PRODUCT,
216
+ ROTYPE_EDITORIAL,
217
+ ROTYPE_ENTRY,
218
+ ROTYPE_EXHIBITION_PERFORMANCE,
219
+ ROTYPE_JOURNAL_ARTICLE,
220
+ ROTYPE_LETTER,
221
+ ROTYPE_MEMORANDUM,
222
+ ROTYPE_METHOD_DESCRIPTION,
223
+ ROTYPE_MODEL,
224
+ ROTYPE_OTHER_CONTRIBUTION,
225
+ ROTYPE_PATENT,
226
+ ROTYPE_PHDTHESIS,
227
+ ROTYPE_POSTER,
228
+ ROTYPE_PREPRINT,
229
+ ROTYPE_PRESENTATION,
230
+ ROTYPE_REGISTERED_REPORT,
231
+ ROTYPE_REPORT,
232
+ ROTYPE_RETRACTION,
233
+ ROTYPE_REVIEW,
234
+ ROTYPE_SOFTWARE,
235
+ ROTYPE_THESIS,
236
+ ROTYPE_WEBSITE]
237
+
238
+ # ########################################################################
239
+ # End of constants section
240
+ # ########################################################################
241
+
242
+
243
+ global _graph
244
+
245
+
246
+ # ##############################################################################
247
+ # Modification for Python module neo4j.
248
+ # ##############################################################################
249
+
250
+ def node_eq(self, other):
251
+ """Modified __eq__ method for Python module neo4j.
252
+
253
+ The __eq__ method of class Node does not work as expected (as of April 2024).
254
+ Two Nodes are supposed to be equal if their Node.element_id's are equal.
255
+
256
+ I would like to be able to test if a 'node' of type Node is in a list called
257
+ 'mylist' with an 'if' statement like this:
258
+ 'if node in mylist ...' or 'if node not in mylist ...',
259
+ but it does not work: even if Nodes are in that list, the 'if' fails.
260
+ I could have made my own class and extended it with an __eq__ method, but
261
+ I choose this hack. It is called a 'Monkey patch': A Monkey patch is a piece
262
+ of Python code which extends or modifies other code at runtime
263
+ (typically at startup).
264
+
265
+ :return: True on equality, or False if not.
266
+ """
267
+ return isinstance(other, Node) and self.element_id == other.element_id
268
+
269
+
270
+ # Modified __eq__ method for Python module neo4j (also see above).
271
+ Node.__eq__ = node_eq
272
+
273
+
274
+ # ##############################################################################
275
+ # Cypher functions.
276
+ # ##############################################################################
277
+ # These are also functions with cypher:
278
+ # - read_all_nodes()
279
+ # - read_all_values_of_property()
280
+ # - get_all_neighbor_nodes()
281
+ # ##############################################################################
282
+ # Note the use of WHERE clauses below. Some of them uses the function elementId(),
283
+ # which does a _direct_ lookup for a node with that id. That is the fastest way possible,
284
+ # compared to a WHERE clause on node['_key'], which is a search.
285
+ # To observe this difference, prefix a Cypher query with PROFILE, e.g.:
286
+ # PROFILE MATCH (n) WHERE elementId(n)=[a string here] RETURN *
287
+ #
288
+ # Read: https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-elementid
289
+ # https://neo4j.com/docs/cypher-manual/current/planning-and-tuning/operators/operators-detail/#query-plan-node-by-elementid-seek
290
+ #
291
+ # [May 6, 2024] Note however, that memgraph on does not have elementId() but only id():
292
+ # https://memgraph.com/docs/querying/differences-in-cypher-implementations.
293
+ # ##############################################################################
294
+
295
+ def open_ricgraph() -> Driver:
296
+ """Open Ricgraph.
297
+
298
+ :return: graph that has been opened.
299
+ """
300
+ global _graph
301
+
302
+ print('Opening ricgraph.\n')
303
+ try:
304
+ _graph = GraphDatabase.driver(GRAPHDB_URL,
305
+ auth=(GRAPHDB_USER, GRAPHDB_PASSWORD))
306
+ _graph.verify_connectivity()
307
+ except:
308
+ print('open_ricgraph(): Error opening graph database backend using these parameters:')
309
+ print('GRAPHDB_URL: ' + GRAPHDB_URL)
310
+ print('GRAPHDB_DATABASENAME: ' + ricgraph_databasename())
311
+ print('GRAPHDB_USER: ' + GRAPHDB_USER)
312
+ print('GRAPHDB_PASSWORD: [see ricgraph.ini file]')
313
+ print('\nAre these parameters correct and did you start your graph database backend "' + GRAPHDB + '"?')
314
+ print('Exiting.')
315
+ exit(1)
316
+
317
+ return _graph
318
+
319
+
320
+ def close_ricgraph() -> None:
321
+ """Close Ricgraph.
322
+ """
323
+ global _graph
324
+
325
+ print('Closing ricgraph.\n')
326
+ _graph.close()
327
+ return
328
+
329
+
330
+ def ricgraph_database() -> str:
331
+ """Return the name of the Ricgraph database backend.
332
+ """
333
+ return GRAPHDB
334
+
335
+
336
+ def ricgraph_databasename() -> str:
337
+ """Return the name of the database in the Ricgraph database backend.
338
+ """
339
+ return GRAPHDB_DATABASENAME
340
+
341
+
342
+ def empty_ricgraph(answer: str = '') -> None:
343
+ """Empty Ricgraph and create new indexes. Side effect: indexes are deleted and created.
344
+
345
+ :param answer: prefilled answer whether the user wants to empty Ricgraph.
346
+ 'yes': Ricgraph will be emptied, no questions asked;
347
+ 'no': Ricgraph will not be emptied, no questions asked;
348
+ any other answer: the user will be asked whether to empty Ricgraph.
349
+ :return: None.
350
+ """
351
+ global _graph
352
+
353
+ if answer == 'yes':
354
+ print('Emptying Ricgraph: Ricgraph will be emptied.\n')
355
+ # Fall through.
356
+ elif answer == 'no':
357
+ print('Emptying Ricgraph: Ricgraph will not be emptied.\n')
358
+ return
359
+ else:
360
+ print('Do you want to empty Ricgraph? You have the following options:')
361
+ print('- "yes": Ricgraph will be emptied.')
362
+ print('- "no": Ricgraph will not be emptied.')
363
+ print('- any other answer: Ricgraph will not be emptied, execution of this script will abort.')
364
+ user_input = input('Please make a choice: ')
365
+ if user_input.lower() == 'yes':
366
+ print('\nRicgraph will be emptied.\n')
367
+ # Fall through.
368
+ elif user_input.lower() == 'no':
369
+ print('\nRicgraph has not been emptied.\n')
370
+ return
371
+ else:
372
+ print('\nRicgraph has not been emptied, exiting.\n')
373
+ exit(1)
374
+
375
+ graphdb_name = ricgraph_database()
376
+ graphdb_databasename = ricgraph_databasename()
377
+
378
+ # Delete the database and start over again.
379
+ # Sometimes the following statement fails with Neo4j Desktop graph database backend
380
+ # if there are many nodes, see e.g.
381
+ # https://stackoverflow.com/questions/23310114/how-to-reset-clear-delete-neo4j-database.
382
+ # Apparently, the community edition of Neo4j does not have a
383
+ # "CREATE OR REPLACE DATABASE customers" command.
384
+ print('Deleting all nodes and edges in Ricgraph...\n')
385
+ _graph.execute_query('MATCH (node) DETACH DELETE node', database_=graphdb_databasename)
386
+
387
+ if graphdb_name == 'neo4j':
388
+ # Do not use TEXT indexes, use RANGE indexes, these are much faster during harvesting
389
+ # (but they do not support a CONTAINS substring search which is being used for a broad search
390
+ # in read_all_nodes()). The RANGE index is the default in Neo4j. More info:
391
+ # https://neo4j.com/docs/cypher-manual/current/indexes/search-performance-indexes/managing-indexes
392
+ # [Jan. 2023] The graph database backend Neo4j can have at most 3 indexes.
393
+ # [June 5, 2024] This is not true anymore. I use 4 indexes.
394
+ _graph.execute_query('DROP INDEX KeyIndex IF EXISTS', database_=graphdb_databasename)
395
+ _graph.execute_query('DROP INDEX NameIndex IF EXISTS', database_=graphdb_databasename)
396
+ _graph.execute_query('DROP INDEX CategoryIndex IF EXISTS', database_=graphdb_databasename)
397
+ _graph.execute_query('DROP INDEX ValueIndex IF EXISTS', database_=graphdb_databasename)
398
+
399
+ print('Creating indexes...')
400
+ _graph.execute_query('CREATE INDEX KeyIndex IF NOT EXISTS FOR (node:RicgraphNode) ON (node._key)',
401
+ database_=graphdb_databasename)
402
+ _graph.execute_query('CREATE INDEX NameIndex IF NOT EXISTS FOR (node:RicgraphNode) ON (node.name)',
403
+ database_=graphdb_databasename)
404
+ _graph.execute_query('CREATE INDEX CategoryIndex IF NOT EXISTS FOR (node:RicgraphNode) ON (node.category)',
405
+ database_=graphdb_databasename)
406
+ _graph.execute_query('CREATE INDEX ValueIndex IF NOT EXISTS FOR (node:RicgraphNode) ON (node.value)',
407
+ database_=graphdb_databasename)
408
+
409
+ print('These indexes have been created:')
410
+ records, summary, keys = _graph.execute_query('SHOW INDEXES',
411
+ database_=graphdb_databasename)
412
+ index_table = pandas.DataFrame(data=records, columns=keys)
413
+ print(index_table.to_string(index=False))
414
+ print('')
415
+ elif graphdb_name == 'memgraph':
416
+ # For Memgraph, we need to create indexes in the following way, otherwise we get an
417
+ # 'Index manipulation not allowed in multicommand transactions'
418
+ # error. We should use implicit (or auto-commit) transactions.
419
+ # See https://memgraph.com/docs/client-libraries/python#transaction-management.
420
+ with _graph.session(database=GRAPHDB) as session:
421
+ # These also work in case they do not exist.
422
+ session.run('DROP INDEX ON :RicgraphNode;')
423
+ session.run('DROP INDEX ON :RicgraphNode(_key);')
424
+ session.run('DROP INDEX ON :RicgraphNode(name);')
425
+ session.run('DROP INDEX ON :RicgraphNode(category);')
426
+ session.run('DROP INDEX ON :RicgraphNode(value);')
427
+
428
+ print('Creating indexes...')
429
+ session.run('CREATE INDEX ON :RicgraphNode;')
430
+ session.run('CREATE INDEX ON :RicgraphNode(_key);')
431
+ session.run('CREATE INDEX ON :RicgraphNode(name);')
432
+ session.run('CREATE INDEX ON :RicgraphNode(category);')
433
+ session.run('CREATE INDEX ON :RicgraphNode(value);')
434
+
435
+ print('These indexes have been created:')
436
+ for index_line in session.run('SHOW INDEX INFO;'):
437
+ print(index_line)
438
+ print('')
439
+
440
+ # session.close() is done automatically because of 'with'.
441
+ else:
442
+ print('empty_ricgraph(): Unknown graph database backend "' + graphdb_name + '".')
443
+ print('Exiting.')
444
+ exit(1)
445
+ return
446
+
447
+
448
+ def ricgraph_nr_nodes() -> int:
449
+ """Count the number of nodes in Ricgraph.
450
+
451
+ :return: the number of nodes, or -1 on error.
452
+ """
453
+ global _graph
454
+
455
+ if _graph is None:
456
+ print('\nricgraph_nr_nodes(): Error: graph has not been initialized or opened.\n\n')
457
+ return -1
458
+
459
+ cypher_query = 'MATCH () RETURN count(*) AS count'
460
+ result = _graph.execute_query(cypher_query,
461
+ result_transformer_=Result.data,
462
+ database_=ricgraph_databasename())
463
+ if len(result) == 0:
464
+ return -1
465
+ result = result[0]
466
+ if 'count' not in result:
467
+ return -1
468
+ result = int(result['count'])
469
+ return result
470
+
471
+
472
+ def ricgraph_nr_edges() -> int:
473
+ """Count the number of edges in Ricgraph.
474
+
475
+ :return: the number of edges, or -1 on error.
476
+ """
477
+ global _graph
478
+
479
+ if _graph is None:
480
+ print('\nricgraph_nr_edges(): Error: graph has not been initialized or opened.\n\n')
481
+ return -1
482
+
483
+ cypher_query = 'MATCH ()-[r]->() RETURN count(r) AS count'
484
+ result = _graph.execute_query(cypher_query,
485
+ result_transformer_=Result.data,
486
+ database_=ricgraph_databasename())
487
+ if len(result) == 0:
488
+ return -1
489
+ result = result[0]
490
+ if 'count' not in result:
491
+ return -1
492
+ result = int(result['count'])
493
+ return result
494
+
495
+
496
+ def ricgraph_nr_edges_of_node(node_element_id: str) -> int:
497
+ """Count the number of edges in Ricgraph of a given node.
498
+
499
+ :param node_element_id: the element_id of the node.
500
+ :return: the number of nodes, or -1 on error.
501
+ """
502
+ global _graph
503
+
504
+ if _graph is None:
505
+ print('\nricgraph_nr_edges_of_node(): Error: graph has not been initialized or opened.\n\n')
506
+ return -1
507
+
508
+ cypher_query = 'MATCH (node:RicgraphNode)-[r]->() '
509
+ if ricgraph_database() == 'neo4j':
510
+ cypher_query += 'WHERE elementId(node)=$node_element_id '
511
+ else:
512
+ cypher_query += 'WHERE id(node)=toInteger($node_element_id) '
513
+ cypher_query += 'RETURN count(r) AS count'
514
+
515
+ result = _graph.execute_query(cypher_query,
516
+ node_element_id=node_element_id,
517
+ result_transformer_=Result.data,
518
+ database_=ricgraph_databasename())
519
+ if len(result) == 0:
520
+ return -1
521
+ result = result[0]
522
+ if 'count' not in result:
523
+ return -1
524
+ result = int(result['count'])
525
+ return result
526
+
527
+
528
+ def cypher_create_node(node_properties: dict) -> Union[Node, None]:
529
+ """
530
+ Create a node in the graph database.
531
+ Create means: the node will be created, regardless if it is already
532
+ present or not. No test for presence of a node is done.
533
+ The properties and their values in 'node_properties' will be added
534
+ as properties & values of the node.
535
+ The 'name', 'category', and 'value' properties are expected to be
536
+ in 'node_properties'.
537
+
538
+ :param node_properties: the properties of the node.
539
+ :return: the node updated, or None on error.
540
+ """
541
+ global graphdb_nr_creates
542
+
543
+ # There are several methods for creating a node in the graph database.
544
+ # This would be an alternative, but it seems to use more memory than the
545
+ # one used now, according to 'PROFILE <cypher query>'.
546
+ # cypher_query = 'CREATE (node:RicgraphNode) SET node=$node_properties ' [etc.]
547
+ cypher_query = 'CREATE (node:RicgraphNode $node_properties) RETURN node'
548
+
549
+ # print('cypher_create_node(): cypher_query: ' + cypher_query)
550
+ # print(' node_properties: ' + str(node_properties))
551
+
552
+ nodes = _graph.execute_query(cypher_query,
553
+ node_properties=node_properties,
554
+ result_transformer_=Result.value,
555
+ database_=ricgraph_databasename())
556
+ graphdb_nr_creates += 1
557
+ if len(nodes) == 0:
558
+ return None
559
+ else:
560
+ return nodes[0]
561
+
562
+
563
+ def cypher_update_node_properties(node_element_id: str, node_properties: dict) -> Union[Node, None]:
564
+ """
565
+ Update node properties in a node in the graph database.
566
+ If the node is not present, nothing will happen.
567
+ Only the properties and their values in 'node_properties' will be changed (if
568
+ a property is present in the node) or added (if the property is not present).
569
+ All other properties (i.e. not specified properties in 'node_properties')
570
+ will be left as they are.
571
+
572
+ :param node_element_id: the element_id of the node.
573
+ :param node_properties: the properties of the node.
574
+ :return: the node updated, or None on error.
575
+ """
576
+ global graphdb_nr_reads, graphdb_nr_updates
577
+
578
+ cypher_query = 'MATCH (node:RicgraphNode) '
579
+ if ricgraph_database() == 'neo4j':
580
+ cypher_query += 'WHERE elementId(node)=$node_element_id '
581
+ else:
582
+ cypher_query += 'WHERE id(node)=toInteger($node_element_id) '
583
+ cypher_query += 'SET node+=$node_properties RETURN node'
584
+
585
+ # print('cypher_update_node_properties(): node_element_id: ' + str(node_element_id) +
586
+ # ', cypher_query: ' + cypher_query)
587
+ # print(' node_properties: ' + str(node_properties))
588
+
589
+ nodes = _graph.execute_query(cypher_query,
590
+ node_element_id=node_element_id,
591
+ node_properties=node_properties,
592
+ result_transformer_=Result.value,
593
+ database_=ricgraph_databasename())
594
+ graphdb_nr_reads += 1
595
+ graphdb_nr_updates += 1
596
+ if len(nodes) == 0:
597
+ return None
598
+ else:
599
+ return nodes[0]
600
+
601
+
602
+ def cypher_merge_nodes(node_merge_from_element_id: str,
603
+ node_merge_to_element_id: str,
604
+ node_merge_to_properties: dict) -> Union[Node, None]:
605
+ """Merge two nodes in the graph database.
606
+ The neighbors of 'node_merge_from' will be merged to node 'node_merge_to'
607
+ and 'node_merge_from' will be deleted.
608
+ The node properties in 'node_merge_to' will be set according to
609
+ 'node_merge_to_properties'.
610
+
611
+ Only the properties and their values in 'node_properties' will be changed (if
612
+ a property is present in the node) or added (if the property is not present).
613
+ All other properties (i.e. not specified properties in 'node_properties')
614
+ will be left as they are.
615
+
616
+ :param node_merge_from_element_id: the element_id of node_merge_from.
617
+ :param node_merge_to_element_id: the element_id of node_merge_to.
618
+ :param node_merge_to_properties: the properties of node_merge_to.
619
+ :return: the node updated, or None on error.
620
+ """
621
+ global graphdb_nr_reads, graphdb_nr_updates
622
+
623
+ graphdb_name = ricgraph_database()
624
+
625
+ if node_merge_from_element_id == node_merge_to_element_id:
626
+ # We only update node properties.
627
+ node = cypher_update_node_properties(node_element_id=node_merge_to_element_id,
628
+ node_properties=node_merge_to_properties)
629
+ return node
630
+
631
+ cypher_query = 'MATCH (node_from:RicgraphNode) '
632
+ if graphdb_name == 'neo4j':
633
+ cypher_query += 'WHERE elementId(node_from)=$node_merge_from_element_id '
634
+ else:
635
+ cypher_query += 'WHERE id(node_from)=toInteger($node_merge_from_element_id) '
636
+ cypher_query += 'MATCH (node_to:RicgraphNode) '
637
+ if graphdb_name == 'neo4j':
638
+ cypher_query += 'WHERE elementId(node_to)=$node_merge_to_element_id '
639
+ else:
640
+ cypher_query += 'WHERE id(node_to)=toInteger($node_merge_to_element_id) '
641
+
642
+ cypher_query += 'SET node_to+=$node_merge_to_properties '
643
+ cypher_query += 'WITH node_from, node_to '
644
+ # Only test for an edge in one direction, that is sufficient.
645
+ cypher_query += 'MATCH (node_from)-[:LINKS_TO]->(neighbor_node) '
646
+ # Prevent creating a self-relationship on node_to.
647
+ cypher_query += 'WHERE node_to <> neighbor_node '
648
+ cypher_query += 'MERGE (node_to)-[:LINKS_TO]->(neighbor_node) '
649
+ cypher_query += 'MERGE (node_to)<-[:LINKS_TO]-(neighbor_node) '
650
+ cypher_query += 'DETACH DELETE node_from '
651
+ cypher_query += 'RETURN node_to'
652
+
653
+ # print('cypher_merge_nodes(): node_merge_from_element_id: ' + str(node_merge_from_element_id))
654
+ # print(' node_merge_to_element_id: ' + str(node_merge_to_element_id))
655
+ # print(' node_merge_to_properties: ' + str(node_merge_to_properties))
656
+ # print(' cypher_query: ' + cypher_query)
657
+
658
+ nodes = _graph.execute_query(cypher_query,
659
+ node_merge_from_element_id=node_merge_from_element_id,
660
+ node_merge_to_element_id=node_merge_to_element_id,
661
+ node_merge_to_properties=node_merge_to_properties,
662
+ result_transformer_=Result.value,
663
+ database_=ricgraph_databasename())
664
+
665
+ # If merge_node_from does not have neighbors, it will not be deleted.
666
+ # Do another Cypher query to be sure it is gone.
667
+ cypher_query = 'MATCH (node_from:RicgraphNode) '
668
+ if graphdb_name == 'neo4j':
669
+ cypher_query += 'WHERE elementId(node_from)=$node_merge_from_element_id '
670
+ else:
671
+ cypher_query += 'WHERE id(node_from)=toInteger($node_merge_from_element_id) '
672
+ cypher_query += 'DETACH DELETE node_from'
673
+ _graph.execute_query(cypher_query,
674
+ node_merge_from_element_id=node_merge_from_element_id,
675
+ result_transformer_=Result.value,
676
+ database_=ricgraph_databasename())
677
+
678
+ nr_edges = ricgraph_nr_edges_of_node(node_element_id=node_merge_to_element_id)
679
+ graphdb_nr_reads += 2 * nr_edges # Approximation: edges are in two directions.
680
+ graphdb_nr_updates += 2 * nr_edges # Approximation: edges are in two directions.
681
+ if len(nodes) == 0:
682
+ return None
683
+ else:
684
+ return nodes[0]
685
+
686
+
687
+ def cypher_create_edge_if_not_exists(left_node_element_id: str, right_node_element_id: str) -> None:
688
+ """Create an edge between two nodes, but only if the edge does not exist.
689
+
690
+ :param left_node_element_id: the element_id of the left node.
691
+ :param right_node_element_id: the element_id of the right node.
692
+ :return: None.
693
+ """
694
+ global graphdb_nr_reads, graphdb_nr_updates
695
+
696
+ graphdb_name = ricgraph_database()
697
+
698
+ # There are several methods for getting the nodes in the graph database.
699
+ # This one takes the most time.
700
+ # It amounts to ?? database hits according to 'PROFILE <cypher query>', using 'AllNodesScan'.
701
+ # cypher_query = 'MATCH (left_node {ID:' + str(left_node.element_id) + '}) ' [etc.]
702
+ # The next one takes average time.
703
+ # It amounts to 23 database hits according to 'PROFILE', using 'AllNodesScan'.
704
+ # cypher_query = 'MATCH (left_node) WHERE left_node._key="' + str(left_node['_key']) + '" ' [etc.]
705
+ # The next one is the fastest.
706
+ # It amounts to 1 database hits according to 'PROFILE' using 'NodeByIdSeek'.
707
+ cypher_query = 'MATCH (left_node:RicgraphNode) '
708
+ if graphdb_name == 'neo4j':
709
+ cypher_query += 'WHERE elementId(left_node)=$left_node_element_id '
710
+ else:
711
+ cypher_query += 'WHERE id(left_node)=toInteger($left_node_element_id) '
712
+ cypher_query += 'MATCH (right_node:RicgraphNode) '
713
+ if graphdb_name == 'neo4j':
714
+ cypher_query += 'WHERE elementId(right_node)=$right_node_element_id '
715
+ else:
716
+ cypher_query += 'WHERE id(right_node)=toInteger($right_node_element_id) '
717
+
718
+ # We could use 'CREATE', but then we may get multiple edges for the same direction.
719
+ # cypher_query += 'CREATE (left_node)-[:LINKS_TO]->(right_node), ' [etc.]
720
+ # A MERGE is more expensive since it first checks for presence of the edge.
721
+ cypher_query += 'MERGE (left_node)-[:LINKS_TO]->(right_node) '
722
+ cypher_query += 'MERGE (left_node)<-[:LINKS_TO]-(right_node) '
723
+
724
+ # print('cypher_create_edge_if_not_exists(): left_node_id: ' + str(left_node_id) + ', right_node_id: '
725
+ # + str(right_node_id) + ', cypher_query: ' + cypher_query)
726
+
727
+ _graph.execute_query(cypher_query,
728
+ left_node_element_id=left_node_element_id,
729
+ right_node_element_id=right_node_element_id,
730
+ database_=ricgraph_databasename())
731
+ graphdb_nr_reads += 2 # one for left_node, one for right_node
732
+ graphdb_nr_updates += 2 # one for ->, one for <-.
733
+ return
734
+
735
+
736
+ # ##############################################################################
737
+ # General Ricgraph functions.
738
+ # ##############################################################################
739
+ def get_ricgraph_ini_file() -> str:
740
+ """Get the location of the ricgraph ini file.
741
+
742
+ :return: the location of the ini file.
743
+ """
744
+ # Try to find RICGRAPH_INI_FILENAME in the root of the virtual environment.
745
+ ricgraph_ini_path = sys.prefix
746
+ ricgraph_ini = os.path.join(ricgraph_ini_path, RICGRAPH_INI_FILENAME)
747
+ if os.path.exists(ricgraph_ini):
748
+ return ricgraph_ini
749
+
750
+ # Try to find RICGRAPH_INI_FILENAME in the parent directory of the venv,
751
+ # which may happen when using a Python IDE.
752
+ ricgraph_ini_path_parent = os.path.dirname(ricgraph_ini_path)
753
+ ricgraph_ini = os.path.join(ricgraph_ini_path_parent, RICGRAPH_INI_FILENAME)
754
+ if os.path.exists(ricgraph_ini):
755
+ return ricgraph_ini
756
+
757
+ print('Ricgraph initialization: error, Ricgraph ini file "' + RICGRAPH_INI_FILENAME + '" not found in')
758
+ print(' directory "' + ricgraph_ini_path + '", nor in')
759
+ print(' directory "' + ricgraph_ini_path_parent + '", exiting.')
760
+ exit(1)
761
+
762
+
763
+ def timestamp(seconds: bool = False) -> str:
764
+ """Get a timestamp only consisting of a time.
765
+
766
+ :param seconds: If True, also show seconds in the timestamp.
767
+ :return: the timestamp.
768
+ """
769
+ now = datetime.now()
770
+ if seconds:
771
+ time_stamp = now.strftime("%H:%M:%S")
772
+ else:
773
+ time_stamp = now.strftime("%H:%M")
774
+ return time_stamp
775
+
776
+
777
+ def datetimestamp(seconds: bool = False) -> str:
778
+ """Get a timestamp consisting of a date and a time.
779
+
780
+ :param seconds: If True, also show seconds in the timestamp.
781
+ :return: the timestamp.
782
+ """
783
+ now = datetime.now()
784
+ if seconds:
785
+ datetime_stamp = now.strftime("%Y-%m-%d %H:%M:%S")
786
+ else:
787
+ datetime_stamp = now.strftime("%Y-%m-%d %H:%M")
788
+ return datetime_stamp
789
+
790
+
791
+ def graphdb_nr_accesses_reset() -> None:
792
+ """Reset the counters that are used to count the number of accesses
793
+ to the graph database backend.
794
+
795
+ :return: None.
796
+ """
797
+ global graphdb_nr_creates, graphdb_nr_reads
798
+ global graphdb_nr_updates, graphdb_nr_deletes
799
+
800
+ graphdb_nr_creates = 0
801
+ graphdb_nr_reads = 0
802
+ graphdb_nr_updates = 0
803
+ graphdb_nr_deletes = 0
804
+ return
805
+
806
+
807
+ def graphdb_nr_accesses_print() -> None:
808
+ """Print the values of the counters for the number of accesses to
809
+ the graph database backend.
810
+
811
+ :return: None.
812
+ """
813
+ # These counters are used to count the number of accesses to the graph database.
814
+ global graphdb_nr_creates, graphdb_nr_reads, graphdb_nr_updates, graphdb_nr_deletes
815
+
816
+ graphdb_total_accesses = graphdb_nr_creates + graphdb_nr_reads
817
+ graphdb_total_accesses += (2 * graphdb_nr_updates) + graphdb_nr_deletes
818
+ nr_nodes = ricgraph_nr_nodes()
819
+ nr_edges = ricgraph_nr_edges()
820
+
821
+ print('\n')
822
+ print('These are the number of accesses to the graph database backend at ' + datetimestamp() + ':')
823
+ print('- total number of creates: {:>10}'.format(graphdb_nr_creates))
824
+ print('- total number of reads: {:>10}'.format(graphdb_nr_reads))
825
+ print('- total number of updates: {:>10}'.format(graphdb_nr_updates))
826
+ print('- total number of deletes: {:>10}'.format(graphdb_nr_deletes))
827
+ print('- total number of accesses: {:>10}'.format(graphdb_total_accesses))
828
+ print('Please note that for the total number of accesses, an "update" counts twice: '
829
+ + 'one for the read and one for the update of the value.')
830
+ print('\n')
831
+ print('These are the number of nodes and edges in Ricgraph:')
832
+ print('- total number of nodes: {:>8}'.format(nr_nodes))
833
+ print('- total number of edges: {:>8}'.format(nr_edges))
834
+ print('\n')
835
+ return
836
+
837
+
838
+ def create_ricgraph_key(name: str, value: str) -> str:
839
+ """Create a key for a node.
840
+ This function generates a composite key for a node in a graph.
841
+
842
+ :param name: name of the node.
843
+ :param value: value of the node.
844
+ :return: key generated for the node.
845
+ """
846
+ # Check and correct for occurrences of RICGRAPH_KEY_SEPARATOR.
847
+ name = name.replace(RICGRAPH_KEY_SEPARATOR, '#')
848
+ value = value.replace(RICGRAPH_KEY_SEPARATOR, '#')
849
+
850
+ return value.lower() + RICGRAPH_KEY_SEPARATOR + name.lower()
851
+
852
+
853
+ def get_namepart_from_ricgraph_key(key: str) -> str:
854
+ """Get the 'name' part from the composite key in a node.
855
+
856
+ :param key: key of the node.
857
+ :return: name part of key for the node.
858
+ """
859
+ key_list = key.split(sep=RICGRAPH_KEY_SEPARATOR)
860
+ if len(key_list) != 2:
861
+ return ''
862
+ return key_list[1]
863
+
864
+
865
+ def get_valuepart_from_ricgraph_key(key: str) -> str:
866
+ """Get the 'value' part from the composite key in a node.
867
+
868
+ :param key: key of the node.
869
+ :return: value part of key for the node.
870
+ """
871
+ key_list = key.split(sep=RICGRAPH_KEY_SEPARATOR)
872
+ if len(key_list) != 2:
873
+ return ''
874
+ return key_list[0]
875
+
876
+
877
+ def create_well_known_url(name: str, value: str) -> str:
878
+ """Create a URL to refer to the source of a 'well known' identifier (i.e. ORCID, ISNI, etc.).
879
+
880
+ :param name: an identifier name, e.g. ORCID, ISNI, etc.
881
+ :param value: the value.
882
+ :return: URL.
883
+ """
884
+ if name == '' or value == '':
885
+ return ''
886
+
887
+ if name == 'DOI':
888
+ return 'https://doi.org/' + value
889
+ elif name == 'GITHUB':
890
+ return 'https://www.github.com/' + value
891
+ elif name == 'ISNI':
892
+ return 'https://isni.org/isni/' + value
893
+ elif name == 'LINKEDIN':
894
+ return 'https://www.linkedin.com/in/' + value
895
+ # elif name == 'OPENALEX':
896
+ # return 'https://openalex.org/' + value
897
+ elif name == 'ORCID':
898
+ return 'https://orcid.org/' + value
899
+ elif name == 'RESEARCHER_ID':
900
+ return 'https://www.webofscience.com/wos/author/record/' + value
901
+ elif name == 'ROR':
902
+ return 'https://ror.org/' + value
903
+ elif name == 'SCOPUS_AUTHOR_ID':
904
+ return 'https://www.scopus.com/authid/detail.uri?authorId=' + value
905
+ elif name == 'TWITTER':
906
+ return 'https://www.twitter.com/' + value
907
+ else:
908
+ return ''
909
+
910
+
911
+ def create_history_line(property_name: str, old_value: str, new_value: str) -> str:
912
+ """Create a line to be used in the 'history' property of a node.
913
+ """
914
+ return 'Updated property "' + property_name + '" from "' + old_value + '" to "' + new_value + '". '
915
+
916
+
917
+ def create_name_cache_in_personroot(node: Node, personroot: Node):
918
+ """This function caches the value of a node with 'name' 'FULL_NAME'
919
+ in the 'comment' property of its person-root node 'personroot'.
920
+ If the cache is not present, it is created.
921
+
922
+ :param node: node to add FULL_NAME to cache.
923
+ :param personroot: personroot node of 'node'. For efficiency, we do not check
924
+ if this really is the case.
925
+ :return: No return value.
926
+ """
927
+ if node is None:
928
+ return
929
+ if personroot is None:
930
+ return
931
+ if node['name'] == 'FULL_NAME':
932
+ time_stamp = datetimestamp()
933
+ if isinstance(personroot['comment'], str):
934
+ if personroot['comment'] == '':
935
+ old_value = str(personroot['comment']) # Not necessary, for symmetry.
936
+ node_properties = {'comment': [node['value']]}
937
+ history_line = create_history_line(property_name='comment',
938
+ old_value=old_value,
939
+ new_value=str(node_properties['comment']))
940
+ node_properties['_history'] = personroot['_history'].copy()
941
+ node_properties['_history'].append(time_stamp + ': ' + history_line)
942
+ cypher_update_node_properties(node_element_id=personroot.element_id,
943
+ node_properties=node_properties)
944
+ elif isinstance(personroot['comment'], list):
945
+ if node['value'] not in personroot['comment']:
946
+ old_value = str(personroot['comment'])
947
+ node_properties = {'comment': personroot['comment'].copy()}
948
+ node_properties['comment'].append(node['value'])
949
+ history_line = create_history_line(property_name='comment',
950
+ old_value=old_value,
951
+ new_value=str(node_properties['comment']))
952
+ node_properties['_history'] = personroot['_history'].copy()
953
+ node_properties['_history'].append(time_stamp + ': ' + history_line)
954
+ cypher_update_node_properties(node_element_id=personroot.element_id,
955
+ node_properties=node_properties)
956
+ # In all other cases: it is already in the cache,
957
+ # or leave it as it is: 'comment' seems to be used for something we don't know.
958
+ return
959
+
960
+
961
+ def recreate_name_cache_in_personroot(personroot: Node):
962
+ """This function recreates the cache of all the nodes with 'name' 'FULL_NAME'
963
+ in the 'comment' property of its person-root node 'personroot'.
964
+ If the cache is not present, it is created.
965
+
966
+ :param personroot: personroot node of 'node'. For efficiency, we do not check
967
+ if this really is the case.
968
+ :return: No return value.
969
+ """
970
+ if personroot is None:
971
+ return
972
+ neighbornodes = get_all_neighbor_nodes(node=personroot)
973
+ name_cache = []
974
+ for node in neighbornodes:
975
+ if node['name'] == 'FULL_NAME':
976
+ if node['value'] not in name_cache:
977
+ name_cache.append(node['value'])
978
+
979
+ if len(name_cache) != 0:
980
+ if (isinstance(personroot['comment'], str) and personroot['comment'] == '') \
981
+ or isinstance(personroot['comment'], list):
982
+ time_stamp = datetimestamp()
983
+ history_line = create_history_line(property_name='comment',
984
+ old_value=str(personroot['comment']),
985
+ new_value=str(name_cache))
986
+ node_properties = {'comment': name_cache.copy(),
987
+ '_history': personroot['_history'].copy()}
988
+ node_properties['_history'].append(time_stamp + ': Cleaned and recreated name cache. ' + history_line)
989
+ cypher_update_node_properties(node_element_id=personroot.element_id,
990
+ node_properties=node_properties)
991
+ # In all other cases: leave it as it is: 'comment' seems to be used for something we don't know.
992
+ return
993
+
994
+
995
+ def create_update_node(name: str, category: str, value: str,
996
+ other_properties: dict = None) -> Union[Node, None]:
997
+ """Create a node, or update its values if is already present.
998
+ The new/updated node will have values 'name', 'category', and 'value', and
999
+ the values in other_properties.
1000
+ For now all properties will have a string value, except for '_source'
1001
+ (a sorted list), and '_history' (a list).
1002
+ Changing the 'name' or 'value' properties (and, subsequently, the '_key' value),
1003
+ cannot be done with this function, since in that case we would not be able
1004
+ to find it.
1005
+
1006
+ :param name: 'name' property of node to create or update.
1007
+ :param category: 'category' property of node.
1008
+ :param value: 'value' property of node.
1009
+ :param other_properties: a dictionary of all the other properties.
1010
+ :return: the node created, or None if this was not possible
1011
+ """
1012
+ if other_properties is None:
1013
+ other_properties = {}
1014
+ global _graph
1015
+
1016
+ if _graph is None:
1017
+ print('\ncreate_update_node(): Error: graph has not been initialized or opened.\n\n')
1018
+ return None
1019
+
1020
+ if not isinstance(name, str) \
1021
+ or not isinstance(category, str) \
1022
+ or not isinstance(value, str):
1023
+ return None
1024
+
1025
+ if other_properties is None:
1026
+ other_properties = {}
1027
+
1028
+ lname = str(name)
1029
+ lcategory = str(category)
1030
+ lvalue = str(value)
1031
+
1032
+ if lname == '' or lcategory == '' or lvalue == '':
1033
+ return None
1034
+
1035
+ node_properties = {}
1036
+ time_stamp = datetimestamp()
1037
+ node = read_node(name=lname, value=lvalue)
1038
+ if node is None:
1039
+ # Create a node.
1040
+ # First do the properties in RICGRAPH_PROPERTIES_STANDARD.
1041
+ node_properties['name'] = lname
1042
+ node_properties['category'] = lcategory
1043
+ node_properties['value'] = lvalue
1044
+
1045
+ # Then do the properties in RICGRAPH_PROPERTIES_ADDITIONAL.
1046
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
1047
+ if prop_name in other_properties:
1048
+ node_properties[prop_name] = str(other_properties[prop_name])
1049
+ else:
1050
+ node_properties[prop_name] = ''
1051
+
1052
+ # If no url_main has been passed, insert an ISNI, DOI, etc. url if category is ISNI, DOI, etc.
1053
+ url = create_well_known_url(name=lname, value=lvalue)
1054
+ if node_properties['url_main'] == '' and url != '':
1055
+ node_properties['url_main'] = url
1056
+
1057
+ # Then do the properties in RICGRAPH_PROPERTIES_HIDDEN.
1058
+ node_properties['_key'] = create_ricgraph_key(name=lname, value=lvalue)
1059
+ if node_properties['source_event'] == '':
1060
+ node_properties['_source'] = []
1061
+ else:
1062
+ node_properties['_source'] = [node_properties['source_event']]
1063
+ node_properties['source_event'] = ''
1064
+
1065
+ if node_properties['history_event'] == '':
1066
+ node_properties['_history'] = [time_stamp + ': Created. ']
1067
+ else:
1068
+ node_properties['_history'] = [time_stamp + ': Created. ' + node_properties['history_event']]
1069
+ node_properties['history_event'] = ''
1070
+
1071
+ new_node = cypher_create_node(node_properties=node_properties)
1072
+ return new_node
1073
+
1074
+ if RICGRAPH_NODEADD_MODE == 'strict' and node['name'] == 'FULL_NAME':
1075
+ # We only get here if we want to connect some other node A to this FULL_NAME node B.
1076
+ # This FULL_NAME node B already exists. Most probably it is connected to a person-root C
1077
+ # and that person-root C to some other nodes D and E.
1078
+ # If we would continue, A would be connected to B, and also to C, D and E.
1079
+ # However, connecting based on a name is not a very good idea, since two
1080
+ # different persons may have the same name.
1081
+ # So don't do this if we are in NODEADD_MODE 'strict'.
1082
+ return None
1083
+
1084
+ # Update a node.
1085
+ history_line = ''
1086
+
1087
+ # First do the properties in RICGRAPH_PROPERTIES_STANDARD.
1088
+ # It is not possible to update 'name' or 'value'.
1089
+ if node['category'] != lcategory:
1090
+ history_line += create_history_line(property_name='category',
1091
+ old_value=node['category'],
1092
+ new_value=lcategory)
1093
+ node_properties['category'] = lcategory
1094
+
1095
+ # Then do the properties in RICGRAPH_PROPERTIES_ADDITIONAL.
1096
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
1097
+ if prop_name not in other_properties:
1098
+ continue
1099
+ if prop_name == 'history_event' or prop_name == 'source_event':
1100
+ # Do not add these to node_properties, and don't add them to _history.
1101
+ continue
1102
+ # Only in case a property is in other_properties, its value may change.
1103
+ present_val = str(node[prop_name])
1104
+ if present_val != str(other_properties[prop_name]):
1105
+ # But its value is only changed if it is different then the old value.
1106
+ node_properties[prop_name] = str(other_properties[prop_name])
1107
+ history_line += create_history_line(property_name=prop_name,
1108
+ old_value=present_val,
1109
+ new_value=str(other_properties[prop_name]))
1110
+
1111
+ # Finally do the properties in RICGRAPH_PROPERTIES_HIDDEN.
1112
+ # It is not possible to update '_key'.
1113
+ if 'source_event' in other_properties:
1114
+ if other_properties['source_event'] != '':
1115
+ if other_properties['source_event'] not in node['_source']:
1116
+ node_properties['_source'] = node['_source'].copy()
1117
+ node_properties['_source'].append(other_properties['source_event'])
1118
+ node_properties['_source'].sort()
1119
+ history_line += create_history_line(property_name='_source',
1120
+ old_value=str(node['_source']),
1121
+ new_value=str(node_properties['_source']))
1122
+
1123
+ if history_line == '':
1124
+ # No changes.
1125
+ return node
1126
+
1127
+ node_properties['_history'] = node['_history'].copy()
1128
+ node_properties['_history'].append(time_stamp + ': Updated. ' + history_line)
1129
+ updated_node = cypher_update_node_properties(node_element_id=node.element_id,
1130
+ node_properties=node_properties)
1131
+ return updated_node
1132
+
1133
+
1134
+ def read_node(name: str = '', value: str = '') -> Union[Node, None]:
1135
+ """Read a node based on name and value.
1136
+ Since all nodes are supposed to be unique if both are
1137
+ specified, return the first one found.
1138
+
1139
+ :param name: 'name' property of node.
1140
+ :param value: 'value' property of node.
1141
+ :return: the node read, or None if no node was found.
1142
+ """
1143
+ global _graph
1144
+ global nodes_cache_key_element_id
1145
+ global graphdb_nr_reads
1146
+
1147
+ if _graph is None:
1148
+ print('\nread_node(): Error: graph has not been initialized or opened.\n\n')
1149
+ return None
1150
+
1151
+ if not isinstance(name, str) or not isinstance(value, str):
1152
+ return None
1153
+
1154
+ if name == '' or value == '':
1155
+ return None
1156
+
1157
+ graphdb_nr_reads += 1
1158
+ key = create_ricgraph_key(name=name, value=value)
1159
+ if key in nodes_cache_key_element_id:
1160
+ node_element_id = nodes_cache_key_element_id[key]
1161
+ cypher_query = 'MATCH (node:RicgraphNode) WHERE '
1162
+ if ricgraph_database() == 'neo4j':
1163
+ cypher_query += 'elementId(node)=$node_element_id '
1164
+ else:
1165
+ cypher_query += 'id(node)=toInteger($node_element_id) '
1166
+ cypher_query += 'RETURN node'
1167
+ nodes = _graph.execute_query(cypher_query,
1168
+ node_element_id=node_element_id,
1169
+ result_transformer_=Result.value,
1170
+ database_=ricgraph_databasename())
1171
+ if len(nodes) == 0:
1172
+ return None
1173
+ else:
1174
+ return nodes[0]
1175
+ else:
1176
+ if len(nodes_cache_key_element_id) > MAX_NODES_CACHE_KEY_ID:
1177
+ # Empty cache.
1178
+ nodes_cache_key_element_id = {}
1179
+
1180
+ cypher_query = 'MATCH (node:RicgraphNode) WHERE (node.name=$node_name) AND (node.value=$node_value) '
1181
+ cypher_query += 'RETURN node'
1182
+ nodes = _graph.execute_query(cypher_query,
1183
+ node_name=name,
1184
+ node_value=value,
1185
+ result_transformer_=Result.value,
1186
+ database_=ricgraph_databasename())
1187
+ if len(nodes) == 0:
1188
+ return None
1189
+ else:
1190
+ node = nodes[0]
1191
+ nodes_cache_key_element_id[key] = node.element_id
1192
+ return node
1193
+
1194
+
1195
+ def read_all_nodes(name: str = '', category: str = '', value: str = '',
1196
+ key: str = '',
1197
+ name_is_exact_match: bool = True,
1198
+ value_is_exact_match: bool = True,
1199
+ max_nr_nodes: int = 0) -> list:
1200
+ """Read a number of nodes based on name, category or value.
1201
+ Any of these parameters can be specified.
1202
+ It is also possible to read a number of nodes based on key.
1203
+ But not name, category, value, and key at the same time.
1204
+ If key has been specified, the values of name_is_exact_match and
1205
+ value_is_exact_match are not used.
1206
+
1207
+ :param name: 'name' property of node.
1208
+ :param category: idem.
1209
+ :param value: idem.
1210
+ :param key: idem.
1211
+ :param name_is_exact_match: if True, then do an exact match search
1212
+ on field 'name', if False, then do a case-insensitive match.
1213
+ Note that a case-insensitive match is more expensive.
1214
+ :param value_is_exact_match: if True, then do an exact match search
1215
+ on field 'value', if False, then do a case-insensitive match.
1216
+ Note that a case-insensitive match is more expensive.
1217
+ :param max_nr_nodes: return at most this number of nodes, 0 = all nodes.
1218
+ :return: list of nodes read, or empty list if nothing found.
1219
+ """
1220
+ global _graph
1221
+ global graphdb_nr_reads
1222
+
1223
+ if _graph is None:
1224
+ print('\nread_all_nodes(): Error: graph has not been initialized or opened.\n\n')
1225
+ return []
1226
+
1227
+ if not isinstance(name, str) \
1228
+ or not isinstance(category, str) \
1229
+ or not isinstance(value, str) \
1230
+ or not isinstance(key, str):
1231
+ return []
1232
+
1233
+ # Special case, for speed.
1234
+ if value_is_exact_match and name != '' and category == '' and value != '':
1235
+ # Ignore 'max_nr_nodes'.
1236
+ cypher_query = 'MATCH (node:RicgraphNode) WHERE (node.name=$node_name) AND (node.value=$node_value) '
1237
+ cypher_query += 'RETURN node'
1238
+ nodes = _graph.execute_query(cypher_query,
1239
+ node_name=name,
1240
+ node_value=value,
1241
+ result_transformer_=Result.value,
1242
+ database_=ricgraph_databasename())
1243
+ nr_nodes = len(nodes)
1244
+ # Unsure what to count here, this seems reasonable. '+ 1' for first node.
1245
+ graphdb_nr_reads += nr_nodes + 1
1246
+ if nr_nodes == 0:
1247
+ return []
1248
+ else:
1249
+ return nodes
1250
+
1251
+ # Now all other cases.
1252
+ # Don't allow a search for everything.
1253
+ if name == '' and category == '' and value == '' and key == '':
1254
+ return []
1255
+
1256
+ if key != '' and name == '' and category == '' and value == '':
1257
+ # Ignore 'max_nr_nodes'.
1258
+ cypher_query = 'MATCH (node:RicgraphNode) WHERE (node._key=$node_key) '
1259
+ cypher_query += 'RETURN node'
1260
+ nodes = _graph.execute_query(cypher_query,
1261
+ node_key=key,
1262
+ result_transformer_=Result.value,
1263
+ database_=ricgraph_databasename())
1264
+ nr_nodes = len(nodes)
1265
+ # Unsure what to count here, this seems reasonable. '+ 1' for first node.
1266
+ graphdb_nr_reads += nr_nodes + 1
1267
+ if nr_nodes == 0:
1268
+ return []
1269
+ else:
1270
+ return nodes
1271
+
1272
+ cypher_query = 'MATCH (node:RicgraphNode) WHERE '
1273
+ if name != '':
1274
+ if name_is_exact_match:
1275
+ cypher_query += '(node.name="' + name + '") AND '
1276
+ else:
1277
+ cypher_query += '(toLower(node.name) CONTAINS toLower("' + name + '")) AND '
1278
+ if category != '':
1279
+ cypher_query += '(node.category="' + category + '") AND '
1280
+ if value != '':
1281
+ # Value may contain special characters.
1282
+ if value_is_exact_match:
1283
+ # Exact match search.
1284
+ cypher_query += '(node.value=$node_value) AND '
1285
+ else:
1286
+ # Case-insensitive search.
1287
+ cypher_query += '(toLower(node.value) CONTAINS toLower($node_value)) AND '
1288
+
1289
+ # Remove last 'AND ', is of length -4.
1290
+ cypher_query = cypher_query[:-4]
1291
+ cypher_query += 'RETURN node '
1292
+
1293
+ if max_nr_nodes > 0:
1294
+ cypher_query += 'LIMIT ' + str(max_nr_nodes)
1295
+ # print(cypher_query)
1296
+
1297
+ if value != '':
1298
+ # Value may contain special characters.
1299
+ nodes = _graph.execute_query(cypher_query,
1300
+ node_value=value,
1301
+ result_transformer_=Result.value,
1302
+ database_=ricgraph_databasename())
1303
+ else:
1304
+ nodes = _graph.execute_query(cypher_query,
1305
+ result_transformer_=Result.value,
1306
+ database_=ricgraph_databasename())
1307
+
1308
+ nr_nodes = len(nodes)
1309
+ # Unsure what to count here, this seems reasonable. '+ 1' for first node.
1310
+ graphdb_nr_reads += nr_nodes + 1
1311
+ if nr_nodes == 0:
1312
+ return []
1313
+ else:
1314
+ return nodes
1315
+
1316
+
1317
+ def read_all_values_of_property(node_property: str = '') -> list:
1318
+ """Read all the values of a certain property.
1319
+
1320
+ :param node_property: the property to find all values.
1321
+ :return: a sorted list with all the values, or empty list on error.
1322
+ """
1323
+ global _graph
1324
+
1325
+ if _graph is None:
1326
+ print('\nread_all_values_of_property(): Error: graph has not been initialized or opened.\n\n')
1327
+ return []
1328
+
1329
+ if node_property != 'name' \
1330
+ and node_property != 'category' \
1331
+ and node_property != '_source':
1332
+ print('\nread_all_values_of_property(): Error: function does not work for property "'
1333
+ + node_property + '".\n\n')
1334
+ return []
1335
+
1336
+ cypher_query = 'MATCH (node:RicgraphNode) RETURN COLLECT (DISTINCT node.' + node_property + ') AS entries'
1337
+ result = _graph.execute_query(cypher_query,
1338
+ result_transformer_=Result.data,
1339
+ database_=ricgraph_databasename())
1340
+ if len(result) == 0:
1341
+ return []
1342
+ result = result[0]
1343
+ if 'entries' not in result:
1344
+ return []
1345
+ result_list = result['entries']
1346
+ if len(result_list) == 0:
1347
+ return []
1348
+ if node_property == '_source':
1349
+ # This is a special case, we have a list of lists that we need to untangle.
1350
+ list_of_lists = result_list
1351
+ result_list = []
1352
+ for item in list_of_lists:
1353
+ if len(item) == 0:
1354
+ continue
1355
+ for encapsulated_item in item:
1356
+ if encapsulated_item not in result_list:
1357
+ result_list.append(encapsulated_item)
1358
+
1359
+ # We need 'key' to do a case insenstive search.
1360
+ result_list_sorted = sorted(result_list, key=str.lower)
1361
+ return result_list_sorted
1362
+
1363
+
1364
+ def update_node_value(name: str, old_value: str, new_value: str) -> Union[Node, None]:
1365
+ """Update a node, change the value property.
1366
+ This is a special case because we change the key.
1367
+ Use carefully, because other property that contain 'old_value' (such
1368
+ as 'url_main' or 'url_other') are not being updated, so they will
1369
+ point to the wrong URL.
1370
+
1371
+ :param name: 'name' property of node.
1372
+ :param old_value: old 'value' property of node.
1373
+ :param new_value: old 'value' property of node.
1374
+ :return: the node updated, or None if this was not possible.
1375
+ """
1376
+ # A lot of the code below is copied from create_update_node().
1377
+ global _graph
1378
+
1379
+ if _graph is None:
1380
+ print('\nupdate_node_value(): Error: graph has not been initialized or opened.\n\n')
1381
+ return None
1382
+
1383
+ if not isinstance(name, str) \
1384
+ or not isinstance(old_value, str) \
1385
+ or not isinstance(new_value, str):
1386
+ return None
1387
+
1388
+ lname = str(name)
1389
+ loldvalue = str(old_value)
1390
+ lnewvalue = str(new_value)
1391
+
1392
+ if lname == '' or loldvalue == '' or lnewvalue == '':
1393
+ return None
1394
+
1395
+ time_stamp = datetimestamp()
1396
+ node = read_node(name=lname, value=loldvalue)
1397
+ if node is None:
1398
+ # Node we want to change does not exist.
1399
+ return None
1400
+
1401
+ newnode = read_node(name=lname, value=lnewvalue)
1402
+ if newnode is not None:
1403
+ # The node we want to change to does already exist.
1404
+ # So this happens to be a merge of two nodes.
1405
+ merged_node = merge_two_nodes(node_merge_from=node,
1406
+ node_merge_to=newnode)
1407
+ return merged_node
1408
+
1409
+ node_properties = {'value': lnewvalue}
1410
+ oldkey = node['_key']
1411
+ newkey = create_ricgraph_key(name=lname, value=lnewvalue)
1412
+ node_properties['_key'] = newkey
1413
+ history_line = create_history_line(property_name='value', old_value=loldvalue, new_value=lnewvalue)
1414
+ history_line += create_history_line(property_name='_key', old_value=oldkey, new_value=newkey)
1415
+ node_properties['_history'] = node['_history'].copy()
1416
+ node_properties['_history'].append(time_stamp + ': Updated. ' + history_line)
1417
+ updated_node = cypher_update_node_properties(node_element_id=node.element_id,
1418
+ node_properties=node_properties)
1419
+
1420
+ if updated_node['name'] == 'FULL_NAME':
1421
+ personroot = get_or_create_personroot_node(person_node=updated_node)
1422
+ recreate_name_cache_in_personroot(personroot=personroot)
1423
+ return node
1424
+
1425
+
1426
+ # Note the similarity with create_nodepairs_and_edges_df().
1427
+ def update_nodes_df(nodes: pandas.DataFrame) -> None:
1428
+ """Update the values in a node, but only if values have been changed.
1429
+ This is done for all the nodes passed in the DataFrame.
1430
+
1431
+ The column names specify the property in a node (i.e. 'name', 'category', 'value').
1432
+ The row value of that column specify the property value of that property.
1433
+
1434
+ :param nodes: the nodes in a DataFrame.
1435
+ :return: None.
1436
+ """
1437
+ nodes_clean = nodes.copy(deep=True)
1438
+ # Ensure that all '' values are NaN, so that those rows can be easily removed with dropna()
1439
+ nodes_clean.replace('', numpy.nan, inplace=True)
1440
+ nodes_clean.dropna(axis=0, how='any', inplace=True)
1441
+ nodes_clean.drop_duplicates(keep='first', inplace=True, ignore_index=True)
1442
+
1443
+ print('There are ' + str(len(nodes_clean)) + ' nodes ('
1444
+ + timestamp() + '), updating node: 0 ', end='')
1445
+ count = 0
1446
+ columns = nodes_clean.columns
1447
+ for row in nodes_clean.itertuples():
1448
+ count += 1
1449
+ if count % 250 == 0:
1450
+ print(count, ' ', end='', flush=True)
1451
+ if count % 2500 == 0:
1452
+ print('(' + timestamp() + ')\n', end='', flush=True)
1453
+
1454
+ node_properties = {}
1455
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
1456
+ for other_name in columns:
1457
+ if prop_name == other_name:
1458
+ node_properties[prop_name] = getattr(row, other_name)
1459
+
1460
+ create_update_node(name=row.name, category=row.category, value=row.value,
1461
+ other_properties=node_properties)
1462
+
1463
+ print(count, '(' + timestamp() + ')\n', end='', flush=True)
1464
+ return
1465
+
1466
+
1467
+ def get_or_create_personroot_node(person_node: Node) -> Union[Node, None]:
1468
+ """Get a 'person-root' node for a given 'person' node, if that node has
1469
+ a 'person-root'. If not, create the 'person-root' node.
1470
+
1471
+ :param person_node: the node.
1472
+ :return: the 'person-root' node, or None on error.
1473
+ """
1474
+ global _graph
1475
+
1476
+ if person_node is None:
1477
+ print('get_or_create_personroot_node(): Error: node is None.')
1478
+ return
1479
+
1480
+ if person_node['category'] != 'person':
1481
+ print('get_or_create_personroot_node(): node category is not person.')
1482
+ return None
1483
+
1484
+ if person_node['name'] == 'person-root':
1485
+ return person_node
1486
+
1487
+ personroot_nodes = get_all_personroot_nodes(node=person_node)
1488
+ if len(personroot_nodes) == 0:
1489
+ # Create the 'person-root' node with a unique value.
1490
+ value = str(uuid.uuid4())
1491
+ personroot = create_update_node(name='person-root', category='person', value=value)
1492
+ cypher_create_edge_if_not_exists(left_node_element_id=person_node.element_id,
1493
+ right_node_element_id=personroot.element_id)
1494
+ return personroot
1495
+
1496
+ if len(personroot_nodes) > 1:
1497
+ print('get_or_create_personroot_node(): not anticipated: person_node "'
1498
+ + person_node['_key'] + '" has more than one person-root nodes: ')
1499
+ for node in personroot_nodes:
1500
+ print('"' + node['_key'] + '" ', end='')
1501
+ print('\nReturn the first.')
1502
+ # And fall through.
1503
+
1504
+ personroot = personroot_nodes[0]
1505
+ return personroot
1506
+
1507
+
1508
+ def connect_person_and_non_person_node(person_node: Node,
1509
+ non_person_node: Node) -> None:
1510
+ """Connect a 'person' and a non 'person' node.
1511
+
1512
+ :param person_node: the left node, being of category 'person'.
1513
+ :param non_person_node: the right node, being of a different category than 'person'.
1514
+ :return: None.
1515
+ """
1516
+ global _graph
1517
+
1518
+ if person_node is None or non_person_node is None:
1519
+ print('connect_persons_and_non_person_node(): Error: (one of the) nodes is None.')
1520
+ return
1521
+
1522
+ if person_node['category'] != 'person' or non_person_node['category'] == 'person':
1523
+ print('connect_person_and_non_person_node(): (one of the) nodes have wrong category.')
1524
+ return
1525
+
1526
+ if person_node['name'] == 'person-root':
1527
+ personroot = person_node
1528
+ else:
1529
+ personroot = get_or_create_personroot_node(person_node=person_node)
1530
+ if personroot is None:
1531
+ return
1532
+
1533
+ cypher_create_edge_if_not_exists(left_node_element_id=non_person_node.element_id,
1534
+ right_node_element_id=personroot.element_id)
1535
+ return
1536
+
1537
+
1538
+ def merge_two_nodes(node_merge_from: Node, node_merge_to: Node) -> Union[Node, None]:
1539
+ """Merge two nodes.
1540
+ The neighbors of 'node_merge_from' will be merged to node 'node_merge_to'
1541
+ and 'node_merge_from' will be deleted.
1542
+
1543
+ :param node_merge_from: the node to merge from.
1544
+ :param node_merge_to: the node to merge to.
1545
+ :return: the merged node, or None if this was not possible.
1546
+ """
1547
+ global _graph
1548
+
1549
+ if _graph is None:
1550
+ print('\nmerge_two_nodes(): Error: graph has not been initialized or opened.\n\n')
1551
+ return None
1552
+
1553
+ if node_merge_from is None and node_merge_to is not None:
1554
+ # Done.
1555
+ return node_merge_to
1556
+
1557
+ if node_merge_from is None or node_merge_to is None:
1558
+ print('merge_two_nodes(): Error: one or both of the nodes is None - cannot be found.')
1559
+ return None
1560
+
1561
+ if node_merge_from['name'] != node_merge_to['name'] \
1562
+ or node_merge_from['category'] != node_merge_to['category']:
1563
+ print('merge_two_nodes(): nodes "' + node_merge_from['_key']
1564
+ + '" and "' + node_merge_to['_key']
1565
+ + '" have a different "name" or "category" field: not supported.')
1566
+ return None
1567
+
1568
+ if node_merge_from == node_merge_to:
1569
+ # They are the same, done.
1570
+ return node_merge_to
1571
+
1572
+ node_merge_to_properties = {'_history': node_merge_to['_history'].copy()}
1573
+ node_merge_to_properties['_source'] = list(set(node_merge_from['_source'] + node_merge_to['_source']))
1574
+ node_merge_to_properties['_source'].sort()
1575
+
1576
+ time_stamp = datetimestamp(seconds=True)
1577
+ count = 0
1578
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1579
+ what_happened += 'Merged node "'
1580
+ what_happened += node_merge_from['_key'] + '" to this node '
1581
+ what_happened += 'and then deleted it.'
1582
+ node_merge_to_properties['_history'].append(what_happened)
1583
+
1584
+ count += 1
1585
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1586
+ what_happened += '----- Start of deleted node history and neighbors -----'
1587
+ node_merge_to_properties['_history'].append(what_happened)
1588
+
1589
+ count += 1
1590
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1591
+ what_happened += 'Start of history of the deleted node.'
1592
+ node_merge_to_properties['_history'].append(what_happened)
1593
+
1594
+ for history in node_merge_from['_history']:
1595
+ count += 1
1596
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1597
+ what_happened += history
1598
+ node_merge_to_properties['_history'].append(what_happened)
1599
+
1600
+ count += 1
1601
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1602
+ what_happened += 'End of history of the deleted node.'
1603
+ node_merge_to_properties['_history'].append(what_happened)
1604
+
1605
+ count += 1
1606
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1607
+ what_happened += 'These were the neighbors of the deleted node, '
1608
+ what_happened += 'now merged with the neighbors of this node:'
1609
+ node_merge_to_properties['_history'].append(what_happened)
1610
+
1611
+ neighbornodes = get_all_neighbor_nodes(node=node_merge_from)
1612
+ if len(neighbornodes) == 0:
1613
+ count += 1
1614
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1615
+ what_happened += '[the node to be deleted has no neighbors]'
1616
+ node_merge_to_properties['_history'].append(what_happened)
1617
+ else:
1618
+ for node in neighbornodes:
1619
+ count += 1
1620
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1621
+ what_happened += '"' + str(node['_key']) + '" '
1622
+ node_merge_to_properties['_history'].append(what_happened)
1623
+
1624
+ count += 1
1625
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1626
+ what_happened += 'End of list of neighbors of the deleted node.'
1627
+ node_merge_to_properties['_history'].append(what_happened)
1628
+
1629
+ count += 1
1630
+ what_happened = time_stamp + '-' + format(count, '02d') + ': '
1631
+ what_happened += '----- End of deleted node history and neighbors -----'
1632
+ node_merge_to_properties['_history'].append(what_happened)
1633
+
1634
+ # Note only properties '_history' and '_source' are changed.
1635
+ merged_node = cypher_merge_nodes(node_merge_from_element_id=node_merge_from.element_id,
1636
+ node_merge_to_element_id=node_merge_to.element_id,
1637
+ node_merge_to_properties=node_merge_to_properties)
1638
+ if merged_node is None:
1639
+ return None
1640
+ if merged_node['name'] == 'person-root':
1641
+ # Property 'comment' is also updated.
1642
+ recreate_name_cache_in_personroot(personroot=merged_node)
1643
+
1644
+ return merged_node
1645
+
1646
+
1647
+ def merge_two_nodes_name_value(node_merge_from_name: str, node_merge_from_value: str,
1648
+ node_merge_to_name: str, node_merge_to_value: str) -> None:
1649
+ """Merge two nodes, by specifying their 'name' and 'value' fields.
1650
+ The neighbors of 'node_merge_from' will be merged to node 'node_merge_to'
1651
+ and 'node_merge_from' will be deleted.
1652
+
1653
+ :param node_merge_from_name: 'name' property of node_merge_from.
1654
+ :param node_merge_from_value: 'value' property of node_merge_from.
1655
+ :param node_merge_to_name: 'name' property of node_merge_to.
1656
+ :param node_merge_to_value: 'value' property of node_merge_to.
1657
+ :return: None.
1658
+ """
1659
+ global _graph
1660
+
1661
+ if _graph is None:
1662
+ print('\nmerge_two_nodes_name_value(): Error: graph has not been initialized or opened.\n\n')
1663
+ return
1664
+
1665
+ node_merge_from = read_node(name=node_merge_from_name,
1666
+ value=node_merge_from_value)
1667
+ node_merge_to = read_node(name=node_merge_to_name,
1668
+ value=node_merge_to_value)
1669
+ merge_two_nodes(node_merge_from=node_merge_from,
1670
+ node_merge_to=node_merge_to)
1671
+ return
1672
+
1673
+
1674
+ def connect_person_and_person_node(left_node: Node, right_node: Node) -> None:
1675
+ """Connect two person nodes.
1676
+
1677
+ :param left_node: the left node.
1678
+ :param right_node: the right node.
1679
+ :return: None.
1680
+ """
1681
+ global _graph
1682
+
1683
+ if left_node is None or right_node is None:
1684
+ print('connect_person_and_person_node(): (one of the) nodes is None.')
1685
+ return
1686
+
1687
+ if left_node['category'] != 'person' or right_node['category'] != 'person':
1688
+ print('connect_person_and_person_node(): (one of the) nodes have wrong category.')
1689
+ return
1690
+
1691
+ if left_node['name'] == 'person-root' or right_node['name'] == 'person-root':
1692
+ print('connect_person_and_person_node(): not anticipated: (one of the) person_nodes '
1693
+ + 'are "person-root" nodes.')
1694
+ return
1695
+
1696
+ nr_edges_left_node = ricgraph_nr_edges_of_node(node_element_id=left_node.element_id)
1697
+ nr_edges_right_node = ricgraph_nr_edges_of_node(node_element_id=right_node.element_id)
1698
+ if nr_edges_left_node == 0 and nr_edges_right_node == 0:
1699
+ # None of the nodes have a 'person-root' node, create one and connect.
1700
+ personroot = get_or_create_personroot_node(person_node=left_node)
1701
+ if personroot is None:
1702
+ return
1703
+ cypher_create_edge_if_not_exists(left_node_element_id=right_node.element_id,
1704
+ right_node_element_id=personroot.element_id)
1705
+ return
1706
+
1707
+ if nr_edges_left_node > 0 and nr_edges_right_node == 0:
1708
+ # 'left_node' already has a 'person-root' node.
1709
+ personroot = get_or_create_personroot_node(person_node=left_node)
1710
+ if personroot is None:
1711
+ return
1712
+ cypher_create_edge_if_not_exists(left_node_element_id=right_node.element_id,
1713
+ right_node_element_id=personroot.element_id)
1714
+ return
1715
+
1716
+ if nr_edges_left_node == 0 and nr_edges_right_node > 0:
1717
+ # 'right_node' already has a 'person-root' node.
1718
+ personroot = get_or_create_personroot_node(person_node=right_node)
1719
+ if personroot is None:
1720
+ return
1721
+ cypher_create_edge_if_not_exists(left_node_element_id=left_node.element_id,
1722
+ right_node_element_id=personroot.element_id)
1723
+ return
1724
+
1725
+ left_personroot_node = get_or_create_personroot_node(person_node=left_node)
1726
+ right_personroot_node = get_or_create_personroot_node(person_node=right_node)
1727
+ if left_personroot_node is None or right_personroot_node is None:
1728
+ return
1729
+
1730
+ if left_personroot_node == right_personroot_node:
1731
+ # Already connected, nothing to do.
1732
+ return
1733
+
1734
+ # Only continue depending on RICGRAPH_NODEADD_MODE.
1735
+ if RICGRAPH_NODEADD_MODE == 'strict':
1736
+ # For more explanation, see file docs/ricgraph_install_configure.md,
1737
+ # section RICGRAPH_NODEADD_MODE.
1738
+ return
1739
+
1740
+ # Connect crosswise.
1741
+ cypher_create_edge_if_not_exists(left_node_element_id=left_node.element_id,
1742
+ right_node_element_id=right_personroot_node.element_id)
1743
+ cypher_create_edge_if_not_exists(left_node_element_id=right_node.element_id,
1744
+ right_node_element_id=left_personroot_node.element_id)
1745
+
1746
+ time_stamp = datetimestamp()
1747
+ message = 'The node pair "'
1748
+ message += left_node['_key'] + '" and "' + right_node['_key']
1749
+ message += '" caused this node to have more than one neighbor. '
1750
+ message += 'These are its person-root nodes: "'
1751
+ message += left_personroot_node['_key'] + '" and "'
1752
+ message += right_personroot_node['_key'] + '". '
1753
+ message += 'This might be caused by a mislabeling in a harvested system.'
1754
+ timestamped_message = time_stamp + ': ' + message
1755
+
1756
+ print('\nconnect_person_and_person_node(): ' + message)
1757
+ node_property = {'_history': left_node['_history'].copy()}
1758
+ node_property['_history'].append(timestamped_message)
1759
+ cypher_update_node_properties(node_element_id=left_node.element_id,
1760
+ node_properties=node_property)
1761
+ node_property = {'_history': right_node['_history'].copy()}
1762
+ node_property['_history'].append(timestamped_message)
1763
+ cypher_update_node_properties(node_element_id=right_node.element_id,
1764
+ node_properties=node_property)
1765
+ return
1766
+
1767
+
1768
+ def connect_two_nodes(left_node: Node, right_node: Node) -> None:
1769
+ """Connect two nodes with two directed edges.
1770
+
1771
+ In case a node is to contain a personal ID (e.g. ORCID, ISNI, etc.) (then it has
1772
+ category = 'person'), a 'person-root' node will be created if it does not exist.
1773
+ This 'person-root' node is to be seen as the 'representative' for a person.
1774
+ All research outputs will be linked to the 'person-root' node, not to the personal ID node.
1775
+
1776
+ :param left_node: the left node.
1777
+ :param right_node: the right node.
1778
+ :return: None.
1779
+ """
1780
+ global _graph
1781
+
1782
+ if left_node is None or right_node is None:
1783
+ return
1784
+
1785
+ if left_node['category'] != 'person' and right_node['category'] != 'person':
1786
+ # This is not a person to person link, link directly
1787
+ cypher_create_edge_if_not_exists(left_node_element_id=left_node.element_id,
1788
+ right_node_element_id=right_node.element_id)
1789
+ return
1790
+
1791
+ # At least one of the nodes is a 'person' link. These should be linked via their 'person-root' node.
1792
+ if left_node['category'] == 'person' and right_node['category'] != 'person':
1793
+ connect_person_and_non_person_node(person_node=left_node,
1794
+ non_person_node=right_node)
1795
+ return
1796
+
1797
+ if left_node['category'] != 'person' and right_node['category'] == 'person':
1798
+ connect_person_and_non_person_node(person_node=right_node,
1799
+ non_person_node=left_node)
1800
+ return
1801
+
1802
+ connect_person_and_person_node(left_node=left_node,
1803
+ right_node=right_node)
1804
+ return
1805
+
1806
+
1807
+ def create_two_nodes_and_edge(name1: str, category1: str, value1: str,
1808
+ name2: str, category2: str, value2: str,
1809
+ **other_properties: Union[dict, str]) -> None:
1810
+ """Create two nodes (if they do not exist) and two directed edges between those two nodes.
1811
+ The nodes are specified by the properties passed.
1812
+ All 'left' nodes end with '1', all right nodes end with '2'.
1813
+
1814
+ :param name1: 'name' property of left node.
1815
+ :param category1: idem.
1816
+ :param value1: idem.
1817
+ :param name2: 'name' property of right node.
1818
+ :param category2: idem.
1819
+ :param value2: idem
1820
+ :param other_properties: a dictionary of all the other properties.
1821
+ Names ending with '1' belong to a 'left' node,
1822
+ names ending with '2' to a 'right' node.
1823
+ :return: None.
1824
+ """
1825
+ node_properties1 = {}
1826
+ node_properties2 = {}
1827
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
1828
+ for other_name, other_value in other_properties.items():
1829
+ if prop_name + '1' == other_name:
1830
+ node_properties1.update({prop_name: other_value})
1831
+ if prop_name + '2' == other_name:
1832
+ node_properties2.update({prop_name: other_value})
1833
+
1834
+ node1 = create_update_node(name=name1, category=category1, value=value1,
1835
+ other_properties=node_properties1)
1836
+ node2 = create_update_node(name=name2, category=category2, value=value2,
1837
+ other_properties=node_properties2)
1838
+ if node1 is None or node2 is None:
1839
+ return
1840
+
1841
+ connect_two_nodes(left_node=node1, right_node=node2)
1842
+ if node1['name'] == 'FULL_NAME':
1843
+ personroot = get_or_create_personroot_node(person_node=node1)
1844
+ create_name_cache_in_personroot(node=node1, personroot=personroot)
1845
+ if node2['name'] == 'FULL_NAME':
1846
+ personroot = get_or_create_personroot_node(person_node=node2)
1847
+ create_name_cache_in_personroot(node=node2, personroot=personroot)
1848
+ return
1849
+
1850
+
1851
+ def create_nodepairs_and_edges_df(left_and_right_nodepairs: pandas.DataFrame) -> None:
1852
+ """Create two nodes (called 'node pairs') and two directed edges between those nodes.
1853
+ This is done for all the nodes passed. The nodes are specified in a DataFrame.
1854
+ One row in the DataFrame contains two nodes which are to be connected.
1855
+
1856
+ The column names specify the property in a node (i.e. 'name1', 'category1', 'value1').
1857
+ The row value of that column specify the property value of that property.
1858
+ All 'left' nodes end with '1', all right nodes end with '2'.
1859
+
1860
+ :param left_and_right_nodepairs: the node pairs in a DataFrame.
1861
+ :return: None.
1862
+ """
1863
+ print('There are ' + str(len(left_and_right_nodepairs)) + ' rows ('
1864
+ + timestamp() + '), creating nodes and edges for row: 0 ', end='')
1865
+ count = 0
1866
+ columns = left_and_right_nodepairs.columns
1867
+ for row in left_and_right_nodepairs.itertuples():
1868
+ count += 1
1869
+ if count % 250 == 0:
1870
+ print(count, ' ', end='', flush=True)
1871
+ if count % 2500 == 0:
1872
+ print('(' + timestamp() + ')\n', end='', flush=True)
1873
+
1874
+ node_properties = {}
1875
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
1876
+ for other_name in columns:
1877
+ if prop_name + '1' == other_name:
1878
+ node_properties[prop_name + '1'] = getattr(row, other_name)
1879
+ for other_name in columns:
1880
+ if prop_name + '2' == other_name:
1881
+ node_properties[prop_name + '2'] = getattr(row, other_name)
1882
+
1883
+ create_two_nodes_and_edge(name1=row.name1, category1=row.category1, value1=row.value1,
1884
+ name2=row.name2, category2=row.category2, value2=row.value2,
1885
+ **node_properties)
1886
+ print(count, '(' + timestamp() + ')\n', end='', flush=True)
1887
+ return
1888
+
1889
+
1890
+ def create_nodepairs_and_edges_params(name1: Union[dict, str], category1: Union[dict, str], value1: Union[dict, str],
1891
+ name2: Union[dict, str], category2: Union[dict, str], value2: Union[dict, str],
1892
+ **other_properties: Union[dict, str]) -> None:
1893
+ """Create two nodes (called 'node pairs') and two directed edges between those nodes.
1894
+ This is done for all the nodes passed. The nodes are specified by values passed to this function.
1895
+ Each value can be either a string or a dict containing strings.
1896
+ All 'left' nodes end with '1', all right nodes end with '2'.
1897
+
1898
+ If it is a string (like 'ORCID') then that value will be used for every
1899
+ property value for that property. That is, every node pair will have the same value for that property.
1900
+
1901
+ If it is a dict containing strings, then every node pair will have a different value for that property.
1902
+
1903
+ :param name1: 'name' property of left node.
1904
+ :param category1: idem.
1905
+ :param value1: idem.
1906
+ :param name2: 'name' property of right node.
1907
+ :param category2: idem.
1908
+ :param value2: idem.
1909
+ :param other_properties: a dictionary of all the other properties.
1910
+ Names ending with '1' belong to a 'left' node,
1911
+ names ending with '2' to a 'right' node.
1912
+ :return: None.
1913
+ """
1914
+ if isinstance(value1, str):
1915
+ left_and_right_nodepairs = pandas.DataFrame(data={'value1': value1},
1916
+ index=[0])
1917
+ else:
1918
+ left_and_right_nodepairs = pandas.DataFrame(data=value1).copy(deep=True)
1919
+ left_and_right_nodepairs.rename(columns={value1.name: 'value1'}, inplace=True)
1920
+
1921
+ # This makes programming more efficient
1922
+ allowed_properties = RICGRAPH_PROPERTIES_ADDITIONAL + RICGRAPH_PROPERTIES_STANDARD
1923
+ ext_other_properties = other_properties.copy()
1924
+ ext_other_properties['name1'] = name1
1925
+ ext_other_properties['name2'] = name2
1926
+ ext_other_properties['category1'] = category1
1927
+ ext_other_properties['category2'] = category2
1928
+ # 'value1' has been included above
1929
+ ext_other_properties['value2'] = value2
1930
+
1931
+ for prop_name in allowed_properties:
1932
+ for other_name in ext_other_properties:
1933
+ other_value = ext_other_properties[other_name]
1934
+ if prop_name + '1' == other_name:
1935
+ if isinstance(other_value, str):
1936
+ left_and_right_nodepairs[prop_name + '1'] = other_value
1937
+ else:
1938
+ left_and_right_nodepairs = pandas.concat([left_and_right_nodepairs, other_value], axis='columns')
1939
+ left_and_right_nodepairs.rename(columns={other_value.name: prop_name + '1'}, inplace=True)
1940
+
1941
+ if prop_name + '2' == other_name:
1942
+ if isinstance(other_value, str):
1943
+ left_and_right_nodepairs[prop_name + '2'] = other_value
1944
+ else:
1945
+ left_and_right_nodepairs = pandas.concat([left_and_right_nodepairs, other_value], axis='columns')
1946
+ left_and_right_nodepairs.rename(columns={other_value.name: prop_name + '2'}, inplace=True)
1947
+
1948
+ # Reorder a bit, starting with the last, only the name-category-value property, since they will surely exist
1949
+ left_and_right_nodepairs.insert(0, 'value2', left_and_right_nodepairs.pop('value2'))
1950
+ left_and_right_nodepairs.insert(0, 'category2', left_and_right_nodepairs.pop('category2'))
1951
+ left_and_right_nodepairs.insert(0, 'name2', left_and_right_nodepairs.pop('name2'))
1952
+ left_and_right_nodepairs.insert(0, 'value1', left_and_right_nodepairs.pop('value1'))
1953
+ left_and_right_nodepairs.insert(0, 'category1', left_and_right_nodepairs.pop('category1'))
1954
+ left_and_right_nodepairs.insert(0, 'name1', left_and_right_nodepairs.pop('name1'))
1955
+
1956
+ create_nodepairs_and_edges_df(left_and_right_nodepairs=left_and_right_nodepairs)
1957
+ return
1958
+
1959
+
1960
+ def unify_personal_identifiers(personal_identifiers: pandas.DataFrame,
1961
+ source_event: Union[dict, str], history_event: Union[dict, str]) -> None:
1962
+ """Unify a collection of personal identifiers (e.g. ORCID, ISNI, etc.).
1963
+ That means that every column is unified to every other column.
1964
+ The identifiers are specified as columns in a DataFrame.
1965
+
1966
+ :param personal_identifiers: DataFrame containing personal identifiers.
1967
+ :param source_event: an event to add to the _source list.
1968
+ :param history_event: a history event to add.
1969
+ :return: None.
1970
+ """
1971
+ # Use i & j to make sure not to unify twice
1972
+ i = 0
1973
+ for column1 in personal_identifiers.columns:
1974
+ i += 1
1975
+ j = 0
1976
+ for column2 in personal_identifiers.columns:
1977
+ j += 1
1978
+ if i >= j:
1979
+ continue
1980
+ print('\nUnifying personal identifiers ' + column1 + ' and ' + column2
1981
+ + ' at ' + timestamp() + '...')
1982
+ identifiers = personal_identifiers[[column1, column2]].copy(deep=True)
1983
+
1984
+ # Ensure that all '' values are NaN, so that those rows can be easily removed with dropna()
1985
+ identifiers.replace('', numpy.nan, inplace=True)
1986
+ # dropna(how='any'): drop row if one or more row values contain NaN
1987
+ identifiers.dropna(axis=0, how='any', inplace=True)
1988
+ identifiers.drop_duplicates(keep='first', inplace=True, ignore_index=True)
1989
+
1990
+ if RICGRAPH_NODEADD_MODE == 'strict':
1991
+ # Remove rows which have a cell value that occurs more than once.
1992
+ # E.g. if we have a DataFrame:
1993
+ # FULL_NAME ORCID
1994
+ # Name1 10
1995
+ # Name2 11
1996
+ # Name3 11
1997
+ # then we remove both rows with ORCID 11.
1998
+ # For more explanation, see file docs/ricgraph_install_configure.md,
1999
+ # section RICGRAPH_NODEADD_MODE.
2000
+ identifiers.drop_duplicates(subset=column1, keep=False, inplace=True, ignore_index=True)
2001
+ identifiers.drop_duplicates(subset=column2, keep=False, inplace=True, ignore_index=True)
2002
+
2003
+ create_nodepairs_and_edges_params(name1=column1, category1='person', value1=identifiers[column1],
2004
+ name2=column2, category2='person', value2=identifiers[column2],
2005
+ source_event1=source_event, source_event2=source_event,
2006
+ history_event1=history_event, history_event2=history_event)
2007
+ print('\nDone at ' + timestamp() + '.\n')
2008
+ return
2009
+
2010
+
2011
+ def print_node_values(node: Node) -> None:
2012
+ """Print the values of all properties in a node.
2013
+
2014
+ :param node: the node.
2015
+ :return: None.
2016
+ """
2017
+ print('Node values:')
2018
+ print('_key: ' + node['_key'])
2019
+ print('name: ' + node['name'])
2020
+ print('category: ' + node['category'])
2021
+ print('value: ' + node['value'])
2022
+ for prop_name in RICGRAPH_PROPERTIES_ADDITIONAL:
2023
+ print(prop_name + ': ' + node[prop_name])
2024
+ print('source:')
2025
+ for source in node['_source']:
2026
+ print('- ' + source)
2027
+ print('history:')
2028
+ for history in node['_history']:
2029
+ print('- ' + history)
2030
+ print('')
2031
+ return
2032
+
2033
+
2034
+ # ##############################################################################
2035
+ # Neighbor node and edge functions.
2036
+ # These start with "get_", not with "read_" as some above.
2037
+ # The difference is that "get_" functions work in constant time (starting from
2038
+ # a node) while the "read_" functions read (aka search, find) the whole graph.
2039
+ # ##############################################################################
2040
+
2041
+
2042
+ # ######
2043
+ # This function does not seem necessary. Use get_all_neighbor_nodes() or
2044
+ # ricgraph_nr_edges_of_node().
2045
+ # Note that this function returns a list of edges, but you cannot get the next node
2046
+ # directly from an edge in that list.
2047
+ # ######
2048
+ # def get_edges(node: Node) -> list:
2049
+ # """Get the edges attached to a node.
2050
+ #
2051
+ # :param node: the node.
2052
+ # :return: a list of the edges, or empty list if no edges are connected.
2053
+ # """
2054
+ # global _graph
2055
+ #
2056
+ # cypher_query = 'MATCH (node)-[edge]->() WHERE elementId(node)=$node_element_id RETURN edge'
2057
+ # edges = _graph.execute_query(cypher_query,
2058
+ # node_element_id=node.element_id,
2059
+ # result_transformer_=Result.value,
2060
+ # database_=ricgraph_databasename())
2061
+ # if len(edges) == 0:
2062
+ # return []
2063
+ # else:
2064
+ # return edges
2065
+ # ######
2066
+
2067
+
2068
+ def get_personroot_node(node: Node) -> Union[Node, None]:
2069
+ """Get the 'person-root' node for any type of node.
2070
+ If 'node' is already a 'person-root' node, return 'node'.
2071
+ If there is more than one person-root node (which should not happen
2072
+ if the category is 'person'), return the first.
2073
+
2074
+ :param node: the node.
2075
+ :return: the person-root node.
2076
+ """
2077
+ personroot_nodes = get_all_personroot_nodes(node)
2078
+ if len(personroot_nodes) == 0:
2079
+ return None
2080
+ else:
2081
+ return personroot_nodes[0]
2082
+
2083
+
2084
+ def get_all_personroot_nodes(node: Node) -> list:
2085
+ """Get the 'person-root' node(s) for any type of node.
2086
+ If 'node' is already a 'person-root' node, return 'node'.
2087
+ If there is more than one person-root node (which can happen if
2088
+ node is e.g. a research output, and which should not happen
2089
+ if the category is 'person'), all will be returned in a list.
2090
+
2091
+ :param node: the node.
2092
+ :return: a list of all the person-root nodes found.
2093
+ """
2094
+ if node is None:
2095
+ return []
2096
+
2097
+ if node['name'] == 'person-root':
2098
+ return [node]
2099
+
2100
+ personroot_nodes = get_all_neighbor_nodes(node=node,
2101
+ name_want='person-root')
2102
+ return personroot_nodes
2103
+
2104
+
2105
+ def get_all_neighbor_nodes(node: Node,
2106
+ name_want: Union[str, list] = '',
2107
+ name_dontwant: Union[str, list] = '',
2108
+ category_want: Union[str, list] = '',
2109
+ category_dontwant: Union[str, list] = '',
2110
+ max_nr_neighbor_nodes: int = 0) -> list:
2111
+ """Get all the neighbors of 'node' in a list.
2112
+ You can restrict the nodes returned by specifying one or more of the
2113
+ other parameters. If more than one is specified, the result is an AND.
2114
+
2115
+ :param node: the node we need neighbors from.
2116
+ :param name_want: either a string which indicates that we only want neighbor
2117
+ nodes where the property 'name' is equal to 'name_want'
2118
+ (e.g. 'ORCID'),
2119
+ or a list containing several node names, indicating
2120
+ that we want all neighbor nodes where the property 'name' equals
2121
+ one of the names in the list 'name_want'
2122
+ (e.g. ['ORCID', 'ISNI', 'FULL_NAME']).
2123
+ If empty (empty string), return all nodes.
2124
+ :param name_dontwant: similar, but for property 'name' and nodes we don't want.
2125
+ If empty (empty string), all nodes are 'wanted'.
2126
+ :param category_want: similar to 'name_want', but now for the property 'category'.
2127
+ :param category_dontwant: similar, but for property 'category' and nodes we don't want.
2128
+ :param max_nr_neighbor_nodes: return at most this number of nodes, 0 = all nodes.
2129
+ :return: the list of neighboring nodes satisfying all these criteria, or
2130
+ empty list if nothing found.
2131
+ """
2132
+ global _graph
2133
+ global graphdb_nr_reads
2134
+
2135
+ if _graph is None:
2136
+ print('\nget_all_neighbor_nodes(): Error: graph has not been initialized or opened.\n\n')
2137
+ return []
2138
+
2139
+ if node is None:
2140
+ return []
2141
+
2142
+ cypher_query = 'MATCH (node:RicgraphNode)-[]->(neighbor:RicgraphNode) '
2143
+ if ricgraph_database() == 'neo4j':
2144
+ cypher_query += 'WHERE (elementId(node)=$node_element_id) '
2145
+ else:
2146
+ cypher_query += 'WHERE (id(node)=toInteger($node_element_id)) '
2147
+
2148
+ nr_of_not_clauses = 0
2149
+ if isinstance(name_want, str):
2150
+ if name_want != '':
2151
+ cypher_query += 'AND (neighbor.name IN ["' + name_want + '"]) '
2152
+ else:
2153
+ if len(name_want) > 0:
2154
+ string_list = '["' + '", "'.join(str(item) for item in name_want) + '"]'
2155
+ cypher_query += 'AND (neighbor.name IN ' + string_list + ') '
2156
+
2157
+ if isinstance(name_dontwant, str):
2158
+ if name_dontwant != '':
2159
+ cypher_query += 'AND (NOT neighbor.name IN ["' + name_dontwant + '"]) '
2160
+ nr_of_not_clauses += 1
2161
+ else:
2162
+ if len(name_dontwant) > 0:
2163
+ string_list = '["' + '", "'.join(str(item) for item in name_dontwant) + '"]'
2164
+ cypher_query += 'AND (NOT neighbor.name IN ' + string_list + ') '
2165
+ nr_of_not_clauses += 1
2166
+
2167
+ if isinstance(category_want, str):
2168
+ if category_want != '':
2169
+ cypher_query += 'AND (neighbor.category IN ["' + category_want + '"]) '
2170
+ else:
2171
+ if len(category_want) > 0:
2172
+ string_list = '["' + '", "'.join(str(item) for item in category_want) + '"]'
2173
+ cypher_query += 'AND (neighbor.category IN ' + string_list + ') '
2174
+
2175
+ if isinstance(category_dontwant, str):
2176
+ if category_dontwant != '':
2177
+ cypher_query += 'AND (NOT neighbor.category IN ["' + category_dontwant + '"]) '
2178
+ nr_of_not_clauses += 1
2179
+ else:
2180
+ if len(category_dontwant) > 0:
2181
+ string_list = '["' + '", "'.join(str(item) for item in category_dontwant) + '"]'
2182
+ cypher_query += 'AND (NOT neighbor.category IN ' + string_list + ') '
2183
+ nr_of_not_clauses += 1
2184
+
2185
+ if nr_of_not_clauses >= 2:
2186
+ # This is a special case in which the Cypher query produces unexpected results.
2187
+ neighbor_nodes = get_all_neighbor_nodes_loop(node=node,
2188
+ name_want=name_want,
2189
+ name_dontwant=name_dontwant,
2190
+ category_want=category_want,
2191
+ category_dontwant=category_dontwant,
2192
+ max_nr_neighbor_nodes=max_nr_neighbor_nodes)
2193
+ return neighbor_nodes
2194
+
2195
+ cypher_query += 'RETURN DISTINCT neighbor '
2196
+ if max_nr_neighbor_nodes > 0:
2197
+ cypher_query += 'LIMIT ' + str(max_nr_neighbor_nodes)
2198
+ # print(cypher_query)
2199
+
2200
+ neighbor_nodes = _graph.execute_query(cypher_query,
2201
+ node_element_id=node.element_id,
2202
+ result_transformer_=Result.value,
2203
+ database_=ricgraph_databasename())
2204
+ nr_neighbors = len(neighbor_nodes)
2205
+ # Unsure what to count here, this seems reasonable. '+ 1' for 'node'.
2206
+ graphdb_nr_reads += nr_neighbors + 1
2207
+ if nr_neighbors == 0:
2208
+ return []
2209
+ else:
2210
+ return neighbor_nodes
2211
+
2212
+
2213
+ def get_all_neighbor_nodes_loop(node: Node,
2214
+ name_want: Union[str, list] = '',
2215
+ name_dontwant: Union[str, list] = '',
2216
+ category_want: Union[str, list] = '',
2217
+ category_dontwant: Union[str, list] = '',
2218
+ max_nr_neighbor_nodes: int = 0) -> list:
2219
+ """Get all the neighbors of 'node' in a list.
2220
+ You can restrict the nodes returned by specifying one or more of the
2221
+ other parameters. If more than one is specified, the result is an AND.
2222
+
2223
+ This is the same function as get_all_neighbor_nodes() but it is much slower
2224
+ since it uses a loop. We need this in case
2225
+ len(name_dontwant_list) > 0 and len(category_dontwant_list) > 0.
2226
+
2227
+ :param node: the node we need neighbors from.
2228
+ :param name_want: either a string which indicates that we only want neighbor
2229
+ nodes where the property 'name' is equal to 'name_want'
2230
+ (e.g. 'ORCID'),
2231
+ or a list containing several node names, indicating
2232
+ that we want all neighbor nodes where the property 'name' equals
2233
+ one of the names in the list 'name_want'
2234
+ (e.g. ['ORCID', 'ISNI', 'FULL_NAME']).
2235
+ If empty (empty string), return all nodes.
2236
+ :param name_dontwant: similar, but for property 'name' and nodes we don't want.
2237
+ If empty (empty string), all nodes are 'wanted'.
2238
+ :param category_want: similar to 'name_want', but now for the property 'category'.
2239
+ :param category_dontwant: similar, but for property 'category' and nodes we don't want.
2240
+ :param max_nr_neighbor_nodes: return at most this number of nodes, 0 = all nodes.
2241
+ :return: the list of neighboring nodes satisfying all these criteria.
2242
+ """
2243
+ global _graph
2244
+ global graphdb_nr_reads
2245
+
2246
+ if _graph is None:
2247
+ print('\nget_all_neighbor_nodes_loop(): Error: graph has not been initialized or opened.\n\n')
2248
+ return []
2249
+
2250
+ if node is None:
2251
+ return []
2252
+
2253
+ candidate_neighbor_nodes = get_all_neighbor_nodes(node=node)
2254
+ if len(candidate_neighbor_nodes) == 0:
2255
+ return []
2256
+
2257
+ # If 'name_want' etc. are strings, convert them to lists.
2258
+ if isinstance(name_want, str):
2259
+ if name_want == '':
2260
+ name_want_list = []
2261
+ else:
2262
+ name_want_list = [name_want]
2263
+ else:
2264
+ name_want_list = name_want
2265
+
2266
+ if isinstance(name_dontwant, str):
2267
+ if name_dontwant == '':
2268
+ name_dontwant_list = []
2269
+ else:
2270
+ name_dontwant_list = [name_dontwant]
2271
+ else:
2272
+ name_dontwant_list = name_dontwant
2273
+
2274
+ if isinstance(category_want, str):
2275
+ if category_want == '':
2276
+ category_want_list = []
2277
+ else:
2278
+ category_want_list = [category_want]
2279
+ else:
2280
+ category_want_list = category_want
2281
+
2282
+ if isinstance(category_dontwant, str):
2283
+ if category_dontwant == '':
2284
+ category_dontwant_list = []
2285
+ else:
2286
+ category_dontwant_list = [category_dontwant]
2287
+ else:
2288
+ category_dontwant_list = category_dontwant
2289
+
2290
+ count = 0
2291
+ all_nodes = A_LARGE_NUMBER
2292
+ if max_nr_neighbor_nodes == 0:
2293
+ max_nr_neighbor_nodes = all_nodes
2294
+ neighbor_nodes = []
2295
+ for neighbor in candidate_neighbor_nodes:
2296
+ if count >= max_nr_neighbor_nodes:
2297
+ break
2298
+ if node == neighbor:
2299
+ continue
2300
+ if neighbor['name'] in name_dontwant_list:
2301
+ continue
2302
+ if neighbor['category'] in category_dontwant_list:
2303
+ continue
2304
+ if len(name_want) == 0 and len(category_want) == 0:
2305
+ neighbor_nodes.append(neighbor)
2306
+ count += 1
2307
+ continue
2308
+ if neighbor['name'] in name_want_list \
2309
+ and neighbor['category'] in category_want_list:
2310
+ neighbor_nodes.append(neighbor)
2311
+ count += 1
2312
+ continue
2313
+ if neighbor['name'] in name_want_list and len(category_want_list) == 0:
2314
+ neighbor_nodes.append(neighbor)
2315
+ count += 1
2316
+ continue
2317
+ if len(name_want_list) == 0 and neighbor['category'] in category_want_list:
2318
+ neighbor_nodes.append(neighbor)
2319
+ count += 1
2320
+ continue
2321
+ # Any other next_node we do not want.
2322
+
2323
+ # Unsure what to count here, this seems reasonable. '+ 1' for 'node'.
2324
+ graphdb_nr_reads += len(neighbor_nodes) + 1
2325
+ return neighbor_nodes
2326
+
2327
+
2328
+ # ##############################################################################
2329
+ # Utility functions
2330
+ # ##############################################################################
2331
+
2332
+ def create_multidimensional_dict(dimension: int, dict_type):
2333
+ """Create a multimensional dict.
2334
+ Non-existing keys will be added automatically.
2335
+ From https://stackoverflow.com/questions/29348345/declaring-a-multi-dimensional-dictionary-in-python,
2336
+ the second answer.
2337
+
2338
+ Example use:
2339
+ new_dict = create_multidimensional_dict(2, int)
2340
+ new_dict['key1']['key2'] += 5
2341
+
2342
+ :param dimension: dimension of the dict.
2343
+ :param dict_type: type of the dict.
2344
+ :return: the dict.
2345
+ """
2346
+ if dimension == 0:
2347
+ return None
2348
+ if dimension == 1:
2349
+ return defaultdict(dict_type)
2350
+ else:
2351
+ return defaultdict(lambda: create_multidimensional_dict(dimension=dimension - 1,
2352
+ dict_type=dict_type))
2353
+
2354
+
2355
+ def lookup_resout_type(research_output_type: str,
2356
+ research_output_mapping: dict) -> str:
2357
+ """Convert a research output type from a harvested system
2358
+ to a shorter and easier Ricgraph research output type, according to a certain mapping.
2359
+ The reason for doing this is to ensure a constant naming of research output
2360
+ types for objects harvested from different sources.
2361
+ For more explanation, see the text at 'Research output types used in Ricgraph' at
2362
+ the start of this file.
2363
+
2364
+ :param research_output_type: A research output type from a source system.
2365
+ :param research_output_mapping: The mapping from the source system to Ricgraph
2366
+ research output types.
2367
+ :return: The result, in a few words.
2368
+ """
2369
+ if research_output_type == '':
2370
+ print('lookup_resout_type(): no research output type specified .')
2371
+ return 'empty'
2372
+
2373
+ if research_output_type not in research_output_mapping:
2374
+ print('lookup_resout_type(): unknown research output type: "' + research_output_type + '".')
2375
+ return 'unknown'
2376
+
2377
+ return research_output_mapping[research_output_type]
2378
+
2379
+
2380
+ # #####
2381
+ # Note:
2382
+ # This function was written using the JSON harvested by the following python files:
2383
+ # - GET: harvest_openalex_to_ricgraph.py.
2384
+ # - POST: harvest_pure_to_ricgraph.py.
2385
+ #
2386
+ # Possibly for other harvests some changes should be made. Please also test the result
2387
+ # with the above-mentioned files, and add the filename of the new harvest script.
2388
+ # #####
2389
+ def harvest_json(url: str, headers: dict, body: dict = None, max_recs_to_harvest: int = 0, chunksize: int = 0) -> list:
2390
+ """Harvest json data from a file.
2391
+
2392
+ :param url: URL to harvest.
2393
+ :param headers: headers required.
2394
+ :param body: the body of a POST request, or [] for a GET request.
2395
+ :param max_recs_to_harvest: maximum records to harvest.
2396
+ :param chunksize: chunk size to use (i.e. the number of records harvested in one call to 'url').
2397
+ :return: list of records in json format, or empty list if nothing found.
2398
+ """
2399
+ if body is None:
2400
+ body = []
2401
+
2402
+ print('Harvesting json data from ' + url + '.')
2403
+ print('Getting data at ' + datetimestamp() + '...')
2404
+
2405
+ all_records = A_LARGE_NUMBER
2406
+ if max_recs_to_harvest == 0:
2407
+ max_recs_to_harvest = all_records
2408
+ if chunksize == 0:
2409
+ chunksize = 1
2410
+ if len(body) == 0:
2411
+ # GET http request
2412
+ request_type = 'get'
2413
+ url += '&per_page=' + str(chunksize)
2414
+ else:
2415
+ # POST http request
2416
+ request_type = 'post'
2417
+ body['size'] = chunksize
2418
+
2419
+ # Do a first harvest to determine the number of records to harvest.
2420
+ if request_type == 'get':
2421
+ response = requests.get(url=url, headers=headers)
2422
+ else:
2423
+ response = requests.post(url=url, headers=headers, json=body)
2424
+
2425
+ if response.status_code != requests.codes.ok:
2426
+ print('harvest_json(): error during harvest, possibly '
2427
+ + 'a missing API-key or mixed up POST body?')
2428
+ print('Status code: ' + str(response.status_code))
2429
+ print('Url: ' + response.url)
2430
+ print('Error: ' + response.text)
2431
+ exit(1)
2432
+
2433
+ chunk_json_data = response.json()
2434
+ total_records = 0
2435
+ if request_type == 'get':
2436
+ if 'meta' in chunk_json_data:
2437
+ if 'count' in chunk_json_data['meta']:
2438
+ total_records = chunk_json_data['meta']['count']
2439
+ else:
2440
+ if 'count' in chunk_json_data:
2441
+ total_records = chunk_json_data['count']
2442
+
2443
+ if total_records == 0:
2444
+ print('harvest_json(): Warning: malformed json, "count" is missing.')
2445
+
2446
+ if max_recs_to_harvest == all_records:
2447
+ print('There are ' + str(total_records)
2448
+ + ' records, harvesting in chunks of ' + str(chunksize)
2449
+ + ' items.')
2450
+ else:
2451
+ print('There are ' + str(total_records)
2452
+ + ' records, harvesting in chunks of ' + str(chunksize)
2453
+ + ', at most ' + str(max_recs_to_harvest) + ' items.')
2454
+
2455
+ print('Harvesting record: ', end='', flush=True)
2456
+
2457
+ json_data = []
2458
+ records_harvested = 0
2459
+ page_nr = 1
2460
+ # And now start the real harvesting.
2461
+ while records_harvested <= max_recs_to_harvest:
2462
+ if request_type == 'get':
2463
+ url_page = url + '&page=' + str(page_nr)
2464
+ response = requests.get(url=url_page, headers=headers)
2465
+ else:
2466
+ if max_recs_to_harvest - records_harvested <= chunksize:
2467
+ # We have to harvest the last few (< chunksize).
2468
+ body['size'] = max_recs_to_harvest - records_harvested
2469
+ body['offset'] = records_harvested
2470
+ response = requests.post(url=url, headers=headers, json=body)
2471
+
2472
+ if response.status_code != requests.codes.ok:
2473
+ print('harvest_json(): error during harvest, possibly '
2474
+ + 'a missing API-key or mixed up POST body?')
2475
+ print('Status code: ' + str(response.status_code))
2476
+ print('Url: ' + response.url)
2477
+ print('Error: ' + response.text)
2478
+ exit(1)
2479
+
2480
+ chunk_json_data = response.json()
2481
+ if request_type == 'get':
2482
+ if 'results' not in chunk_json_data:
2483
+ print('harvest_json(): Error: malformed json, "results" is missing.')
2484
+ return []
2485
+ if len(chunk_json_data['results']) == 0:
2486
+ break
2487
+ json_items = chunk_json_data['results']
2488
+ else:
2489
+ if 'items' not in chunk_json_data:
2490
+ print('harvest_json(): Error: malformed json, "items" is missing.')
2491
+ return []
2492
+ if len(chunk_json_data['items']) == 0:
2493
+ break
2494
+ json_items = chunk_json_data['items']
2495
+
2496
+ json_data += json_items
2497
+ print(records_harvested, ' ', end='', flush=True)
2498
+ if page_nr % 10 == 0:
2499
+ if page_nr != 0:
2500
+ print('(' + timestamp() + ')\n', end='', flush=True)
2501
+ records_harvested += chunksize
2502
+ page_nr += 1
2503
+
2504
+ print(' Done at ' + timestamp() + '.\n')
2505
+ return json_data
2506
+
2507
+
2508
+ def harvest_json_and_write_to_file(filename: str, url: str, headers: dict, body: dict = None,
2509
+ max_recs_to_harvest: int = 0, chunksize: int = 0) -> list:
2510
+ """Harvest json data and write the data found to a file.
2511
+ This data is a list of records in json format. If no records are harvested, nothing is written.
2512
+
2513
+ :param filename: filename of the file to use for writing.
2514
+ :param url: URL to harvest.
2515
+ :param headers: headers required.
2516
+ :param body: the body of a POST request, or '' for a GET request.
2517
+ :param max_recs_to_harvest: maximum records to harvest.
2518
+ :param chunksize: chunk size to use (i.e. the number of records harvested in one call to 'url').
2519
+ :return: list of records in json format, or empty list if nothing found.
2520
+ """
2521
+ if body is None:
2522
+ body = []
2523
+
2524
+ json_data = harvest_json(url=url,
2525
+ headers=headers,
2526
+ body=body,
2527
+ max_recs_to_harvest=max_recs_to_harvest,
2528
+ chunksize=chunksize)
2529
+ if len(json_data) == 0:
2530
+ return []
2531
+ write_json_to_file(json_data=json_data, filename=filename)
2532
+ return json_data
2533
+
2534
+
2535
+ def write_json_to_file(json_data: list, filename: str) -> None:
2536
+ """Write json data to a file.
2537
+ If no records are harvested, nothing is written.
2538
+
2539
+ :param json_data: a list of records in json format.
2540
+ :param filename: filename of the file to use for writing.
2541
+ :return: None.
2542
+ """
2543
+ print('Saving json data to ' + filename + '... ', end='', flush=True)
2544
+ with open(filename, 'w') as fd:
2545
+ json.dump(obj=json_data, fp=fd, ensure_ascii=False, indent=2)
2546
+ print('Done.')
2547
+ return
2548
+
2549
+
2550
+ def read_json_from_file(filename: str) -> list:
2551
+ """Read json data from a file.
2552
+
2553
+ :param filename: filename of the file to use for writing.
2554
+ :return: list of records in json format, or empty list if nothing found.
2555
+ """
2556
+ print('Reading json data from ' + filename + '... ', end='', flush=True)
2557
+ with open(filename) as fd:
2558
+ json_data = json.load(fp=fd)
2559
+ print('Done.')
2560
+ if json_data is None:
2561
+ return []
2562
+ return json_data
2563
+
2564
+
2565
+ def read_dataframe_from_csv(filename: str, columns: dict = None,
2566
+ nr_rows: int = None, datatype=None) -> pandas.DataFrame:
2567
+ """Reads a datastructure (DataFrame) from file
2568
+
2569
+ :param filename: csv file to read.
2570
+ :param columns: which columns to read.
2571
+ :param nr_rows: how many rows to read (default: all).
2572
+ :param datatype: type of (some) columns to read.
2573
+ :return: dataframe read.
2574
+ """
2575
+ try:
2576
+ # Some inputfiles have problems reading in utf-8
2577
+ csv_data = pandas.read_csv(filename,
2578
+ sep=',',
2579
+ usecols=columns,
2580
+ dtype=datatype,
2581
+ engine='python',
2582
+ nrows=nr_rows,
2583
+ parse_dates=False,
2584
+ iterator=False,
2585
+ quotechar='"',
2586
+ encoding='utf-8')
2587
+ except BaseException:
2588
+ print('read_dataframe_from_csv(): error reading in utf-8 format, reading in latin-1 format.')
2589
+ csv_data = pandas.read_csv(filename,
2590
+ sep=',',
2591
+ usecols=columns,
2592
+ dtype=datatype,
2593
+ engine='python',
2594
+ nrows=nr_rows,
2595
+ parse_dates=False,
2596
+ iterator=False,
2597
+ quotechar='"',
2598
+ encoding='latin-1')
2599
+ return csv_data
2600
+
2601
+
2602
+ def write_dataframe_to_csv(filename: str, df: pandas.DataFrame) -> None:
2603
+ """Write a datastructure (DataFrame) to file.
2604
+
2605
+ :param filename: csv file to write.
2606
+ :param df: dataframe to write.
2607
+ :return: None.
2608
+ """
2609
+ print('\nWriting to csv file: ' + filename + '... ', end='', flush=True)
2610
+ df.to_csv(filename,
2611
+ sep=',',
2612
+ quotechar='"',
2613
+ quoting=csv.QUOTE_ALL,
2614
+ encoding='utf-8',
2615
+ index=False)
2616
+ print('Done.')
2617
+ return
2618
+
2619
+
2620
+ def convert_nodes_to_list_of_dict(nodes_list: list,
2621
+ max_nr_items: str = '0') -> list:
2622
+ """Convert a list of nodes to a list of dict.
2623
+
2624
+ :param nodes_list: The list of nodes.
2625
+ :param max_nr_items: The maximum number of items to return.
2626
+ :return: A list of dicts
2627
+ """
2628
+ if max_nr_items == '0':
2629
+ max_nr_nodes = A_LARGE_NUMBER
2630
+ else:
2631
+ max_nr_nodes = int(max_nr_items)
2632
+
2633
+ result_list = []
2634
+ count = 0
2635
+ for node in nodes_list:
2636
+ count += 1
2637
+ if count > max_nr_nodes:
2638
+ break
2639
+ result = {}
2640
+ for item in node:
2641
+ result[item] = node[item]
2642
+ result_list.append(result)
2643
+ return result_list
2644
+
2645
+
2646
+ def create_http_response(result_list: list = None,
2647
+ message: str = '',
2648
+ http_status: int = HTTP_RESPONSE_OK) -> Tuple[dict, int]:
2649
+ """Create an HTTP response.
2650
+
2651
+ :param result_list: A list of dicts, to be put in the 'result' section of
2652
+ the response.
2653
+ :param message: An optional message to be put in the 'meta' section of
2654
+ the response.
2655
+ :param http_status: The HTTP status code to be put in the 'meta' section of
2656
+ the response.
2657
+ :return: An HTTP response (as dict, to be translated to json)
2658
+ and an HTTP response code.
2659
+ """
2660
+ if result_list is None:
2661
+ result_list = []
2662
+
2663
+ meta = {'count': len(result_list),
2664
+ 'page': 1, # More pages not implemented yet.
2665
+ 'per_page': len(result_list), # Should be page length, not implemented yet.
2666
+ 'status': http_status}
2667
+ if message != '':
2668
+ meta['message'] = message
2669
+
2670
+ response = {'meta': meta,
2671
+ 'results': result_list}
2672
+ return response, http_status
2673
+
2674
+
2675
+ def print_commandline_arguments(argument_list: list) -> None:
2676
+ """Print the script name and all command line arguments.
2677
+
2678
+ :param argument_list: the argument list.
2679
+ :return: None.
2680
+ """
2681
+ if len(argument_list) == 0:
2682
+ print('print_commandline_arguments(): Error, empty argument_list passed, exiting.')
2683
+ exit(1)
2684
+
2685
+ print('\nScript name: ' + argument_list[0])
2686
+ if len(argument_list) == 1:
2687
+ print('Command line arguments: [none]')
2688
+ else:
2689
+ print('Command line arguments: '
2690
+ + ' '.join([str(arg) for arg in argument_list[1:]])
2691
+ + '\n')
2692
+ return
2693
+
2694
+
2695
+ def get_commandline_argument(argument: str, argument_list: list) -> str:
2696
+ """Get the value of a command line argument
2697
+
2698
+ :param argument: the command line argument name.
2699
+ :param argument_list: the argument list.
2700
+ :return: the value that belongs to 'argument'.
2701
+ """
2702
+ length = len(argument_list)
2703
+ if length == 0:
2704
+ print('get_commandline_argument(): Error, empty argument_list passed, exiting.')
2705
+ exit(1)
2706
+
2707
+ if str(argument) == '':
2708
+ # No argument passed.
2709
+ return ''
2710
+
2711
+ if length == 1:
2712
+ # The argument list contains the script name only.
2713
+ return ''
2714
+
2715
+ for i in range(1, length - 1):
2716
+ if str(argument_list[i]) == str(argument):
2717
+ if i + 1 <= length:
2718
+ # Only get the next index if we are still in the array bounds.
2719
+ return str(argument_list[i + 1])
2720
+ return ''
2721
+
2722
+
2723
+ # ############################################
2724
+ # ################### main ###################
2725
+ # ############################################
2726
+ # This will be executed on module initialization
2727
+ config = configparser.ConfigParser()
2728
+ config.read(get_ricgraph_ini_file())
2729
+ try:
2730
+ RICGRAPH_PROPERTIES_STANDARD = tuple(config['Ricgraph']['ricgraph_properties_standard'].split(','))
2731
+ RICGRAPH_PROPERTIES_HIDDEN = tuple(config['Ricgraph']['ricgraph_properties_hidden'].split(','))
2732
+ RICGRAPH_PROPERTIES_ADDITIONAL = tuple(config['Ricgraph']['ricgraph_properties_additional'].split(','))
2733
+ if len(RICGRAPH_PROPERTIES_STANDARD) == 0 \
2734
+ or len(RICGRAPH_PROPERTIES_HIDDEN) == 0 \
2735
+ or len(RICGRAPH_PROPERTIES_ADDITIONAL) == 0:
2736
+ print('Ricgraph initialization: error, ricgraph_properties_standard and/or')
2737
+ print(' ricgraph_properties_hidden and/or ricgraph_properties_additional')
2738
+ print(' are empty in Ricgraph ini file, exiting.')
2739
+ exit(1)
2740
+
2741
+ if RICGRAPH_PROPERTIES_STANDARD[0] == '' \
2742
+ or RICGRAPH_PROPERTIES_HIDDEN[0] == '' \
2743
+ or RICGRAPH_PROPERTIES_ADDITIONAL[0] == '':
2744
+ print('Ricgraph initialization: error, ricgraph_properties_standard and/or')
2745
+ print(' ricgraph_properties_hidden and/or ricgraph_properties_additional')
2746
+ print(' are empty in Ricgraph ini file, exiting.')
2747
+ exit(1)
2748
+
2749
+ # For more explanation, see file docs/ricgraph_install_configure.md,
2750
+ # section RICGRAPH_NODEADD_MODE.
2751
+ RICGRAPH_NODEADD_MODE = config['Ricgraph']['ricgraph_nodeadd_mode']
2752
+ if RICGRAPH_NODEADD_MODE != 'strict' and RICGRAPH_NODEADD_MODE != 'lenient':
2753
+ print('Ricgraph initialization: error, unknown value "' + RICGRAPH_NODEADD_MODE
2754
+ + '" for ricgraph_nodeadd_mode in Ricgraph ini file, exiting.')
2755
+ exit(1)
2756
+ else:
2757
+ print('Ricgraph is using "' + RICGRAPH_NODEADD_MODE + '" for ricgraph_nodeadd_mode.')
2758
+ except KeyError:
2759
+ print('Ricgraph initialization: error, ricgraph_properties_standard and/or')
2760
+ print(' ricgraph_properties_hidden and/or ricgraph_properties_additional and/or ricgraph_nodeadd_mode')
2761
+ print(' not found in Ricgraph ini file, exiting.')
2762
+ exit(1)
2763
+
2764
+ try:
2765
+ GRAPHDB = config['GraphDB']['graphdb']
2766
+ GRAPHDB_HOSTNAME = config['GraphDB']['graphdb_hostname']
2767
+ GRAPHDB_DATABASENAME = config['GraphDB']['graphdb_databasename']
2768
+ GRAPHDB_USER = config['GraphDB']['graphdb_user']
2769
+ GRAPHDB_PASSWORD = config['GraphDB']['graphdb_password']
2770
+ GRAPHDB_SCHEME = config['GraphDB']['graphdb_scheme']
2771
+ GRAPHDB_PORT = config['GraphDB']['graphdb_port']
2772
+ if GRAPHDB == '' or GRAPHDB_HOSTNAME == '' or GRAPHDB_DATABASENAME == '' \
2773
+ or GRAPHDB_USER == '' \
2774
+ or GRAPHDB_PASSWORD == '' or GRAPHDB_SCHEME == '' or GRAPHDB_PORT == '':
2775
+ print('Ricgraph initialization: error, one or more of the GraphDB parameters '
2776
+ + 'empty in Ricgraph ini file, exiting.')
2777
+ exit(1)
2778
+ GRAPHDB_URL = '{scheme}://{hostname}:{port}'.format(scheme=GRAPHDB_SCHEME,
2779
+ hostname=GRAPHDB_HOSTNAME,
2780
+ port=GRAPHDB_PORT)
2781
+ except KeyError:
2782
+ print('Ricgraph initialization: error, one or more of GraphDB parameters '
2783
+ + 'not found in Ricgraph ini file, exiting.')
2784
+ exit(1)
2785
+
2786
+ # Make sure DataFrames are printed with all columns on full width
2787
+ pandas.set_option('display.max_columns', None)
2788
+ pandas.set_option('display.width', 500)