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.
- chemrecon/__init__.py +73 -0
- chemrecon/chem/__init__.py +0 -0
- chemrecon/chem/chemreaction.py +223 -0
- chemrecon/chem/constant_compounds.py +3 -0
- chemrecon/chem/create_mol.py +91 -0
- chemrecon/chem/elements.py +141 -0
- chemrecon/chem/gml/__init__.py +0 -0
- chemrecon/chem/gml/gml.py +324 -0
- chemrecon/chem/gml/gml_reactant_matching.py +130 -0
- chemrecon/chem/gml/gml_to_rdk.py +217 -0
- chemrecon/chem/mol.py +483 -0
- chemrecon/chem/sumformula.py +120 -0
- chemrecon/connection.py +97 -0
- chemrecon/core/__init__.py +0 -0
- chemrecon/core/id_types.py +687 -0
- chemrecon/core/ontology.py +209 -0
- chemrecon/core/populate_query_handler.py +336 -0
- chemrecon/core/query_handler.py +587 -0
- chemrecon/database/__init__.py +1 -0
- chemrecon/database/connect.py +63 -0
- chemrecon/database/connection_params/chemrecon_pub.dbinfo +5 -0
- chemrecon/database/connection_params/local_docker_dev.dbinfo +5 -0
- chemrecon/database/connection_params/local_docker_init.dbinfo +5 -0
- chemrecon/database/connection_params/local_docker_pub.dbinfo +5 -0
- chemrecon/database/params.py +88 -0
- chemrecon/entrygraph/draw.py +119 -0
- chemrecon/entrygraph/entrygraph.py +301 -0
- chemrecon/entrygraph/explorationprotocol.py +199 -0
- chemrecon/entrygraph/explore.py +421 -0
- chemrecon/entrygraph/explore_procedure.py +183 -0
- chemrecon/entrygraph/filter.py +88 -0
- chemrecon/entrygraph/scoring.py +141 -0
- chemrecon/query/__init__.py +26 -0
- chemrecon/query/create_entry.py +86 -0
- chemrecon/query/default_protocols.py +57 -0
- chemrecon/query/find_entry.py +84 -0
- chemrecon/query/get_relations.py +143 -0
- chemrecon/query/get_structures_from_compound.py +65 -0
- chemrecon/schema/__init__.py +86 -0
- chemrecon/schema/db_object.py +363 -0
- chemrecon/schema/direction.py +10 -0
- chemrecon/schema/entry_types/__init__.py +0 -0
- chemrecon/schema/entry_types/aam.py +34 -0
- chemrecon/schema/entry_types/aam_repr.py +37 -0
- chemrecon/schema/entry_types/compound.py +52 -0
- chemrecon/schema/entry_types/enzyme.py +49 -0
- chemrecon/schema/entry_types/molstructure.py +64 -0
- chemrecon/schema/entry_types/molstructure_repr.py +41 -0
- chemrecon/schema/entry_types/reaction.py +57 -0
- chemrecon/schema/enums.py +154 -0
- chemrecon/schema/procedural_relation_entrygraph.py +66 -0
- chemrecon/schema/relation_types_composed/__init__.py +0 -0
- chemrecon/schema/relation_types_composed/compound_has_molstructure_relation.py +59 -0
- chemrecon/schema/relation_types_composed/reaction_has_aam_relation.py +50 -0
- chemrecon/schema/relation_types_procedural/__init__.py +0 -0
- chemrecon/schema/relation_types_procedural/aam_convert_relation.py +69 -0
- chemrecon/schema/relation_types_procedural/compound_select_structure_proceduralrelation.py +36 -0
- chemrecon/schema/relation_types_procedural/compound_similarlity_proceduralrelation.py +1 -0
- chemrecon/schema/relation_types_procedural/molstructure_convert_relation.py +49 -0
- chemrecon/schema/relation_types_procedural/reaction_select_aam_proceduralrelation.py +38 -0
- chemrecon/schema/relation_types_procedural/reaction_similarity_proceduralrelation.py +1 -0
- chemrecon/schema/relation_types_source/__init__.py +0 -0
- chemrecon/schema/relation_types_source/aam_involves_molstructure_relation.py +77 -0
- chemrecon/schema/relation_types_source/aam_repr_involves_molstructure_repr_relation.py +79 -0
- chemrecon/schema/relation_types_source/compound_has_structure_representation_relation.py +33 -0
- chemrecon/schema/relation_types_source/compound_reference_relation.py +34 -0
- chemrecon/schema/relation_types_source/molstructure_standardisation_relation.py +71 -0
- chemrecon/schema/relation_types_source/ontology/__init__.py +0 -0
- chemrecon/schema/relation_types_source/ontology/compound_ontology.py +369 -0
- chemrecon/schema/relation_types_source/ontology/enzyme_ontology.py +142 -0
- chemrecon/schema/relation_types_source/ontology/reaction_ontology.py +140 -0
- chemrecon/schema/relation_types_source/reaction_has_aam_representation_relation.py +34 -0
- chemrecon/schema/relation_types_source/reaction_has_enzyme_relation.py +71 -0
- chemrecon/schema/relation_types_source/reaction_involves_compound_relation.py +69 -0
- chemrecon/schema/relation_types_source/reaction_reference_relation.py +33 -0
- chemrecon/scripts/initialize_database.py +494 -0
- chemrecon/utils/copy_signature.py +10 -0
- chemrecon/utils/encodeable_list.py +11 -0
- chemrecon/utils/get_id_type.py +70 -0
- chemrecon/utils/hungarian.py +31 -0
- chemrecon/utils/reactant_matching.py +168 -0
- chemrecon/utils/rxnutils.py +44 -0
- chemrecon/utils/set_cwd.py +12 -0
- chemrecon-0.1.1.dist-info/METADATA +143 -0
- chemrecon-0.1.1.dist-info/RECORD +86 -0
- 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
|
+
# ----------------------------------------------------------------------------------------------------------
|