chemrecon 0.1.1__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.
Files changed (86) hide show
  1. chemrecon/__init__.py +73 -0
  2. chemrecon/chem/__init__.py +0 -0
  3. chemrecon/chem/chemreaction.py +223 -0
  4. chemrecon/chem/constant_compounds.py +3 -0
  5. chemrecon/chem/create_mol.py +91 -0
  6. chemrecon/chem/elements.py +141 -0
  7. chemrecon/chem/gml/__init__.py +0 -0
  8. chemrecon/chem/gml/gml.py +324 -0
  9. chemrecon/chem/gml/gml_reactant_matching.py +130 -0
  10. chemrecon/chem/gml/gml_to_rdk.py +217 -0
  11. chemrecon/chem/mol.py +483 -0
  12. chemrecon/chem/sumformula.py +120 -0
  13. chemrecon/connection.py +97 -0
  14. chemrecon/core/__init__.py +0 -0
  15. chemrecon/core/id_types.py +687 -0
  16. chemrecon/core/ontology.py +209 -0
  17. chemrecon/core/populate_query_handler.py +336 -0
  18. chemrecon/core/query_handler.py +587 -0
  19. chemrecon/database/__init__.py +1 -0
  20. chemrecon/database/connect.py +63 -0
  21. chemrecon/database/connection_params/chemrecon_pub.dbinfo +5 -0
  22. chemrecon/database/connection_params/local_docker_dev.dbinfo +5 -0
  23. chemrecon/database/connection_params/local_docker_init.dbinfo +5 -0
  24. chemrecon/database/connection_params/local_docker_pub.dbinfo +5 -0
  25. chemrecon/database/params.py +88 -0
  26. chemrecon/entrygraph/draw.py +119 -0
  27. chemrecon/entrygraph/entrygraph.py +301 -0
  28. chemrecon/entrygraph/explorationprotocol.py +199 -0
  29. chemrecon/entrygraph/explore.py +421 -0
  30. chemrecon/entrygraph/explore_procedure.py +183 -0
  31. chemrecon/entrygraph/filter.py +88 -0
  32. chemrecon/entrygraph/scoring.py +141 -0
  33. chemrecon/query/__init__.py +26 -0
  34. chemrecon/query/create_entry.py +86 -0
  35. chemrecon/query/default_protocols.py +57 -0
  36. chemrecon/query/find_entry.py +84 -0
  37. chemrecon/query/get_relations.py +143 -0
  38. chemrecon/query/get_structures_from_compound.py +65 -0
  39. chemrecon/schema/__init__.py +86 -0
  40. chemrecon/schema/db_object.py +363 -0
  41. chemrecon/schema/direction.py +10 -0
  42. chemrecon/schema/entry_types/__init__.py +0 -0
  43. chemrecon/schema/entry_types/aam.py +34 -0
  44. chemrecon/schema/entry_types/aam_repr.py +37 -0
  45. chemrecon/schema/entry_types/compound.py +52 -0
  46. chemrecon/schema/entry_types/enzyme.py +49 -0
  47. chemrecon/schema/entry_types/molstructure.py +64 -0
  48. chemrecon/schema/entry_types/molstructure_repr.py +41 -0
  49. chemrecon/schema/entry_types/reaction.py +57 -0
  50. chemrecon/schema/enums.py +154 -0
  51. chemrecon/schema/procedural_relation_entrygraph.py +66 -0
  52. chemrecon/schema/relation_types_composed/__init__.py +0 -0
  53. chemrecon/schema/relation_types_composed/compound_has_molstructure_relation.py +59 -0
  54. chemrecon/schema/relation_types_composed/reaction_has_aam_relation.py +50 -0
  55. chemrecon/schema/relation_types_procedural/__init__.py +0 -0
  56. chemrecon/schema/relation_types_procedural/aam_convert_relation.py +69 -0
  57. chemrecon/schema/relation_types_procedural/compound_select_structure_proceduralrelation.py +36 -0
  58. chemrecon/schema/relation_types_procedural/compound_similarlity_proceduralrelation.py +1 -0
  59. chemrecon/schema/relation_types_procedural/molstructure_convert_relation.py +49 -0
  60. chemrecon/schema/relation_types_procedural/reaction_select_aam_proceduralrelation.py +38 -0
  61. chemrecon/schema/relation_types_procedural/reaction_similarity_proceduralrelation.py +1 -0
  62. chemrecon/schema/relation_types_source/__init__.py +0 -0
  63. chemrecon/schema/relation_types_source/aam_involves_molstructure_relation.py +77 -0
  64. chemrecon/schema/relation_types_source/aam_repr_involves_molstructure_repr_relation.py +79 -0
  65. chemrecon/schema/relation_types_source/compound_has_structure_representation_relation.py +33 -0
  66. chemrecon/schema/relation_types_source/compound_reference_relation.py +34 -0
  67. chemrecon/schema/relation_types_source/molstructure_standardisation_relation.py +71 -0
  68. chemrecon/schema/relation_types_source/ontology/__init__.py +0 -0
  69. chemrecon/schema/relation_types_source/ontology/compound_ontology.py +369 -0
  70. chemrecon/schema/relation_types_source/ontology/enzyme_ontology.py +142 -0
  71. chemrecon/schema/relation_types_source/ontology/reaction_ontology.py +140 -0
  72. chemrecon/schema/relation_types_source/reaction_has_aam_representation_relation.py +34 -0
  73. chemrecon/schema/relation_types_source/reaction_has_enzyme_relation.py +71 -0
  74. chemrecon/schema/relation_types_source/reaction_involves_compound_relation.py +69 -0
  75. chemrecon/schema/relation_types_source/reaction_reference_relation.py +33 -0
  76. chemrecon/scripts/initialize_database.py +494 -0
  77. chemrecon/utils/copy_signature.py +10 -0
  78. chemrecon/utils/encodeable_list.py +11 -0
  79. chemrecon/utils/get_id_type.py +70 -0
  80. chemrecon/utils/hungarian.py +31 -0
  81. chemrecon/utils/reactant_matching.py +168 -0
  82. chemrecon/utils/rxnutils.py +44 -0
  83. chemrecon/utils/set_cwd.py +12 -0
  84. chemrecon-0.1.1.dist-info/METADATA +143 -0
  85. chemrecon-0.1.1.dist-info/RECORD +86 -0
  86. chemrecon-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,209 @@
