chython 2.19__cp313-cp313-win_amd64.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 (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
chython/files/MRVrw.py ADDED
@@ -0,0 +1,532 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from collections import defaultdict
20
+ from itertools import count, islice, chain
21
+ from pathlib import Path
22
+ from typing import Union, List, Iterator, Dict, Optional
23
+ from xml.etree.ElementTree import iterparse, tostring
24
+ from ._convert import create_molecule, create_reaction
25
+ from ._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
26
+ from .mdl import postprocess_molecule
27
+ from ..containers import MoleculeContainer, ReactionContainer
28
+ from ..exceptions import EmptyMolecule, EmptyReaction
29
+
30
+
31
+ bond_map = {8: '1" queryType="Any', 4: 'A', 1: '1', 2: '2', 3: '3',
32
+ 'Any': 8, 'any': 8, 'A': 4, 'a': 4, '1': 1, '2': 2, '3': 3}
33
+
34
+
35
+ def _local_name(tag):
36
+ if tag[0] == '{':
37
+ return tag.split('}', 1)[1]
38
+ return tag
39
+
40
+
41
+ def xml_dict(parent_element, stop_list=None):
42
+ stop_list = set() if stop_list is None else set(stop_list)
43
+ out = {}
44
+ for x, y in parent_element.items():
45
+ y = y.strip()
46
+ if y:
47
+ x = '@%s' % x.strip()
48
+ out[x] = y
49
+
50
+ text = []
51
+ if len(parent_element):
52
+ elements_grouped = defaultdict(list)
53
+ for element in parent_element:
54
+ name = _local_name(element.tag)
55
+ if name in stop_list:
56
+ tail = element.tail
57
+ element.tail = None
58
+ text.append(tostring(element, encoding='unicode'))
59
+ element.tail = tail
60
+ else:
61
+ elements_grouped[name].append(element)
62
+
63
+ if element.tail:
64
+ t = element.tail.strip()
65
+ if t:
66
+ text.append(t)
67
+
68
+ for element_tag, element_group in elements_grouped.items():
69
+ if len(element_group) == 1:
70
+ out[element_tag] = xml_dict(element_group[0], stop_list)
71
+ else:
72
+ out[element_tag] = [xml_dict(x, stop_list) for x in element_group]
73
+
74
+ if parent_element.text:
75
+ t = parent_element.text.strip()
76
+ if t:
77
+ text.insert(0, t)
78
+ if text:
79
+ out['$'] = ''.join(text)
80
+
81
+ return out
82
+
83
+
84
+ class MRVRead:
85
+ """
86
+ ChemAxon MRV files reader. works similar to opened file object. support `with` context manager.
87
+ on initialization accept opened in binary mode file, string path to file,
88
+ pathlib.Path object or another binary buffered reader object
89
+ """
90
+ molecule_cls = MoleculeContainer
91
+ reaction_cls = ReactionContainer
92
+
93
+ def __init__(self, file, *, ignore: bool = True, remap: bool = False,
94
+ calc_cis_trans: bool = False, ignore_stereo: bool = False, ignore_bad_isotopes: bool = False):
95
+ """
96
+ :param ignore: Skip some checks of data or try to fix some errors.
97
+ :param remap: Remap atom numbers started from one.
98
+ :param calc_cis_trans: Calculate cis/trans marks from 2d coordinates.
99
+ :param ignore_stereo: Ignore stereo data.
100
+ :param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
101
+ """
102
+ if isinstance(file, str):
103
+ self.__file = open(file, 'rb')
104
+ self.__is_buffer = False
105
+ elif isinstance(file, Path):
106
+ self.__file = file.open('rb')
107
+ self.__is_buffer = False
108
+ elif hasattr(file, 'read'):
109
+ self.__file = file
110
+ self.__is_buffer = True
111
+ else:
112
+ raise TypeError('invalid file. file-like object or path to file expected')
113
+ self.__ignore = ignore
114
+ self.__remap = remap
115
+ self.__calc_cis_trans = calc_cis_trans
116
+ self.__ignore_stereo = ignore_stereo
117
+ self.__ignore_bad_isotopes = ignore_bad_isotopes
118
+ self.__tell = 0
119
+ self.__xml = iterparse(self.__file, events=('end',))
120
+ self.__buffer = None
121
+
122
+ def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
123
+ """
124
+ Parse whole file
125
+
126
+ :param amount: number of records to read
127
+ """
128
+ if amount:
129
+ return list(islice(iter(self), amount))
130
+ return list(iter(self))
131
+
132
+ def read_structure(self, *, current: bool = True):
133
+ """
134
+ Read Reaction or Molecule container.
135
+
136
+ :param current: return current structure if already parsed, otherwise read next
137
+ """
138
+ data = self._read_block(current=current)
139
+ meta = self.read_metadata()
140
+ log = []
141
+
142
+ if 'molecule' in data and isinstance(data['molecule'], dict):
143
+ data = data['molecule']
144
+ tmp = parse_molecule(data)
145
+ postprocess_parsed_molecule(tmp, remap=self.__remap, ignore=self.__ignore)
146
+ parse_sgroup(data, tmp)
147
+ mol = create_molecule(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _cls=self.molecule_cls)
148
+ if not self.__ignore_stereo:
149
+ postprocess_molecule(mol, tmp, calc_cis_trans=self.__calc_cis_trans)
150
+ if meta:
151
+ mol.meta.update(meta)
152
+ return mol
153
+ elif 'reaction' in data and isinstance(data['reaction'], dict):
154
+ data = data['reaction']
155
+ tmp = {'reactants': [], 'products': [], 'reagents': [], 'log': log, 'title': data.get('@title')}
156
+
157
+ n = 0
158
+ for tag, group in (('reactantList', 'reactants'), ('productList', 'products'), ('agentList', 'reagents')):
159
+ if tag in data and 'molecule' in data[tag]:
160
+ molecule = data[tag]['molecule']
161
+ if isinstance(molecule, dict):
162
+ molecule = (molecule,)
163
+ for m in molecule:
164
+ n += 1
165
+ try:
166
+ tmp[group].append(parse_molecule(m))
167
+ except ValueError as e:
168
+ if isinstance(e, EmptyMolecule):
169
+ log.append(f'ignored empty molecule {n}')
170
+ elif self.__ignore:
171
+ log.append(f'ignored molecule {n} with {e}')
172
+ else:
173
+ raise
174
+
175
+ if not tmp['reactants'] and not tmp['products'] and not tmp['reagents']:
176
+ raise EmptyReaction
177
+
178
+ postprocess_parsed_reaction(tmp, remap=self.__remap, ignore=self.__ignore)
179
+ rxn = create_reaction(tmp, ignore_bad_isotopes=self.__ignore_bad_isotopes, _m_cls=self.molecule_cls,
180
+ _r_cls=self.reaction_cls)
181
+ if not self.__ignore_stereo:
182
+ for mol, parsed in zip(rxn.molecules(), chain(tmp['reactants'], tmp['reagents'], tmp['products'])):
183
+ postprocess_molecule(mol, parsed, calc_cis_trans=self.__calc_cis_trans)
184
+ if meta:
185
+ rxn.meta.update(meta)
186
+ return rxn
187
+ else:
188
+ raise ValueError('reaction or molecule expected')
189
+
190
+ def read_metadata(self, *, current: bool = True) -> Dict[str, str]:
191
+ """
192
+ Read metadata block
193
+ """
194
+ data = self._read_block(current=current)
195
+ if 'molecule' in data and isinstance(data['molecule'], dict):
196
+ data = data['molecule']
197
+ elif 'reaction' in data and isinstance(data['reaction'], dict):
198
+ data = data['reaction']
199
+ else:
200
+ raise ValueError('reaction or molecule expected')
201
+
202
+ if 'propertyList' in data and 'property' in data['propertyList']:
203
+ data = data['propertyList']['property']
204
+ meta = {}
205
+ if isinstance(data, dict):
206
+ key = data['@title']
207
+ val = data['scalar']['$'].strip()
208
+ if key and val:
209
+ meta[key] = val
210
+ else:
211
+ meta['chython_unparsed_metadata'] = [data]
212
+ else:
213
+ for x in data:
214
+ key = x['@title']
215
+ val = x['scalar']['$'].strip()
216
+ if key and val:
217
+ meta[key] = val
218
+ else:
219
+ if 'chython_unparsed_metadata' not in meta:
220
+ meta['chython_unparsed_metadata'] = []
221
+ meta['chython_unparsed_metadata'].append(x)
222
+ return meta
223
+ else:
224
+ return {}
225
+
226
+ def close(self, force: bool = False):
227
+ """
228
+ Close opened file
229
+
230
+ :param force: force closing of externally opened file or buffer
231
+ """
232
+ if not self.__is_buffer or force:
233
+ self.__file.close()
234
+
235
+ def tell(self):
236
+ """
237
+ Number of records processed from the original file
238
+ """
239
+ return self.__tell
240
+
241
+ def __enter__(self):
242
+ return self
243
+
244
+ def __exit__(self, _type, value, traceback):
245
+ self.close()
246
+
247
+ def __iter__(self) -> Iterator[Union[ReactionContainer, MoleculeContainer]]:
248
+ while True:
249
+ try:
250
+ yield self.read_structure(current=False)
251
+ except ValueError:
252
+ pass
253
+ except EOFError:
254
+ return
255
+
256
+ def __next__(self) -> Union[ReactionContainer, MoleculeContainer]:
257
+ return next(iter(self))
258
+
259
+ def _read_block(self, *, current: bool = True) -> dict:
260
+ if not current or not self.__buffer:
261
+ self.__buffer = None
262
+ for event, e in self.__xml:
263
+ if _local_name(e.tag) == 'MChemicalStruct':
264
+ self.__buffer = xml_dict(e)
265
+ self.__tell += 1
266
+ e.clear()
267
+ break
268
+ else:
269
+ raise EOFError
270
+ return self.__buffer
271
+
272
+
273
+ def parse_molecule(data):
274
+ atoms, bonds, stereo = [], [], []
275
+ log = []
276
+ atom_map = {}
277
+ if 'atom' in data['atomArray']:
278
+ da = data['atomArray']['atom']
279
+ if isinstance(da, dict):
280
+ da = (da,)
281
+ for n, atom in enumerate(da):
282
+ atom_map[atom['@id']] = n
283
+ atoms.append({'element': atom['@elementType'],
284
+ 'isotope': int(atom['@isotope']) if '@isotope' in atom else None,
285
+ 'charge': int(atom.get('@formalCharge', 0)),
286
+ 'is_radical': '@radical' in atom,
287
+ 'parsed_mapping': int(atom.get('@mrvMap', 0))})
288
+ if '@z3' in atom:
289
+ atoms[-1].update(x=float(atom['@x3']), y=float(atom['@y3']), z=float(atom['@z3']))
290
+ else:
291
+ atoms[-1].update(x=float(atom['@x2']) / 2, y=float(atom['@y2']) / 2)
292
+ if '@mrvQueryProps' in atom:
293
+ raise ValueError('queries unsupported')
294
+ if '@hydrogenCount' in atom:
295
+ atoms[-1]['implicit_hydrogens'] = int(atom['@hydrogenCount'])
296
+ else:
297
+ atom = data['atomArray']
298
+ for n, (_id, e) in enumerate(zip(atom['@atomID'].split(), atom['@elementType'].split())):
299
+ atom_map[_id] = n
300
+ atoms.append({'element': e})
301
+ if '@z3' in atom:
302
+ for a, x, y, z in zip(atoms, atom['@x3'].split(), atom['@y3'].split(), atom['@z3'].split()):
303
+ a['x'] = float(x)
304
+ a['y'] = float(y)
305
+ a['z'] = float(z)
306
+ else:
307
+ for a, x, y in zip(atoms, atom['@x2'].split(), atom['@y2'].split()):
308
+ a['x'] = float(x) / 2
309
+ a['y'] = float(y) / 2
310
+ if '@isotope' in atom:
311
+ for a, x in zip(atoms, atom['@isotope'].split()):
312
+ if x != '0':
313
+ a['isotope'] = int(x)
314
+ if '@formalCharge' in atom:
315
+ for a, x in zip(atoms, atom['@formalCharge'].split()):
316
+ if x != '0':
317
+ a['charge'] = int(x)
318
+ if '@mrvMap' in atom:
319
+ for a, x in zip(atoms, atom['@mrvMap'].split()):
320
+ if x != '0':
321
+ a['parsed_mapping'] = int(x)
322
+ if '@radical' in atom:
323
+ for a, x in zip(atoms, atom['@radical'].split()):
324
+ if x != '0':
325
+ a['is_radical'] = True
326
+ if '@mrvQueryProps' in atom:
327
+ raise ValueError('queries unsupported')
328
+ if not atoms:
329
+ raise EmptyMolecule
330
+
331
+ if 'bond' in data['bondArray']:
332
+ db = data['bondArray']['bond']
333
+ if isinstance(db, dict):
334
+ db = (db,)
335
+ for bond in db:
336
+ order = bond_map[bond['@queryType' if '@queryType' in bond else '@order']]
337
+ a1, a2 = bond['@atomRefs2'].split()
338
+ if 'bondStereo' in bond:
339
+ if '$' in bond['bondStereo']:
340
+ s = bond['bondStereo']['$']
341
+ if s == 'H':
342
+ stereo.append((atom_map[a1], atom_map[a2], -1))
343
+ elif s == 'W':
344
+ stereo.append((atom_map[a1], atom_map[a2], 1))
345
+ else:
346
+ log.append('invalid or unsupported stereo')
347
+ else:
348
+ log.append('incorrect bondStereo tag')
349
+ bonds.append((atom_map[a1], atom_map[a2], order))
350
+
351
+ return {'atoms': atoms, 'bonds': bonds, 'stereo': stereo,
352
+ 'title': data.get('@title'), 'log': log, 'atom_map': atom_map}
353
+
354
+
355
+ def parse_sgroup(data, molecule):
356
+ if 'molecule' in data:
357
+ data = data['molecule']
358
+ if isinstance(data, dict):
359
+ data = (data,)
360
+
361
+ sgroups = {}
362
+ atom_map = molecule['mapping']
363
+ atom_map = {k: atom_map[v] for k, v in molecule['atom_map'].items()}
364
+ for x in data:
365
+ if '@atomRefs' in x:
366
+ atoms = [atom_map[x] for x in x['@atomRefs'].split()]
367
+ elif 'AttachmentPointArray' in x:
368
+ atoms = x['AttachmentPointArray']['attachmentPoint']
369
+ if isinstance(atoms, dict):
370
+ atoms = (atoms,)
371
+ atoms = [atom_map[x['@atom']] for x in atoms]
372
+ else:
373
+ continue
374
+ tmp = {k[1:]: v for k, v in x.items() if k not in ('@atomRefs', '@id', '@molID') and k.startswith('@')}
375
+ tmp['atoms'] = atoms
376
+ sgroups[x['@id']] = tmp
377
+ molecule['meta'] = sgroups
378
+
379
+
380
+ class MRVWrite:
381
+ """
382
+ ChemAxon MRV files writer. works similar to opened for writing file object. support `with` context manager.
383
+ on initialization accept opened for writing in text mode file, string path to file,
384
+ pathlib.Path object or another buffered writer object
385
+ """
386
+ def __init__(self, file, mapping: bool = True):
387
+ """
388
+ :param mapping: write atom mapping.
389
+ """
390
+ if isinstance(file, str):
391
+ self.__file = open(file, 'w')
392
+ self.__is_buffer = False
393
+ elif isinstance(file, Path):
394
+ self.__file = file.open('w')
395
+ self.__is_buffer = False
396
+ elif hasattr(file, 'write'):
397
+ self.__file = file
398
+ self.__is_buffer = True
399
+ else:
400
+ raise TypeError('invalid file. file-like object or path to file expected')
401
+ self.__writable = True
402
+ self.__finalized = False
403
+ self.__mapping = mapping
404
+
405
+ def close(self, force=False):
406
+ """
407
+ Write close tag of MRV file and close opened file
408
+
409
+ :param force: force closing of externally opened file or buffer
410
+ """
411
+ if not self.__finalized:
412
+ self.__file.write('</cml>\n')
413
+ self.__finalized = True
414
+ if self.__writable:
415
+ self.write = self.__write_closed
416
+ self.__writable = False
417
+
418
+ if not self.__is_buffer or force:
419
+ self.__file.close()
420
+
421
+ def __enter__(self):
422
+ return self
423
+
424
+ def __exit__(self, _type, value, traceback):
425
+ self.close()
426
+
427
+ @staticmethod
428
+ def __write_closed(_):
429
+ raise ValueError('I/O operation on closed writer')
430
+
431
+ def write(self, data: Union[ReactionContainer, MoleculeContainer]):
432
+ """
433
+ Write single molecule or reaction into file
434
+ """
435
+ self.__file.write('<cml>\n')
436
+ self.__write(data)
437
+ self.write = self.__write
438
+
439
+ def __write(self, data):
440
+ file = self.__file
441
+ file.write('<MDocument><MChemicalStruct>')
442
+ if isinstance(data, ReactionContainer):
443
+ if not data._arrow:
444
+ data.fix_positions()
445
+ if data.name:
446
+ file.write(f'<reaction title="{data.name}">')
447
+ else:
448
+ file.write('<reaction>')
449
+
450
+ if data.meta:
451
+ file.write('<propertyList>')
452
+ for k, v in data.meta.items():
453
+ if isinstance(v, str):
454
+ v = f'<![CDATA[{v}]]>'
455
+ file.write(f'<property title="{k}"><scalar>{v}</scalar></property>')
456
+ file.write('</propertyList>')
457
+
458
+ c = count(1)
459
+ for i, j in ((data.reactants, 'reactantList'), (data.reagents, 'agentList'),
460
+ (data.products, 'productList')):
461
+ if not i:
462
+ continue
463
+ file.write(f'<{j}>')
464
+ for n, m in zip(c, i):
465
+ if m.name:
466
+ file.write(f'<molecule title="{m.name}" molID="m{n}">')
467
+ else:
468
+ file.write(f'<molecule molID="m{n}">')
469
+
470
+ self.__write_molecule(m)
471
+ file.write('</molecule>')
472
+ file.write(f'</{j}>')
473
+
474
+ file.write(f'<arrow type="DEFAULT" x1="{data._arrow[0] * 2:.4f}" y1="0" '
475
+ f'x2="{data._arrow[1] * 2:.4f}" y2="0"/></reaction>')
476
+ elif not isinstance(data, MoleculeContainer):
477
+ raise TypeError('MoleculeContainer expected')
478
+ else:
479
+ if data.name:
480
+ file.write(f'<molecule title="{data.name}">')
481
+ else:
482
+ file.write('<molecule>')
483
+ if data.meta:
484
+ file.write('<propertyList>')
485
+ for k, v in data.meta.items():
486
+ if isinstance(v, str):
487
+ v = f'<![CDATA[{v}]]>'
488
+ file.write(f'<property title="{k}"><scalar>{v}</scalar></property>')
489
+ file.write('</propertyList>')
490
+
491
+ self.__write_molecule(data)
492
+ file.write('</molecule>')
493
+ file.write('</MChemicalStruct></MDocument>\n')
494
+
495
+ def __write_molecule(self, g):
496
+ bg = g._bonds
497
+ mapping = self.__mapping
498
+
499
+ file = self.__file
500
+ file.write('<atomArray>')
501
+ for n, atom in g.atoms():
502
+ x, y = atom.x, atom.y
503
+ file.write(f'<atom id="a{n}" elementType="{atom.atomic_symbol}" x2="{x * 2:.4f}" y2="{y * 2:.4f}"')
504
+ if mapping:
505
+ file.write(f' mrvMap="{n}"')
506
+ if atom.charge:
507
+ file.write(f' formalCharge="{atom.charge}"')
508
+ if atom.is_radical:
509
+ file.write(' radical="monovalent"')
510
+ if atom.isotope:
511
+ file.write(f' isotope="{atom.isotope}"')
512
+ if atom.implicit_hydrogens is not None:
513
+ file.write(f' hydrogenCount="{atom.implicit_hydrogens}"')
514
+ file.write('/>')
515
+ file.write('</atomArray>')
516
+
517
+ file.write('<bondArray>')
518
+ wedge = defaultdict(set)
519
+ n = 0 # empty wedge trick
520
+ for n, (i, j, s) in enumerate(g._wedge_map, start=1):
521
+ file.write(f'<bond id="b{n}" atomRefs2="a{i} a{j}" order="{bond_map[bg[i][j].order]}">'
522
+ f'<bondStereo>{s == 1 and "W" or "H"}</bondStereo></bond>')
523
+ wedge[i].add(j)
524
+ wedge[j].add(i)
525
+ for i, j, bond in g.bonds():
526
+ if j not in wedge[i]:
527
+ n += 1
528
+ file.write(f'<bond id="b{n}" atomRefs2="a{i} a{j}" order="{bond_map[bond.order]}"/>')
529
+ file.write('</bondArray>')
530
+
531
+
532
+ __all__ = ['MRVRead', 'MRVWrite']