helmkit 0.1.0__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.
helmkit/molecule.py ADDED
@@ -0,0 +1,456 @@
1
+ import copy
2
+ import re
3
+ import warnings
4
+ from importlib.resources import files
5
+ from typing import Dict
6
+ from typing import List
7
+ from typing import Optional
8
+ from typing import Tuple
9
+
10
+ from rdkit import Chem
11
+
12
+
13
+ class SequenceConstants:
14
+ def_path = "helmkit.data"
15
+ def_lib_filename = "monomers.sdf"
16
+ monomer_join = "-"
17
+ chain_separator = "."
18
+ csv_separator = ","
19
+ helm_polymer = "|"
20
+ max_rgroups = 4
21
+
22
+
23
+ def get_molecule_property(molecule: Chem.Mol, property_name: str, default=None):
24
+ return (
25
+ molecule.GetProp(property_name) if molecule.HasProp(property_name) else default
26
+ )
27
+
28
+
29
+ def parse_comma_separated_property(
30
+ molecule: Chem.Mol, property_name: str, convert_func=None
31
+ ) -> List:
32
+ property_value = get_molecule_property(molecule, property_name)
33
+ if not property_value:
34
+ return []
35
+
36
+ values = property_value.split(SequenceConstants.csv_separator)
37
+ if convert_func:
38
+ values = [convert_func(v) if v != "None" else None for v in values]
39
+ else:
40
+ values = [None if v == "None" else v for v in values]
41
+
42
+ return values
43
+
44
+
45
+ def infer_attachment_points(molecule: Chem.Mol, rgroup_indices: List[int]) -> List[int]:
46
+ """Infer attachment points by finding atoms bonded to R-group atoms."""
47
+ attachment_points = []
48
+
49
+ for r_idx in rgroup_indices:
50
+ if r_idx is None:
51
+ attachment_points.append(None)
52
+ continue
53
+
54
+ atom = molecule.GetAtomWithIdx(r_idx)
55
+
56
+ for bond in atom.GetBonds():
57
+ other_idx = bond.GetOtherAtomIdx(r_idx)
58
+ attachment_points.append(other_idx)
59
+ break
60
+ else:
61
+ attachment_points.append(None)
62
+ warnings.warn(
63
+ f"R-group atom {r_idx} has no bonds to determine attachment point"
64
+ )
65
+
66
+ return attachment_points
67
+
68
+
69
+ def load_monomer_library(library_path: str) -> Dict:
70
+ """Load and prepare monomer data from SDF file."""
71
+ monomers_dict = {}
72
+ supplier = Chem.SDMolSupplier(library_path)
73
+
74
+ for mol in supplier:
75
+ if mol is None:
76
+ continue
77
+
78
+ symbol = get_molecule_property(mol, "symbol")
79
+ if not symbol:
80
+ continue
81
+
82
+ rgroups = parse_comma_separated_property(mol, "m_Rgroups")
83
+ rgroup_idx = parse_comma_separated_property(mol, "m_RgroupIdx", int)
84
+ attachment_point_idx = infer_attachment_points(mol, rgroup_idx)
85
+
86
+ monomers_dict[symbol] = {
87
+ "m_romol": mol,
88
+ "m_Rgroups": rgroups,
89
+ "m_RgroupIdx": rgroup_idx,
90
+ "m_attachmentPointIdx": attachment_point_idx,
91
+ "m_type": get_molecule_property(mol, "m_type", ""),
92
+ "m_subtype": get_molecule_property(mol, "m_subtype", ""),
93
+ "m_abbr": get_molecule_property(mol, "m_abbr", ""),
94
+ }
95
+
96
+ return monomers_dict
97
+
98
+
99
+ class Molecule:
100
+ """Single class for HELM to RDKit Mol conversion."""
101
+
102
+ def __init__(self, helm: str, monomer_df: Optional[Dict] = None):
103
+ """Initialize a Molecule object from a HELM string."""
104
+ self.mol = None
105
+ self.offset = []
106
+ self.bondlist = []
107
+ self.monomers = []
108
+ self.chains = {}
109
+ self.chain_offset = {}
110
+
111
+ if monomer_df is None:
112
+ default_monomer_df_filepath = files(SequenceConstants.def_path).joinpath(
113
+ SequenceConstants.def_lib_filename
114
+ )
115
+ self.monomer_df = load_monomer_library(str(default_monomer_df_filepath))
116
+ else:
117
+ self.monomer_df = monomer_df
118
+
119
+ self._parse_helm_string(helm)
120
+ self._build_molecule()
121
+
122
+ if not isinstance(self.mol, Chem.rdchem.Mol):
123
+ raise RuntimeError("Failed to initialize RDKit Mol object")
124
+
125
+ def _parse_helm_string(self, helm: str) -> None:
126
+ """Parse a HELM string into molecular components."""
127
+ helm_parts = self._split_helm_sections(helm)
128
+
129
+ if len(helm_parts) < 5:
130
+ warnings.warn(f"Problem with HELM string - not enough sections: {helm}")
131
+ return
132
+
133
+ polymer_sections, connection_sections = helm_parts[0], helm_parts[1]
134
+
135
+ if not polymer_sections:
136
+ warnings.warn(f"No simple polymers in HELM string {helm}")
137
+ return
138
+
139
+ self._process_polymers(polymer_sections)
140
+ self._process_connections(connection_sections)
141
+ self._create_backbone_bonds()
142
+ self._fix_rgroups()
143
+
144
+ def _split_helm_sections(self, helm: str) -> List:
145
+ """Split a HELM string into its components."""
146
+ parts = helm.split("$", 4)
147
+ parts.extend([""] * (5 - len(parts)))
148
+
149
+ parts[0] = (
150
+ parts[0].split(SequenceConstants.helm_polymer)
151
+ if SequenceConstants.helm_polymer in parts[0]
152
+ else [parts[0]]
153
+ )
154
+
155
+ if parts[1]:
156
+ parts[1] = (
157
+ parts[1].split(SequenceConstants.helm_polymer)
158
+ if SequenceConstants.helm_polymer in parts[1]
159
+ else [parts[1]]
160
+ )
161
+ else:
162
+ parts[1] = []
163
+
164
+ return parts
165
+
166
+ def _split_sequence_with_brackets(self, sequence: str) -> List[str]:
167
+ """Split a sequence into individual monomers, respecting brackets."""
168
+ result = []
169
+ current = ""
170
+ bracket_depth = 0
171
+
172
+ for char in sequence:
173
+ if char == "[":
174
+ bracket_depth += 1
175
+ current += char
176
+ elif char == "]":
177
+ bracket_depth -= 1
178
+ current += char
179
+ elif char == "." and bracket_depth == 0:
180
+ result.append(current)
181
+ current = ""
182
+ else:
183
+ current += char
184
+
185
+ if current:
186
+ result.append(current)
187
+
188
+ return result
189
+
190
+ def _extract_chain_id(self, chain_str: str) -> Tuple[int, bool]:
191
+ """Extract chain ID and validate chain type."""
192
+ if chain_str.startswith("CHEM"):
193
+ return None, False
194
+
195
+ if not chain_str.startswith("PEPTIDE"):
196
+ warnings.warn(f"Non-peptide chain: {chain_str}")
197
+ return None, False
198
+
199
+ try:
200
+ return int(chain_str.replace("PEPTIDE", "")), True
201
+ except ValueError:
202
+ warnings.warn(f"Invalid chain ID: {chain_str}")
203
+ return None, False
204
+
205
+ def _process_monomer(
206
+ self, monomer_name: str, chain_id: int, residue_idx: int
207
+ ) -> Optional[Dict]:
208
+ """Process a single monomer."""
209
+ monomer_name = re.sub(r"\[(.*)\]", r"\1", monomer_name)
210
+
211
+ if monomer_name not in self.monomer_df:
212
+ raise ValueError(f"Monomer {monomer_name} not found in monomer library")
213
+
214
+ monomer_info = self.monomer_df[monomer_name]
215
+
216
+ return {
217
+ "m_name": monomer_name,
218
+ "m_chainID": chain_id,
219
+ "m_resID": residue_idx,
220
+ "m_romol": monomer_info["m_romol"],
221
+ "m_Rgroups": copy.deepcopy(monomer_info["m_Rgroups"]),
222
+ "m_RgroupIdx": monomer_info["m_RgroupIdx"],
223
+ "m_attachmentPointIdx": monomer_info["m_attachmentPointIdx"],
224
+ "m_type": monomer_info["m_type"],
225
+ "m_subtype": monomer_info["m_subtype"],
226
+ "m_abbr": monomer_info["m_abbr"],
227
+ }
228
+
229
+ def _process_polymers(self, polymers: List[str]) -> None:
230
+ """Process polymer chains from HELM."""
231
+ monomer_idx = 0
232
+ chain_types = []
233
+ chain_monomer_ids = []
234
+ pattern = re.compile(r"{(.*?)}")
235
+
236
+ for chain in polymers:
237
+ chain = chain.strip()
238
+
239
+ match = pattern.search(chain)
240
+ if not match:
241
+ warnings.warn(f"No sequence in polymer: {chain}")
242
+ continue
243
+
244
+ id_chain = chain[: match.start()]
245
+
246
+ chain_id, valid = self._extract_chain_id(id_chain)
247
+ if not valid:
248
+ continue
249
+
250
+ sequence = match.group(1)
251
+ if not sequence:
252
+ warnings.warn(f"Empty polymer: {chain}")
253
+ continue
254
+
255
+ residues = self._split_sequence_with_brackets(sequence)
256
+
257
+ self.chain_offset[chain_id] = monomer_idx
258
+
259
+ chain_monomer_ids_local = []
260
+ monomer_types = set()
261
+
262
+ for residue_idx, monomer_name in enumerate(residues):
263
+ monomer = self._process_monomer(monomer_name, chain_id, residue_idx)
264
+ if not monomer:
265
+ continue
266
+
267
+ self.monomers.append(monomer)
268
+ chain_monomer_ids_local.append(monomer_idx)
269
+ monomer_types.add(monomer["m_type"])
270
+ monomer_idx += 1
271
+
272
+ if len(monomer_types) == 1:
273
+ chain_type = "peptide" if "aa" in monomer_types else "chem"
274
+ else:
275
+ chain_type = "mixed"
276
+
277
+ chain_types.append(chain_type)
278
+ chain_monomer_ids.append(chain_monomer_ids_local)
279
+
280
+ self.chains = {
281
+ "s_nChains": len(polymers),
282
+ "s_cType": chain_types,
283
+ "s_monomerIDs": chain_monomer_ids,
284
+ }
285
+
286
+ def _parse_connection(self, connection_str: str) -> Optional[Tuple]:
287
+ """Parse a single connection string."""
288
+ parts = connection_str.split(",")
289
+ if len(parts) != 3:
290
+ warnings.warn(f"Invalid connection format: {connection_str}")
291
+ return None
292
+
293
+ chain_id1, chain_id2, bond_spec = parts
294
+
295
+ try:
296
+ chain_id1 = int(chain_id1.replace("PEPTIDE", ""))
297
+ chain_id2 = int(chain_id2.replace("PEPTIDE", ""))
298
+
299
+ bond_parts = re.split(r"[-:]", bond_spec)
300
+ if len(bond_parts) != 4:
301
+ warnings.warn(f"Invalid bond format: {bond_spec}")
302
+ return None
303
+
304
+ residue1, rgroup1, residue2, rgroup2 = bond_parts
305
+
306
+ residue1 = int(residue1) - 1
307
+ residue2 = int(residue2) - 1
308
+ rgroup1 = int(rgroup1.replace("R", ""))
309
+ rgroup2 = int(rgroup2.replace("R", ""))
310
+
311
+ return chain_id1, residue1, rgroup1, chain_id2, residue2, rgroup2
312
+ except (ValueError, IndexError) as e:
313
+ warnings.warn(f"Error parsing connection {connection_str}: {e}")
314
+ return None
315
+
316
+ def _process_connections(self, connections: List[str]) -> None:
317
+ """Process connections between chains."""
318
+ if not connections:
319
+ return
320
+
321
+ for connection_str in connections:
322
+ parsed = self._parse_connection(connection_str)
323
+ if not parsed:
324
+ continue
325
+
326
+ chain_id1, residue1, rgroup1, chain_id2, residue2, rgroup2 = parsed
327
+
328
+ monomer_idx1 = self.chain_offset[chain_id1] + residue1
329
+ monomer_idx2 = self.chain_offset[chain_id2] + residue2
330
+
331
+ monomer1 = self.monomers[monomer_idx1]
332
+ monomer2 = self.monomers[monomer_idx2]
333
+
334
+ attachment_idx1 = monomer1["m_attachmentPointIdx"][rgroup1 - 1]
335
+ attachment_idx2 = monomer2["m_attachmentPointIdx"][rgroup2 - 1]
336
+
337
+ self.bondlist.append(
338
+ [monomer_idx1, attachment_idx1, monomer_idx2, attachment_idx2]
339
+ )
340
+
341
+ def _create_backbone_bonds(self) -> None:
342
+ """Create peptide backbone bonds within each chain."""
343
+ if not self.chains:
344
+ return
345
+
346
+ for chain_ids in self.chains["s_monomerIDs"]:
347
+ for i in range(len(chain_ids) - 1):
348
+ monomer_idx1 = chain_ids[i]
349
+ monomer_idx2 = chain_ids[i + 1]
350
+
351
+ monomer1 = self.monomers[monomer_idx1]
352
+ monomer2 = self.monomers[monomer_idx2]
353
+
354
+ attachment_points1 = monomer1["m_attachmentPointIdx"]
355
+ attachment_points2 = monomer2["m_attachmentPointIdx"]
356
+
357
+ self.bondlist.append(
358
+ [
359
+ monomer_idx1,
360
+ attachment_points1[1],
361
+ monomer_idx2,
362
+ attachment_points2[0],
363
+ ]
364
+ )
365
+
366
+ def _fix_rgroups(self) -> None:
367
+ """Mark R-groups that are used in bonds to be deleted later."""
368
+ for bond in self.bondlist:
369
+ monomer_idx1, attachment_idx1, monomer_idx2, attachment_idx2 = bond
370
+
371
+ self._mark_used_rgroup(monomer_idx1, attachment_idx1)
372
+ self._mark_used_rgroup(monomer_idx2, attachment_idx2)
373
+
374
+ def _mark_used_rgroup(self, monomer_idx: int, attachment_idx: int) -> None:
375
+ """Mark an R-group as used based on its attachment point index."""
376
+ monomer = self.monomers[monomer_idx]
377
+ for i, idx in enumerate(monomer["m_attachmentPointIdx"]):
378
+ if idx == attachment_idx:
379
+ monomer["m_Rgroups"][i] = None
380
+ break
381
+
382
+ def _build_molecule(self) -> None:
383
+ """Build the RDKit molecule from parsed monomer and bond data."""
384
+ self._generate_atom_offsets()
385
+ self._combine_monomers()
386
+ self._add_bonds()
387
+ self._process_rgroups()
388
+ self._sanitize()
389
+
390
+ def _generate_atom_offsets(self) -> None:
391
+ """Generate atom offsets for each monomer in the molecule."""
392
+ self.offset = [0]
393
+ current_offset = 0
394
+
395
+ for monomer in self.monomers:
396
+ atom_count = monomer["m_romol"].GetNumAtoms()
397
+ current_offset += atom_count
398
+ self.offset.append(current_offset)
399
+
400
+ def _combine_monomers(self) -> None:
401
+ """Combine all monomers into a single molecule."""
402
+ if not self.monomers:
403
+ self.mol = Chem.RWMol()
404
+ return
405
+
406
+ combined_mol = self.monomers[0]["m_romol"]
407
+
408
+ for i in range(1, len(self.monomers)):
409
+ combined_mol = Chem.CombineMols(combined_mol, self.monomers[i]["m_romol"])
410
+
411
+ self.mol = Chem.RWMol(combined_mol)
412
+
413
+ def _add_bonds(self) -> None:
414
+ """Add bonds between monomers based on bond list."""
415
+ for monomer1_idx, atom1_idx, monomer2_idx, atom2_idx in self.bondlist:
416
+ absolute_atom1_idx = self.offset[monomer1_idx] + atom1_idx
417
+ absolute_atom2_idx = self.offset[monomer2_idx] + atom2_idx
418
+
419
+ self.mol.AddBond(
420
+ absolute_atom1_idx, absolute_atom2_idx, Chem.BondType.SINGLE
421
+ )
422
+
423
+ def _process_rgroups(self) -> None:
424
+ """Process R-groups in the molecule, replacing or removing as needed."""
425
+ rwmol = Chem.RWMol(self.mol)
426
+
427
+ for idx, monomer in enumerate(self.monomers):
428
+ rgroups = monomer["m_Rgroups"]
429
+ rgroup_idx = monomer["m_RgroupIdx"]
430
+ atom_offset = self.offset[idx]
431
+
432
+ for i in range(min(len(rgroups), SequenceConstants.max_rgroups)):
433
+ if rgroups[i] is not None:
434
+ self._replace_rgroup(rwmol, atom_offset, rgroup_idx[i], rgroups[i])
435
+
436
+ self.mol = rwmol
437
+
438
+ def _replace_rgroup(
439
+ self, rdkit_mol: Chem.RWMol, atom_offset: int, atom_idx: int, atom_type: str
440
+ ) -> None:
441
+ """Replace an R-group with the appropriate atom type."""
442
+ absolute_idx = atom_offset + atom_idx
443
+
444
+ if atom_type == "OH":
445
+ try:
446
+ oxygen_atom = Chem.Atom(8) # Oxygen
447
+ rdkit_mol.ReplaceAtom(absolute_idx, oxygen_atom)
448
+ except Exception as e:
449
+ warnings.warn(f"Failed to replace R-group with OH: {e}")
450
+ elif atom_type != "H":
451
+ warnings.warn(f"Unrecognized R-group type: {atom_type}")
452
+
453
+ def _sanitize(self) -> None:
454
+ """Clean up the molecule by removing dummy atoms and sanitizing."""
455
+ self.mol = Chem.DeleteSubstructs(self.mol, Chem.MolFromSmarts("[#0]"))
456
+ Chem.SanitizeMol(self.mol)
helmkit/py.typed ADDED
File without changes
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: helmkit
3
+ Version: 0.1.0
4
+ Summary: Parse HELM strings into RDKit molecules
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: rdkit>=2025.3.3
8
+ Description-Content-Type: text/markdown
9
+
10
+ # helmkit
11
+
12
+ A Python library for converting HELM (Hierarchical Editing Language for Macromolecules) notation to RDKit molecules.
13
+
14
+ ## Basic Usage
15
+
16
+ ```python
17
+ from helmkit import Molecule
18
+
19
+ # Create a molecule from a HELM string
20
+ helm_string = "PEPTIDE1{A.R.G}$$$"
21
+ molecule = Molecule(helm_string)
22
+
23
+ # Access the RDKit molecule object
24
+ rdkit_mol = molecule.mol
25
+ ```
26
+
27
+ ## Quick Example
28
+
29
+ ```python
30
+ from helmkit import Molecule
31
+ from rdkit.Chem import AllChem, Draw
32
+
33
+ # Create a simple tripeptide (Ala-Arg-Gly)
34
+ molecule = Molecule("PEPTIDE1{A.R.G}$$$")
35
+
36
+ # Generate 2D coordinates for visualization
37
+ AllChem.Compute2DCoords(molecule.mol)
38
+
39
+ # Save the image
40
+ img = Draw.MolToImage(molecule.mol)
41
+ img.save("tripeptide.png")
42
+ ```
43
+
44
+ ## Understanding HELM Notation
45
+
46
+ HELM (Hierarchical Editing Language for Macromolecules) is a notation for representing complex biomolecules. A basic HELM string has the following format:
47
+
48
+ ```
49
+ PEPTIDE1{A.R.G}$PEPTIDE2{S.G.T}$PEPTIDE1,PEPTIDE2,1:R1-4:R3$$
50
+ ```
51
+
52
+ Where:
53
+ - `PEPTIDE1{A.R.G}` defines the first chain (a peptide with amino acids A, R, G)
54
+ - `PEPTIDE2{S.G.T}` defines the second chain
55
+ - `PEPTIDE1,PEPTIDE2,1:R1-4:R3` defines a connection between the chains (R1 of residue 1 in PEPTIDE1 connects to R3 of residue 4 in PEPTIDE2)
56
+ - `$` characters separate different sections of the HELM string
57
+
58
+ ## Using Custom Monomer Data
59
+
60
+ By default, helmkit uses the monomer data in `helmkit/data/monomers.sdf`. To use a custom SDF file:
61
+
62
+ ```python
63
+ from helmkit import Molecule, load_monomer_library
64
+
65
+ # Load your custom monomer data
66
+ custom_sdf_path = "/path/to/your/custom_monomers.sdf"
67
+ custom_monomers = load_monomer_library(custom_sdf_path)
68
+
69
+ # Create molecule with custom monomer data
70
+ molecule = Molecule("PEPTIDE1{A.R.G}$$$", monomer_df=custom_monomers)
71
+ ```
72
+
73
+ ## SDF File Structure Requirements
74
+
75
+ The SDF file containing monomer data must have the following properties for each molecule:
76
+
77
+ ### Required Properties:
78
+ - `symbol`: A unique identifier for the monomer (e.g., "A" for alanine)
79
+ - `m_RgroupIdx`: Comma-separated list of R-group atom indices (e.g., "1,2,None,None")
80
+
81
+ ### Optional Properties:
82
+ - `m_Rgroups`: Comma-separated list of R-group types (e.g., "H,OH,None,None")
83
+ - `m_type`: Monomer type (e.g., "aa" for amino acid)
84
+ - `m_subtype`: Monomer subtype
85
+ - `m_abbr`: Monomer abbreviation
86
+
87
+ ### Example SDF Entry:
88
+
89
+ ```
90
+ Your molecule atom data here...
91
+ ...
92
+
93
+ > <symbol>
94
+ A
95
+
96
+ > <m_Rgroups>
97
+ H,OH,None,None
98
+
99
+ > <m_RgroupIdx>
100
+ 1,2,None,None
101
+
102
+ > <m_type>
103
+ aa
104
+
105
+ > <m_subtype>
106
+ natural
107
+
108
+ > <m_abbr>
109
+ Ala
110
+
111
+ $$$$
112
+ ```
@@ -0,0 +1,8 @@
1
+ helmkit/__init__.py,sha256=Zyka5sRICmVd3z8L_u0mHcecPTpfSWXx1x1x7BaWLIE,183
2
+ helmkit/molecule.py,sha256=jlAEXk1GPL5onUcpnoAfTSXvwWkvXxp4_ffjGg6C7-g,15563
3
+ helmkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ helmkit/data/monomers.sdf,sha256=MgRDLSP6CINOPC8tTzkMOYjRMaK4fpe1WgSolchtUM8,454859
5
+ helmkit-0.1.0.dist-info/METADATA,sha256=khFwKs1ZoEMvMpusqiqVPWTJKGct0TMhP40Ez-fesLM,2714
6
+ helmkit-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ helmkit-0.1.0.dist-info/licenses/LICENSE,sha256=PVD0q3h7qOWaJmtnP2THjFqmIp04z9A-9G2D_th8EUU,1068
8
+ helmkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 adaliaramon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.