1
+ """ Defines the ontology entrytypes
2
+ """
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional
7
+
8
+
9
+ # Define the base class and possible properties of the ontology relations
10
+ # ----------------------------------------------------------------------------------------------------------------------
11
+ class OntologyType:
12
+ name: str
13
+ description: str
14
+ synonyms: dict[str, str] # For each source, what this relationship is called
15
+
16
+ # Applies to compounds / reactions
17
+ for_comp: bool
18
+ for_reac: bool
19
+ for_enzy: bool
20
+
21
+ # Relational properties
22
+ symmetric: bool
23
+ transitive: bool
24
+ inverse: Optional[OntologyType]
25
+ # If there is a corresponding inverse relation. Not applicable if symmetric
26
+ is_primary: bool # If relation has an inverse, is_primary is true if this relation is shown
27
+ # If no primaries, then both are shown with a dash /
28
+ # Drawing
29
+ draw: str # 'full' or 'arrow'
30
+
31
+ enum_type: 'Ontology'
32
+
33
+ def __init__(
34
+ self,
35
+ name: str,
36
+ description: str,
37
+ for_comp: bool,
38
+ for_reac: bool,
39
+ for_enzy: bool,
40
+ symmetric: bool,
41
+ transitive: bool = False,
42
+ inverse: OntologyType = None,
43
+ is_primary: bool = False,
44
+ synonyms: dict[str, str] = None,
45
+ draw = 'full'
46
+ ):
47
+ self.name = name
48
+ self.description = description
49
+ self.for_comp = for_comp
50
+ self.for_reac = for_reac
51
+ self.for_enzy = for_enzy
52
+ self.symmetric = symmetric
53
+ self.transitive = transitive
54
+ self.inverse = inverse
55
+ self.is_primary = is_primary
56
+ self.synonyms = synonyms if synonyms else {}
57
+ self.draw = draw
58
+
59
+
60
+
61
+ # Define the ontologies
62
+ # ----------------------------------------------------------------------------------------------------------------------
63
+
64
+ IS_A = OntologyType(
65
+ name = 'Is a',
66
+ description = 'Defines a subclass relation.',
67
+ for_comp = True,
68
+ for_reac = True,
69
+ for_enzy = True,
70
+ symmetric = False,
71
+ synonyms = {
72
+ 'chebi': 'is_a'
73
+ }
74
+ )
75
+
76
+ HAS_INSTANCE = OntologyType(
77
+ name = 'Has instance',
78
+ description = 'Reverse of is_a relation',
79
+ for_comp = True,
80
+ for_reac = True,
81
+ for_enzy = True,
82
+ symmetric = False,
83
+ )
84
+ IS_A.inverse = HAS_INSTANCE
85
+ HAS_INSTANCE.inverse = IS_A
86
+ IS_A.is_primary = True
87
+
88
+ HAS_PART = OntologyType(
89
+ name = 'Has part',
90
+ description = 'TODO',
91
+ for_comp = True,
92
+ for_reac = False,
93
+ for_enzy = False,
94
+ symmetric = False,
95
+ synonyms = {
96
+ 'chebi': 'has_part'
97
+ }
98
+ )
99
+
100
+ TAUTOMER = OntologyType(
101
+ name = 'Tautomer',
102
+ description = 'TODO',
103
+ for_comp = True,
104
+ for_reac = False,
105
+ for_enzy = False,
106
+ symmetric = True,
107
+ transitive = True,
108
+ synonyms = {
109
+ 'chebi': 'is_tautomer_of'
110
+ }
111
+ )
112
+
113
+ CONJUCATE_ACID = OntologyType(
114
+ name = 'Conjugate acid',
115
+ description = 'TODO',
116
+ for_comp = True,
117
+ for_reac = False,
118
+ for_enzy = False,
119
+ symmetric = False,
120
+ transitive = False,
121
+ synonyms = {
122
+ 'chebi': 'is_conjugate_acid_of'
123
+ }
124
+ )
125
+
126
+ CONJUGATE_BASE = OntologyType(
127
+ name = 'Conjugate base',
128
+ description = 'TODO',
129
+ for_comp = True,
130
+ for_reac = False,
131
+ for_enzy = False,
132
+ symmetric = False,
133
+ transitive = False,
134
+ synonyms = {
135
+ 'chebi': 'is_conjugate_base_of'
136
+ }
137
+ )
138
+ CONJUCATE_ACID.inverse = CONJUGATE_BASE
139
+ CONJUGATE_BASE.inverse = CONJUCATE_ACID
140
+
141
+ STEREOISOMER = OntologyType(
142
+ name = 'Stereoisomer',
143
+ description = 'TODO',
144
+ for_comp = True,
145
+ for_reac = False,
146
+ for_enzy = False,
147
+ symmetric = True,
148
+ transitive = True,
149
+ synonyms = {}
150
+ )
151
+
152
+ ENANTIOMER = OntologyType(
153
+ name = 'Enantiomer',
154
+ description = 'TODO',
155
+ for_comp = True,
156
+ for_reac = False,
157
+ for_enzy = False,
158
+ symmetric = True,
159
+ transitive = True,
160
+ synonyms = {
161
+ 'chebi': 'is_enantiomer_of'
162
+ }
163
+ )
164
+
165
+ HAS_NEW_ID = OntologyType(
166
+ name = 'New ID',
167
+ description = 'Is deprecated ID of',
168
+ for_comp = True,
169
+ for_reac = True,
170
+ for_enzy = True,
171
+ symmetric = False,
172
+ transitive = True,
173
+ synonyms = {},
174
+ draw = 'arrow'
175
+ )
176
+
177
+ HAS_OLD_ID = OntologyType(
178
+ name = 'Old ID',
179
+ description = 'Has deprecated ID',
180
+ for_comp = True,
181
+ for_reac = True,
182
+ for_enzy = True,
183
+ symmetric = False,
184
+ transitive = True,
185
+ synonyms = {},
186
+ draw = 'arrow'
187
+ )
188
+ HAS_NEW_ID.inverse = HAS_OLD_ID
189
+ HAS_OLD_ID.inverse = HAS_NEW_ID
190
+ HAS_NEW_ID.is_primary = True
191
+
192
+
193
+ # Make list of all ontology relations
194
+ # ----------------------------------------------------------------------------------------------------------------------
195
+ ontology_relations: list[OntologyType] = [
196
+ IS_A, HAS_INSTANCE, HAS_PART, TAUTOMER, CONJUCATE_ACID, CONJUGATE_BASE, ENANTIOMER, HAS_NEW_ID, HAS_OLD_ID
197
+ ]
198
+
199
+ ont_str_dict: dict[str, OntologyType] = dict()
200
+ for rel in ontology_relations:
201
+ ont_str_dict[rel.name] = rel
202
+ for syn in rel.synonyms.values():
203
+ ont_str_dict[syn] = rel
204
+
205
+ def get_relation_type_from_str(s: str) -> OntologyType | None:
206
+ try:
207
+ return ont_str_dict[s]
208
+ except KeyError:
209
+ return None
@@ -0,0 +1,336 @@
1
+ from typing import Optional
2
+
3
+ import psycopg as pg
4
+ import psycopg.sql as sql
5
+
6
+ from chemrecon.schema.db_object import Entry, Relation
7
+ import chemrecon.core.query_handler
8
+
9
+ class PopulateQueryHandler(chemrecon.core.query_handler.QueryHandler):
10
+ """ Entries and relations are added to a PopulationQueryHandler, which batches operations and
11
+ sends them to the database.
12
+ When added, the handler will assign a correct ReconID to the object based on the current state of the
13
+ corresponding sequence.
14
+ Should be used as a context.
15
+ """
16
+
17
+ # Adding entries to the handler
18
+ # ----------------------------------------------------------------------------------------------------------
19
+ # For adding entries when the assigned reconid is needed. This cannot be batched.
20
+ def add_entry[T: Entry](
21
+ self,
22
+ entry: T,
23
+ returning: bool = True,
24
+ authoritative: bool = False
25
+ ) -> Optional[int]:
26
+ """ Add an entry to the DB, returning its reconid immediately.
27
+ If authoritative, allow overriding fields added previously.
28
+ If not authoritative, do nothing on conflict.
29
+ """
30
+ if returning:
31
+ return self.add_entries([entry], returning = returning, authoritative = authoritative)[0]
32
+ else:
33
+ self.add_entries([entry], returning = returning, authoritative = authoritative)
34
+ return None
35
+
36
+ def add_entries[T: Entry](
37
+ self,
38
+ entries: list[T],
39
+ returning: bool = True,
40
+ authoritative: bool = False
41
+ ) -> Optional[list[int]]:
42
+ """ TODO
43
+ """
44
+ entrytype = type(entries[0])
45
+ cols = entrytype.get_columns(include_recon_id = False)
46
+ index_cols = entrytype.get_index_columns()
47
+ table_identifier = sql.Identifier(entrytype.get_table_name())
48
+
49
+ # TODO 'returning' clause can be removed if not returning?
50
+ q = sql.SQL("""
51
+ INSERT INTO {entrytypetable} ({columns})
52
+ VALUES ({placeholders})
53
+ ON CONFLICT ({index_columns})
54
+ DO UPDATE SET ({columns}) = ROW({coalesced_values})
55
+ RETURNING {reconid_col};
56
+ """).format(
57
+ entrytypetable = table_identifier,
58
+ reconid_col = sql.Identifier('recon_id'),
59
+ columns = sql.SQL(',').join(
60
+ sql.Identifier(c.name) for c in cols
61
+ ),
62
+ index_name = sql.Identifier(f'{entrytype.get_table_name()}_index'),
63
+ index_columns = sql.SQL(',').join( # TODO better way with named constraint?
64
+ sql.Identifier(c.name) if not c.index_hash
65
+ else sql.SQL('md5({colname})').format(colname = sql.Identifier(c.name))
66
+ for c in index_cols
67
+ ),
68
+ placeholders = sql.SQL(', ').join([sql.SQL('%s') for _ in cols]),
69
+ coalesced_values = sql.SQL(',').join(
70
+ # Coalesce values, preferring existing entry
71
+ sql.SQL('COALESCE(excluded.{col}, {table}.{col})').format(
72
+ col = sql.Identifier(c.name),
73
+ table = table_identifier
74
+ )
75
+ for c in cols
76
+ )
77
+ )
78
+
79
+ # Execute
80
+ try:
81
+ self.c.executemany(
82
+ q,
83
+ params_seq = [
84
+ list(e.get_columns_with_values(include_recon_id = False).values())
85
+ for e in entries
86
+ ],
87
+ returning = returning
88
+ )
89
+
90
+ # Return as dict
91
+ if returning:
92
+ return [i[0] for i in self._extract_results_from_cursor()]
93
+ else:
94
+ return None
95
+ except pg.errors.UniqueViolation as e:
96
+ raise KeyError(f'Unique violation: {e}\n\t{e}')
97
+ except pg.errors.SyntaxError as e:
98
+ print(f'ERROR:\n{q.as_string()}\n\t{e}')
99
+ raise RuntimeError
100
+
101
+ def add_relation[T1: Entry, T2: Entry](self, relation: Relation[T1, T2]):
102
+ """ When adding a relation, it is expected that the 'entry_1' and 'entry_2' fields are filled such that
103
+ the appropriate key can be looked up.
104
+ """
105
+ self.add_relations([relation])
106
+
107
+ def add_relations[T1: Entry, T2: Entry](self, relations: list[Relation[T1, T2]]):
108
+ # TODO dispatch to add by reconid or by entries, depending
109
+ # TODO change to the plural version, specialised for adding only one
110
+ raise NotImplementedError()
111
+
112
+ def add_relation_by_recon_ids[T1: Entry, T2: Entry](
113
+ self,
114
+ recon_id_1: int,
115
+ recon_id_2: int,
116
+ relation: Relation[T1, T2]
117
+ ):
118
+ """ Add a relation, using the recon_id fields of the given relation.
119
+ """
120
+ self.add_relations_by_recon_ids([(recon_id_1, recon_id_2, relation)])
121
+
122
+ def add_relations_by_recon_ids[T1: Entry, T2: Entry](
123
+ self,
124
+ relations: list[tuple[int, int, Relation[T1, T2]]]
125
+ ):
126
+ """ Add relations by recon_ids. """
127
+ reltype = type(relations[0][2])
128
+
129
+ # Add recon_ids to the relation, sort recon_ids of needed
130
+ if reltype.symmetric:
131
+ for r in relations:
132
+ r[2].recon_id_1, r[2].recon_id_2 = sorted((r[0], r[1]))
133
+ else:
134
+ for r in relations:
135
+ r[2].recon_id_1, r[2].recon_id_2 = r[0], r[1]
136
+
137
+ cols = reltype.get_columns()
138
+ q = sql.SQL("""
139
+ INSERT INTO {rel_table_name} ({cols})
140
+ VALUES ({placeholders})
141
+ ON CONFLICT DO NOTHING;
142
+ """).format(
143
+ rel_table_name = sql.Identifier(reltype.get_table_name()),
144
+ cols = sql.SQL(', ').join(sql.Identifier(c.name) for c in cols),
145
+ placeholders = sql.SQL(', ').join([sql.SQL('%s') for _ in cols])
146
+ )
147
+ self.c.executemany(
148
+ q,
149
+ params_seq = [
150
+ list(r[2].get_columns_with_values().values())
151
+ for r in relations
152
+ ]
153
+ )
154
+
155
+ def add_relation_by_entries[T1: Entry, T2: Entry](
156
+ self,
157
+ entry_1: T1,
158
+ entry_2: T2,
159
+ relation: Relation[T1, T2]
160
+ ):
161
+ """ Add a relation, using the entry fields of the given relation.
162
+ """
163
+ self.add_relations_by_entries([(entry_1, entry_2, relation)])
164
+
165
+ def add_relations_by_entries[T1: Entry, T2: Entry](
166
+ self,
167
+ relations: list[tuple[T1, T2, Relation[T1, T2]]]
168
+ ):
169
+ """ Add relations using the entry fields of each given relation.
170
+ """
171
+ reltype = type(relations[0][2])
172
+ all_rel_cols = reltype.get_columns()
173
+ attrcols = reltype.get_attribute_columns()
174
+ e1_table, e2_table = reltype.get_entry_table_names()
175
+ e1_index_cols = reltype.source_entrytype.get_index_columns()
176
+ e2_index_cols = reltype.target_entrytype.get_index_columns()
177
+
178
+ if reltype.symmetric:
179
+ q_ = sql.SQL("""
180
+ WITH r_id1 AS (
181
+ SELECT recon_id
182
+ FROM {entry_1_table}
183
+ WHERE ({entry_1_index_cols}) = ({entry_1_placeholders})
184
+ ), r_id2 AS (
185
+ SELECT recon_id
186
+ FROM {entry_2_table}
187
+ WHERE ({entry_2_index_cols}) = ({entry_2_placeholders})
188
+ )
189
+ INSERT INTO {rel_table_name} ({all_columns})
190
+ VALUES (
191
+ LEAST((SELECT recon_id FROM r_id1), (SELECT recon_id FROM r_id2)),
192
+ GREATEST((SELECT recon_id FROM r_id1), (SELECT recon_id FROM r_id2))
193
+ {attr_placeholders}
194
+ )
195
+ ON CONFLICT DO NOTHING;
196
+ """)
197
+ else:
198
+ q_ = sql.SQL("""
199
+ WITH r_id1 AS (
200
+ SELECT recon_id
201
+ FROM {entry_1_table}
202
+ WHERE ({entry_1_index_cols}) = ({entry_1_placeholders})
203
+ ), r_id2 AS (
204
+ SELECT recon_id
205
+ FROM {entry_2_table}
206
+ WHERE ({entry_2_index_cols}) = ({entry_2_placeholders})
207
+ )
208
+ INSERT INTO {rel_table_name} ({all_columns})
209
+ VALUES ((SELECT recon_id FROM r_id1), (SELECT recon_id FROM r_id2) {attr_placeholders})
210
+ ON CONFLICT DO NOTHING;
211
+ """)
212
+
213
+ # Prepare
214
+ q = q_.format(
215
+ # Entry 1
216
+ entry_1_table = sql.Identifier(e1_table),
217
+ entry_1_index_cols = sql.SQL(', ').join(
218
+ sql.Identifier(c.name) for c in e1_index_cols
219
+ ),
220
+ entry_1_placeholders = sql.SQL(', ').join(sql.SQL('%s') for _ in e1_index_cols),
221
+ # Entry2
222
+ entry_2_table = sql.Identifier(e2_table),
223
+ entry_2_index_cols = sql.SQL(', ').join(
224
+ sql.Identifier(c.name) for c in e2_index_cols
225
+ ),
226
+ entry_2_placeholders = sql.SQL(', ').join(sql.SQL('%s') for _ in e2_index_cols),
227
+ # Attributes
228
+ rel_table_name = sql.Identifier(reltype.get_table_name()),
229
+ all_columns = sql.SQL(', ').join(
230
+ sql.Identifier(c.name) for c in all_rel_cols
231
+ ),
232
+ attr_placeholders = (
233
+ sql.SQL(', ') + sql.SQL(', ').join(sql.SQL('%s') for _ in attrcols)
234
+ ) if len(attrcols) > 0 else sql.SQL('')
235
+ )
236
+
237
+ # Execute
238
+ self.c.executemany(
239
+ q,
240
+ params_seq = [
241
+ [
242
+ *e1.get_index_columns_with_values().values(),
243
+ *e2.get_index_columns_with_values().values(),
244
+ *r.get_columns_with_values(include_recon_ids = False).values()
245
+ ]
246
+ for e1, e2, r in relations
247
+ ],
248
+ returning = False
249
+ )
250
+
251
+ # Connected adders
252
+ def add_entry_with_relations[T1: Entry, T2: Entry](
253
+ self, entry: T1, relations: list[tuple[Relation[T1, T2], T2]]
254
+ ) -> int:
255
+ """ An entry and a number of relations to possibly new entries with the first entry as the source.
256
+ Relations do not need to have their recon_id_1, recon_id_2, or entries filled.
257
+ Returns id of source entry
258
+ """
259
+ # Add the T1 entry
260
+ e_recon_id = self.add_entry(entry, returning = True)
261
+
262
+ if len(relations) == 0:
263
+ # Short circuit if no relations given
264
+ return e_recon_id
265
+
266
+ rels, t2_entries = zip(*relations)
267
+ rel_type = type(rels[0])
268
+ t2_type = type(t2_entries[0])
269
+
270
+ # Add relations and secondary entries
271
+ self.add_relations_to_entry_with_reconid(
272
+ recon_id = e_recon_id,
273
+ entry_table = type(entry),
274
+ relations = relations
275
+ )
276
+
277
+ # Return id of added/found source entry
278
+ return e_recon_id
279
+
280
+
281
+ def add_relations_to_entry_with_reconid[T1: Entry, T2: Entry](
282
+ self,
283
+ recon_id: int,
284
+ entry_table: type[Entry],
285
+ relations: list[tuple[Relation[T1, T2], T2]] | list[tuple[Relation[T2, T1], T2]]
286
+ ) -> list[int]:
287
+ """ Add a number of relations from the given reconid.
288
+ Returns a list of assigned Recon IDs in the same order as the input.
289
+ """
290
+ if len(relations) == 0:
291
+ return []
292
+
293
+ # Gather types
294
+ rels, t2_entries = zip(*relations)
295
+ rel_type = type(rels[0])
296
+ t2_type = type(t2_entries[0])
297
+
298
+ # Add all T2 entries
299
+ t2_ids: list[int] = self.add_entries(t2_entries, returning = True, authoritative = False)
300
+ if len(t2_ids) != len(relations):
301
+ raise ValueError
302
+
303
+ # Dispatch, setting recon_ids of added relations depending on type
304
+ if rel_type.symmetric:
305
+ # T2 = T1, Relation[T1, T1]
306
+ for t2_id, rel in zip(t2_ids, rels):
307
+ rel: Relation[T1, T1]
308
+ rel.recon_id_1 = recon_id
309
+ rel.recon_id_2 = t2_id
310
+ rel.recon_id_1, rel.recon_id_2 = sorted((rel.recon_id_1, rel.recon_id_2)) # Keep invariant
311
+
312
+ elif rel_type.source_entrytype is entry_table:
313
+ # Relation[T1, T2]
314
+ for t2_id, rel in zip(t2_ids, rels):
315
+ rel: Relation[T1, T2]
316
+ rel.recon_id_1, rel.recon_id_2 = (recon_id, t2_id)
317
+
318
+ elif rel_type.target_entrytype is entry_table:
319
+ # Relation[T2, T1]
320
+ for t2_id, rel in zip(t2_ids, rels):
321
+ rel: Relation[T2, T1]
322
+ rel.recon_id_1, rel.recon_id_2 = (t2_id, recon_id)
323
+
324
+ else:
325
+ raise ValueError()
326
+
327
+ # Add relations by recon ids
328
+ self.add_relations_by_recon_ids(
329
+ [(rel.recon_id_1, rel.recon_id_2, rel) for rel in rels]
330
+ )
331
+
332
+ # Return recon_ids assigned by db
333
+ return t2_ids
334
+
335
+ # Utility functions
336
+ # ----------------------------------------------------------------------------------------------------------