chython 2.4__cp312-cp312-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 (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. chython-2.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,209 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2024 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 ...exceptions import EmptyMolecule
20
+
21
+
22
+ def parse_mol_v3000(data, *, _header=True):
23
+ if _header:
24
+ title = data[0].strip() or None
25
+ data = data[4:]
26
+ else:
27
+ title = None
28
+
29
+ atom_count, bonds_count, *kvs = data[1][13:].split()
30
+ atom_count = int(atom_count)
31
+ if not atom_count:
32
+ raise EmptyMolecule
33
+ bonds_count = int(bonds_count)
34
+
35
+ log = []
36
+ atoms = []
37
+ bonds = []
38
+ stereo = []
39
+ meta = {}
40
+ atom_map = {}
41
+ star_points = []
42
+
43
+ for kv in kvs:
44
+ if '=' in kv:
45
+ k, v = kv.split('=', 1)
46
+ if k and v:
47
+ meta[k] = v
48
+
49
+ # concatenate line breaks
50
+ tmp = []
51
+ keep = None
52
+ for line in data[3:]:
53
+ if line.endswith('-\n'):
54
+ line = line[7:-2] # skip `M V30 ` and `-\n`
55
+ if keep:
56
+ keep += line
57
+ else:
58
+ keep = line.lstrip()
59
+ else:
60
+ line = line[7:] # skip `M V30 `
61
+ if keep:
62
+ tmp.append(keep + line.rstrip())
63
+ keep = None
64
+ else:
65
+ tmp.append(line.strip())
66
+ data = tmp
67
+
68
+ for line in data[:atom_count]:
69
+ n, a, x, y, z, m, *kvs = split(line)
70
+ if a.startswith(('[', 'NOT')):
71
+ raise ValueError('list of atoms not supported')
72
+ elif a == '*':
73
+ star_points.append(n)
74
+ continue
75
+ elif a == 'R#':
76
+ raise ValueError('R-groups not supported')
77
+
78
+ i = None
79
+ c = 0
80
+ r = False
81
+ for kv in kvs:
82
+ k, v = kv.split('=', 1)
83
+ if k == 'CHG':
84
+ c = int(v)
85
+ elif k == 'MASS':
86
+ i = int(v)
87
+ elif k == 'RAD':
88
+ r = True
89
+ if a == 'D':
90
+ if i:
91
+ raise ValueError('isotope on deuterium atom')
92
+ a = 'H'
93
+ i = 2
94
+
95
+ atom_map[n] = len(atoms)
96
+ atoms.append({'element': a, 'isotope': i, 'charge': c, 'is_radical': r,
97
+ 'x': float(x), 'y': float(y), 'z': float(z), 'parsed_mapping': int(m)})
98
+
99
+ for line in data[2 + atom_count: 2 + atom_count + bonds_count]:
100
+ _, t, a1, a2, *kvs = split(line)
101
+ if a1 in star_points:
102
+ if a2 in star_points:
103
+ log.append('invalid bond ignored: star-point to star-point')
104
+ continue
105
+ try:
106
+ star = atom_map[a2]
107
+ except KeyError:
108
+ raise ValueError('invalid atoms number')
109
+ endpoints = None
110
+ elif a2 in star_points:
111
+ try:
112
+ star = atom_map[a1]
113
+ except KeyError:
114
+ raise ValueError('invalid atoms number')
115
+ endpoints = None
116
+ else:
117
+ star = None
118
+ try:
119
+ t = int(t)
120
+ if t in (9, 10): # added ad-hoc for bond type 9
121
+ t = 8
122
+ log.append('coordinate bond replaced to special')
123
+ bonds.append((atom_map[a1], atom_map[a2], t))
124
+ except KeyError:
125
+ raise ValueError('invalid atoms numbers')
126
+
127
+ for kv in kvs:
128
+ k, v = kv.split('=')
129
+ if k == 'CFG':
130
+ if v == '1':
131
+ stereo.append((atom_map[a1], atom_map[a2], 1))
132
+ elif v == '3':
133
+ stereo.append((atom_map[a1], atom_map[a2], -1))
134
+ else:
135
+ log.append('invalid or unsupported stereo')
136
+ elif k == 'ENDPTS':
137
+ endpoints = v[1:-1].split()
138
+ if len(endpoints) != int(endpoints[0]) + 1:
139
+ raise ValueError('invalid ENDPTS block')
140
+ if star is not None:
141
+ if endpoints: # noqa
142
+ for m in endpoints[1:]: # noqa
143
+ try:
144
+ bonds.append((star, atom_map[m], 8))
145
+ except KeyError:
146
+ raise ValueError('invalid atoms numbers in ENDPTS block')
147
+ else:
148
+ log.append('Bond ignored. Star atom not allowed as endpoint')
149
+
150
+ drop = True
151
+ for line in data[3 + atom_count + bonds_count:]:
152
+ if line.startswith('END CTAB'):
153
+ break
154
+ elif drop:
155
+ if line.startswith('BEGIN SGROUP'):
156
+ drop = False
157
+ continue
158
+ elif line.startswith('END SGROUP'):
159
+ break
160
+
161
+ _, _type, i, *kvs = split(line)
162
+ if _type.startswith('DAT'):
163
+ a = f = d = None
164
+ for kv in kvs:
165
+ k, v = kv.split('=', 1)
166
+ if k == 'ATOMS':
167
+ a = tuple(atom_map[x] for x in v[1:-1].split()[1:] if x not in star_points)
168
+ elif k == 'FIELDNAME':
169
+ f = v.strip('"')
170
+ if k == 'FIELDDATA':
171
+ d = v.strip('"')
172
+ if a and f and d:
173
+ if f == 'MRV_IMPLICIT_H':
174
+ atoms[a[0]]['implicit_hydrogens'] = int(d[6:])
175
+ else:
176
+ log.append(f'ignored SGROUP DAT {i}: {a}\t{f}\t{d}')
177
+ elif _type.startswith('SRU'):
178
+ raise ValueError('Polymers not supported')
179
+
180
+ return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'meta': meta, 'log': log}
181
+
182
+
183
+ def split(line): # todo optimize
184
+ collect = []
185
+ tmp = []
186
+ until = None
187
+ for s in line:
188
+ if until:
189
+ tmp.append(s)
190
+ if s == until:
191
+ until = None
192
+ elif s == '(':
193
+ tmp.append('(')
194
+ until = ')'
195
+ elif s == '"':
196
+ tmp.append(s)
197
+ until = '"'
198
+ elif s == ' ':
199
+ if tmp:
200
+ collect.append(''.join(tmp))
201
+ tmp = []
202
+ else:
203
+ tmp.append(s)
204
+ if tmp:
205
+ collect.append(''.join(tmp))
206
+ return collect
207
+
208
+
209
+ __all__ = ['parse_mol_v3000']
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2024 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 .emol import parse_mol_v3000
20
+ from ...exceptions import EmptyMolecule, EmptyReaction, InvalidV2000
21
+
22
+
23
+ def parse_rxn_v3000(data, *, ignore=True):
24
+ tmp = data[4][13:].split()
25
+ reactants_count = int(tmp[0])
26
+ products_count = int(tmp[1]) + reactants_count
27
+ reagents_count = (int(tmp[2]) if len(tmp) == 3 else 0) + products_count
28
+
29
+ if not reagents_count:
30
+ raise EmptyReaction
31
+
32
+ title = data[1].strip() or None
33
+ log = []
34
+ molecules = []
35
+
36
+ start = 1
37
+ for n in range(0, reagents_count):
38
+ try:
39
+ start = next(n for n, x in enumerate(data[start + 5:], start + 5) if x.startswith('M V30 BEGIN CTAB'))
40
+ except StopIteration:
41
+ raise InvalidV2000
42
+
43
+ try:
44
+ molecules.append(parse_mol_v3000(data[start:], _header=False))
45
+ except ValueError as e:
46
+ if isinstance(e, EmptyMolecule):
47
+ log.append(f'ignored empty molecule {n}')
48
+ elif ignore:
49
+ log.append(f'ignored molecule {n} with {e}')
50
+ else:
51
+ raise
52
+
53
+ if (lm := len(molecules)) < reactants_count:
54
+ reactants_count -= 1
55
+ products_count -= 1
56
+ reagents_count -= 1
57
+ elif lm < products_count:
58
+ products_count -= 1
59
+ reagents_count -= 1
60
+ else:
61
+ reagents_count -= 1
62
+
63
+ return {'reactants': molecules[:reactants_count], 'products': molecules[reactants_count:products_count],
64
+ 'reagents': molecules[products_count:], 'title': title, 'log': log}
65
+
66
+
67
+ __all__ = ['parse_rxn_v3000']
@@ -0,0 +1,144 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2024 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 ...exceptions import EmptyMolecule, InvalidCharge, InvalidV2000
20
+
21
+
22
+ _ctf_data = {'R': 'is_radical', 'C': 'charge', 'I': 'isotope'}
23
+ _charge_map = {' 0': 0, ' 1': 3, ' 2': 2, ' 3': 1, ' 4': 0, ' 5': -1, ' 6': -2, ' 7': -3}
24
+
25
+
26
+ def parse_mol_v2000(data):
27
+ line = data[3]
28
+
29
+ atoms_count = int(line[0:3])
30
+ bonds_count = int(line[3:6])
31
+ if not atoms_count:
32
+ raise EmptyMolecule
33
+
34
+ log = []
35
+ title = data[0].strip() or None
36
+ atoms = []
37
+ bonds = []
38
+ stereo = []
39
+ dat = {}
40
+
41
+ for line in data[4: 4 + atoms_count]:
42
+ try:
43
+ charge = _charge_map[line[36:39]]
44
+ except KeyError:
45
+ raise InvalidCharge
46
+ element = line[31:34].strip()
47
+ isotope = line[34:36]
48
+ delta_isotope = None
49
+
50
+ if element in 'AL':
51
+ raise ValueError('queries not supported')
52
+ elif element == 'D':
53
+ element = 'H'
54
+ if isotope != ' 0':
55
+ raise ValueError('isotope on deuterium atom')
56
+ isotope = 2
57
+ elif isotope != ' 0':
58
+ delta_isotope = int(isotope)
59
+ isotope = None
60
+ else:
61
+ isotope = None
62
+
63
+ mapping = line[60:63]
64
+ atoms.append({'element': element, 'charge': charge, 'isotope': isotope,
65
+ 'parsed_mapping': int(mapping) if mapping else 0, 'x': float(line[0:10]), 'y': float(line[10:20]),
66
+ 'z': float(line[20:30]), 'delta_isotope': delta_isotope})
67
+
68
+ for line in data[4 + atoms_count: 4 + atoms_count + bonds_count]:
69
+ a1, a2 = int(line[0:3]) - 1, int(line[3:6]) - 1
70
+ s = line[9:12]
71
+ if s == ' 1':
72
+ stereo.append((a1, a2, 1))
73
+ elif s == ' 6':
74
+ stereo.append((a1, a2, -1))
75
+ elif s != ' 0':
76
+ log.append(f'unsupported or invalid stereo: {line}')
77
+ b = int(line[6:9])
78
+ if b == 9: # added ad-hoc for bond type 9
79
+ b = 8
80
+ log.append(f'coordinate bond replaced with special: {line}')
81
+ bonds.append((a1, a2, b))
82
+
83
+ for line in data[4 + atoms_count + bonds_count:]:
84
+ if line.startswith('M END'):
85
+ break
86
+ elif line.startswith('M ALS'):
87
+ raise ValueError('list of atoms not supported')
88
+ elif line.startswith(('M ISO', 'M RAD', 'M CHG')):
89
+ _type = _ctf_data[line[3]]
90
+ for i in range(int(line[6:9])):
91
+ i8 = i * 8
92
+ atom = int(line[10 + i8:13 + i8])
93
+ if not atom or atom > len(atoms):
94
+ raise InvalidV2000('invalid atoms number')
95
+ atom = atoms[atom - 1]
96
+ atom[_type] = int(line[14 + i8:17 + i8])
97
+
98
+ elif line.startswith('M STY'):
99
+ for i in range(int(line[6:9])):
100
+ i8 = i * 8
101
+ if (st := line[14 + i8:17 + i8]) == 'DAT':
102
+ dat[int(line[10 + i8:13 + i8])] = {}
103
+ elif st == 'SUP':
104
+ dat[int(line[10 + i8:13 + i8])] = {'type': 'MDL_SUP'}
105
+ elif line.startswith('M SAL'):
106
+ i = int(line[7:10])
107
+ if i in dat:
108
+ dat[i]['atoms'] = tuple(int(line[14 + 4 * i:17 + 4 * i]) - 1 for i in range(int(line[10:13])))
109
+ elif line.startswith('M SDT'):
110
+ i = int(line[7:10])
111
+ if i in dat:
112
+ dat[i]['type'] = line.split()[-1].lower()
113
+ elif line.startswith('M SED'):
114
+ i = int(line[7:10])
115
+ if i in dat:
116
+ dat[i]['value'] = line[10:].strip().replace('/', '').lower()
117
+ elif line.startswith('M SMT'):
118
+ i = int(line[7:10])
119
+ if i in dat:
120
+ dat[i]['value'] = line[10:].strip()
121
+ elif not line.startswith('M SDD'):
122
+ log.append(f'ignored line: {line}')
123
+
124
+ for a in atoms:
125
+ if 'is_radical' in a: # int to bool
126
+ a['is_radical'] = True
127
+ for x in dat.values():
128
+ try:
129
+ _type = x['type']
130
+ if _type == 'mrv_implicit_h':
131
+ _atoms = x['atoms']
132
+ value = x['value']
133
+ if len(_atoms) != 1 or _atoms[0] == -1 or not value:
134
+ raise InvalidV2000(f'MRV_IMPLICIT_H spec invalid {x}')
135
+ atoms[_atoms[0]]['implicit_hydrogens'] = int(value[6:])
136
+ else:
137
+ log.append(f'ignored data: {x}')
138
+ except KeyError:
139
+ raise InvalidV2000(f'Invalid SGROUP {x}')
140
+
141
+ return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'log': log}
142
+
143
+
144
+ __all__ = ['parse_mol_v2000']
@@ -0,0 +1,225 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2023 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 abc import ABCMeta, abstractmethod
20
+ from base64 import urlsafe_b64encode
21
+ from fileinput import FileInput
22
+ from io import StringIO, TextIOWrapper
23
+ from itertools import islice
24
+ from os.path import abspath, join
25
+ from pathlib import Path
26
+ from pickle import load, UnpicklingError
27
+ from sys import platform
28
+ from tempfile import gettempdir
29
+ from typing import Union, Iterator, List, Dict, Optional
30
+ from ...containers import ReactionContainer, MoleculeContainer
31
+
32
+
33
+ class MDLReadMeta(ABCMeta):
34
+ def __call__(cls, *args, **kwargs):
35
+ if kwargs.get('indexable'):
36
+ _cls = type(cls.__name__, (cls,), {'__len__': lambda x: len(x._shifts), '__module__': cls.__module__})
37
+ obj = object.__new__(_cls) # noqa
38
+ else:
39
+ obj = object.__new__(cls) # noqa
40
+ obj.__init__(*args, **kwargs)
41
+ return obj
42
+
43
+
44
+ class MDLRead(metaclass=MDLReadMeta):
45
+ def __init__(self, file, buffer_size=1000, indexable=False, ignore=True, remap=False, ignore_bad_isotopes=False,
46
+ ignore_stereo=False, calc_cis_trans=False):
47
+ if isinstance(file, str):
48
+ self._file = open(file)
49
+ self._is_buffer = False
50
+ elif isinstance(file, Path):
51
+ self._file = file.open()
52
+ self._is_buffer = False
53
+ elif isinstance(file, (TextIOWrapper, StringIO, FileInput)):
54
+ self._file = file
55
+ self._is_buffer = True
56
+ else:
57
+ raise TypeError('invalid file. TextIOWrapper, StringIO or FileInput subclasses or path to file expected')
58
+ self._shifts = None
59
+ self._tell = 0
60
+ self._buffer_size = buffer_size
61
+ self._buffer = None
62
+ self._ignore = ignore
63
+ self._remap = remap
64
+ self._ignore_bad_isotopes = ignore_bad_isotopes
65
+ self._ignore_stereo = ignore_stereo
66
+ self._calc_cis_trans = calc_cis_trans
67
+ if indexable:
68
+ self._load_cache()
69
+
70
+ def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
71
+ """
72
+ Parse whole file
73
+
74
+ :param amount: number of records to read
75
+ """
76
+ if amount:
77
+ return list(islice(iter(self), amount))
78
+ return list(iter(self))
79
+
80
+ @abstractmethod
81
+ def read_structure(self, *, current: bool = True):
82
+ """
83
+ Read Reaction or Molecule container.
84
+
85
+ :param current: return current structure if already parsed, otherwise read next
86
+ """
87
+
88
+ @abstractmethod
89
+ def read_metadata(self, *, current: bool = True) -> Dict[str, str]:
90
+ """
91
+ Read metadata block
92
+ """
93
+
94
+ def close(self, force: bool = False):
95
+ """
96
+ Close opened file
97
+
98
+ :param force: force closing of externally opened file or buffer
99
+ """
100
+ if not self._is_buffer or force:
101
+ self._file.close()
102
+
103
+ def tell(self):
104
+ """
105
+ Number of records processed from the original file
106
+ """
107
+ return self._tell
108
+
109
+ def seek(self, offset):
110
+ """
111
+ Shift to a given record number
112
+ """
113
+ if self._shifts:
114
+ if 0 <= offset < len(self._shifts):
115
+ self._tell = offset
116
+ self._buffer = None
117
+ self._file.seek(self._shifts[offset])
118
+ else:
119
+ raise IndexError('invalid offset')
120
+ else:
121
+ raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
122
+
123
+ @abstractmethod
124
+ def reset_index(self):
125
+ """
126
+ Create (rewrite) indexation table. Implemented only for object that
127
+ is a real file (the path to the file is specified) because the external grep utility is used.
128
+ """
129
+
130
+ def read_block(self, *, current: bool = True) -> str:
131
+ """
132
+ Read full record block with metadata
133
+ """
134
+ return ''.join(self._read_block(current=current))
135
+
136
+ @abstractmethod
137
+ def _read_block(self, *, current: bool = True) -> List[str]:
138
+ """
139
+ Read full record block with metadata
140
+ """
141
+
142
+ def _load_cache(self):
143
+ """
144
+ Load existing cache or create new. Working only for UNIX-like systems and local files (not buffers).
145
+ """
146
+ if platform == 'win32' or self._is_buffer:
147
+ return
148
+ try:
149
+ with open(self._cache_path, 'rb') as f:
150
+ self._shifts = load(f)
151
+ except FileNotFoundError: # cache not found
152
+ self.reset_index()
153
+ except IsADirectoryError as e:
154
+ raise IsADirectoryError(f'Please delete {self._cache_path} directory') from e
155
+ except (UnpicklingError, EOFError) as e: # invalid file. ask user to check it.
156
+ raise UnpicklingError(f'Invalid cache file {self._cache_path}. Please delete it') from e
157
+
158
+ @property
159
+ def _cache_path(self):
160
+ return abspath(join(gettempdir(), 'chython_' + urlsafe_b64encode(abspath(self._file.name).encode()).decode()))
161
+
162
+ def __enter__(self):
163
+ return self
164
+
165
+ def __exit__(self, _type, value, traceback):
166
+ self.close()
167
+
168
+ def __iter__(self) -> Iterator[Union[ReactionContainer, MoleculeContainer]]:
169
+ while True:
170
+ try:
171
+ yield self.read_structure(current=False)
172
+ except ValueError:
173
+ pass
174
+ except EOFError:
175
+ return
176
+
177
+ def __next__(self) -> Union[ReactionContainer, MoleculeContainer]:
178
+ return next(iter(self))
179
+
180
+ def __getitem__(self, item) -> Union[ReactionContainer, MoleculeContainer,
181
+ List[Union[ReactionContainer, MoleculeContainer]]]:
182
+ """
183
+ Getting the item by index from the original file,
184
+ For slices records with errors skipped.
185
+ """
186
+ if self._shifts:
187
+ _len = len(self._shifts)
188
+ if isinstance(item, int):
189
+ if item >= _len or item < -_len:
190
+ raise IndexError('List index out of range')
191
+ if item < 0:
192
+ item += _len
193
+ self.seek(item)
194
+ return self.read_structure()
195
+ elif isinstance(item, slice):
196
+ start, stop, step = item.indices(_len)
197
+ if start == stop:
198
+ return []
199
+ if step == 1:
200
+ self.seek(start)
201
+ records = []
202
+ for _ in range(start, stop):
203
+ try:
204
+ records.append(self.read_structure(current=False))
205
+ except EOFError:
206
+ break
207
+ except ValueError:
208
+ pass
209
+ else:
210
+ records = []
211
+ for index in range(start, stop, step):
212
+ self.seek(index)
213
+ try:
214
+ records.append(self.read_structure())
215
+ except EOFError:
216
+ break
217
+ except ValueError:
218
+ pass
219
+ return records
220
+ else:
221
+ raise TypeError('Indices must be integers or slices')
222
+ raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
223
+
224
+
225
+ __all__ = ['MDLRead']