cdxml-toolkit 0.5.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.
Files changed (91) hide show
  1. cdxml_toolkit/__init__.py +18 -0
  2. cdxml_toolkit/_jre/__init__.py +2 -0
  3. cdxml_toolkit/_jre/temurin-21-jre-win-x64.zip +0 -0
  4. cdxml_toolkit/analysis/__init__.py +35 -0
  5. cdxml_toolkit/analysis/deterministic/__init__.py +12 -0
  6. cdxml_toolkit/analysis/deterministic/discover_experiment_files.py +413 -0
  7. cdxml_toolkit/analysis/deterministic/lab_book_formatter.py +701 -0
  8. cdxml_toolkit/analysis/deterministic/lcms_file_categorizer.py +928 -0
  9. cdxml_toolkit/analysis/deterministic/lcms_identifier.py +598 -0
  10. cdxml_toolkit/analysis/deterministic/mass_resolver.py +654 -0
  11. cdxml_toolkit/analysis/deterministic/multi_lcms_analyzer.py +1412 -0
  12. cdxml_toolkit/analysis/deterministic/procedure_writer.py +446 -0
  13. cdxml_toolkit/analysis/extract_nmr.py +47 -0
  14. cdxml_toolkit/analysis/format_procedure_entry.py +479 -0
  15. cdxml_toolkit/analysis/lcms_analyzer.py +1299 -0
  16. cdxml_toolkit/analysis/parse_analysis_file.py +134 -0
  17. cdxml_toolkit/cdxml_builder.py +920 -0
  18. cdxml_toolkit/cdxml_utils.py +342 -0
  19. cdxml_toolkit/chemdraw/__init__.py +5 -0
  20. cdxml_toolkit/chemdraw/_chemscript_server.py +562 -0
  21. cdxml_toolkit/chemdraw/cdx_converter.py +527 -0
  22. cdxml_toolkit/chemdraw/cdxml_to_image.py +262 -0
  23. cdxml_toolkit/chemdraw/cdxml_to_image_rdkit.py +296 -0
  24. cdxml_toolkit/chemdraw/chemscript_bridge.py +901 -0
  25. cdxml_toolkit/constants.py +304 -0
  26. cdxml_toolkit/coord_normalizer.py +438 -0
  27. cdxml_toolkit/deterministic_pipeline/__init__.py +6 -0
  28. cdxml_toolkit/deterministic_pipeline/legacy/__init__.py +5 -0
  29. cdxml_toolkit/deterministic_pipeline/legacy/eln_cdx_cleanup.py +509 -0
  30. cdxml_toolkit/deterministic_pipeline/legacy/eln_enrichment.py +1394 -0
  31. cdxml_toolkit/deterministic_pipeline/legacy/scheme_aligner.py +428 -0
  32. cdxml_toolkit/deterministic_pipeline/legacy/scheme_polisher.py +1337 -0
  33. cdxml_toolkit/deterministic_pipeline/legacy/scheme_polisher_v2.py +1340 -0
  34. cdxml_toolkit/deterministic_pipeline/scheme_reader_audit.py +931 -0
  35. cdxml_toolkit/deterministic_pipeline/scheme_reader_verify.py +1160 -0
  36. cdxml_toolkit/image/__init__.py +15 -0
  37. cdxml_toolkit/image/reaction_from_image.py +2103 -0
  38. cdxml_toolkit/image/structure_from_image.py +1711 -0
  39. cdxml_toolkit/layout/__init__.py +5 -0
  40. cdxml_toolkit/layout/alignment.py +1642 -0
  41. cdxml_toolkit/layout/reaction_cleanup.py +1002 -0
  42. cdxml_toolkit/layout/scheme_merger.py +2260 -0
  43. cdxml_toolkit/mcp_server/__init__.py +0 -0
  44. cdxml_toolkit/mcp_server/__main__.py +5 -0
  45. cdxml_toolkit/mcp_server/server.py +1567 -0
  46. cdxml_toolkit/naming/__init__.py +6 -0
  47. cdxml_toolkit/naming/aligned_namer.py +2342 -0
  48. cdxml_toolkit/naming/mol_builder.py +3722 -0
  49. cdxml_toolkit/naming/name_decomposer.py +2843 -0
  50. cdxml_toolkit/naming/reactions_datamol.json +2414 -0
  51. cdxml_toolkit/office/__init__.py +5 -0
  52. cdxml_toolkit/office/doc_from_template.py +722 -0
  53. cdxml_toolkit/office/ole_embedder.py +808 -0
  54. cdxml_toolkit/office/ole_extractor.py +272 -0
  55. cdxml_toolkit/perception/__init__.py +10 -0
  56. cdxml_toolkit/perception/compound_search.py +229 -0
  57. cdxml_toolkit/perception/eln_csv_parser.py +240 -0
  58. cdxml_toolkit/perception/rdf_parser.py +664 -0
  59. cdxml_toolkit/perception/reactant_heuristic.py +1045 -0
  60. cdxml_toolkit/perception/reaction_parser.py +2150 -0
  61. cdxml_toolkit/perception/scheme_reader.py +2948 -0
  62. cdxml_toolkit/perception/scheme_refine.py +1404 -0
  63. cdxml_toolkit/perception/scheme_segmenter.py +619 -0
  64. cdxml_toolkit/perception/spatial_assignment.py +1013 -0
  65. cdxml_toolkit/rdkit_utils.py +605 -0
  66. cdxml_toolkit/render/__init__.py +17 -0
  67. cdxml_toolkit/render/auto_layout.py +229 -0
  68. cdxml_toolkit/render/compact_parser.py +632 -0
  69. cdxml_toolkit/render/parser.py +706 -0
  70. cdxml_toolkit/render/render_scheme.py +267 -0
  71. cdxml_toolkit/render/renderer.py +2387 -0
  72. cdxml_toolkit/render/schema.py +90 -0
  73. cdxml_toolkit/render/scheme_maker.py +1043 -0
  74. cdxml_toolkit/render/scheme_yaml_writer.py +1487 -0
  75. cdxml_toolkit/resolve/__init__.py +13 -0
  76. cdxml_toolkit/resolve/cas_resolver.py +430 -0
  77. cdxml_toolkit/resolve/chemscanner_abbreviations.json +28813 -0
  78. cdxml_toolkit/resolve/condensed_formula.py +493 -0
  79. cdxml_toolkit/resolve/jre_manager.py +195 -0
  80. cdxml_toolkit/resolve/reagent_abbreviations.json +1046 -0
  81. cdxml_toolkit/resolve/reagent_db.py +285 -0
  82. cdxml_toolkit/resolve/superatom_data.json +2856 -0
  83. cdxml_toolkit/resolve/superatom_table.py +146 -0
  84. cdxml_toolkit/text_formatting.py +298 -0
  85. cdxml_toolkit-0.5.0.dist-info/METADATA +318 -0
  86. cdxml_toolkit-0.5.0.dist-info/RECORD +91 -0
  87. cdxml_toolkit-0.5.0.dist-info/WHEEL +5 -0
  88. cdxml_toolkit-0.5.0.dist-info/entry_points.txt +17 -0
  89. cdxml_toolkit-0.5.0.dist-info/licenses/LICENSE +21 -0
  90. cdxml_toolkit-0.5.0.dist-info/licenses/NOTICE.md +37 -0
  91. cdxml_toolkit-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ """Resolve — turning chemical names, formulae, and abbreviations into SMILES.
2
+
3
+ The 4-tier resolution chain and all supporting databases:
4
+ Tier 1: curated reagent database (~186 entries)
5
+ Tier 2: generative condensed-formula parser
6
+ Tier 3: OPSIN (via reactant_heuristic)
7
+ Tier 4: PubChem name/CAS lookup
8
+ """
9
+
10
+ from .reagent_db import get_reagent_db, ReagentDB
11
+ from .condensed_formula import resolve_condensed_formula
12
+ from .cas_resolver import resolve_name_to_smiles, resolve_cas
13
+ from .superatom_table import lookup_smiles, get_superatom_table
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CAS Number Resolver via PubChem PUG REST API
4
+ Resolves CAS numbers to compound name, MW, molecular formula, SMILES,
5
+ and optionally 2D coordinates.
6
+
7
+ Usage:
8
+ python cas_resolver.py 534-17-8
9
+ python cas_resolver.py 534-17-8 51364-51-3 98327-87-8 123-91-1
10
+ python cas_resolver.py 534-17-8 --coords --output result.json
11
+ python cas_resolver.py --batch cas_list.txt --pretty
12
+
13
+ PubChem API docs:
14
+ https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{CAS}/...
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ import time
21
+ import urllib.request
22
+ import urllib.error
23
+ from typing import Dict, Any, List, Optional
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # PubChem API base
27
+ # ---------------------------------------------------------------------------
28
+
29
+ PUBCHEM_BASE = "https://pubchem.ncbi.nlm.nih.gov/rest/pug"
30
+
31
+ # Properties to request in a single call
32
+ PROPERTIES = "IUPACName,MolecularWeight,MolecularFormula,CanonicalSMILES,IsomericSMILES"
33
+
34
+ # Rate limiting: PubChem asks for max 5 requests/second
35
+ REQUEST_DELAY = 0.25 # seconds between requests
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Core resolution function (importable by other tools)
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def resolve_cas(cas: str, include_coords: bool = False) -> Optional[Dict[str, Any]]:
43
+ """
44
+ Resolve a single CAS number via PubChem PUG REST.
45
+
46
+ Args:
47
+ cas: CAS registry number (e.g. "534-17-8").
48
+ include_coords: If True, also fetch 2D atom coordinates.
49
+
50
+ Returns:
51
+ Dict with keys: cas, name, mw, formula, smiles, isomeric_smiles,
52
+ and optionally coords_2d. Returns None if the lookup fails.
53
+ """
54
+ if not cas or not _validate_cas(cas):
55
+ return None
56
+
57
+ # Step 1: Get compound properties
58
+ props = _fetch_properties(cas)
59
+ if props is None:
60
+ return None
61
+
62
+ result = {
63
+ "cas": cas,
64
+ "name": props.get("IUPACName", ""),
65
+ "mw": props.get("MolecularWeight"),
66
+ "formula": props.get("MolecularFormula", ""),
67
+ "smiles": (props.get("IsomericSMILES")
68
+ or props.get("CanonicalSMILES")
69
+ or props.get("SMILES")
70
+ or props.get("ConnectivitySMILES", "")),
71
+ "isomeric_smiles": (props.get("IsomericSMILES")
72
+ or props.get("SMILES", "")),
73
+ "cid": props.get("CID"),
74
+ }
75
+
76
+ # Convert MW to float
77
+ if result["mw"] is not None:
78
+ try:
79
+ result["mw"] = float(result["mw"])
80
+ except (ValueError, TypeError):
81
+ pass
82
+
83
+ # Step 2: Optionally get 2D coordinates
84
+ if include_coords and result.get("cid"):
85
+ coords = _fetch_2d_coords(result["cid"])
86
+ if coords:
87
+ result["coords_2d"] = coords
88
+
89
+ return result
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # PubChem API helpers
94
+ # ---------------------------------------------------------------------------
95
+
96
+ def _validate_cas(cas: str) -> bool:
97
+ """
98
+ Validate CAS number format (digits-digits-digit with check digit).
99
+
100
+ CAS format: up to 10 digits as XXX...X-YY-Z where Z is a check digit.
101
+ """
102
+ import re
103
+ if not re.match(r'^\d{2,7}-\d{2}-\d$', cas):
104
+ return False
105
+
106
+ # Verify check digit
107
+ digits_only = cas.replace("-", "")
108
+ check = int(digits_only[-1])
109
+ body = digits_only[:-1]
110
+ total = sum(int(d) * (i + 1) for i, d in enumerate(reversed(body)))
111
+ return total % 10 == check
112
+
113
+
114
+ def _fetch_properties(cas: str) -> Optional[Dict[str, Any]]:
115
+ """Fetch compound properties from PubChem by CAS number."""
116
+ url = f"{PUBCHEM_BASE}/compound/name/{cas}/property/{PROPERTIES}/JSON"
117
+
118
+ try:
119
+ req = urllib.request.Request(url)
120
+ req.add_header("User-Agent", "chem-tools/1.0 (cas_resolver.py)")
121
+ with urllib.request.urlopen(req, timeout=15) as resp:
122
+ data = json.loads(resp.read().decode("utf-8"))
123
+ props_list = data.get("PropertyTable", {}).get("Properties", [])
124
+ if props_list:
125
+ return props_list[0]
126
+ except urllib.error.HTTPError as e:
127
+ if e.code == 404:
128
+ print(f" CAS {cas}: not found in PubChem", file=sys.stderr)
129
+ else:
130
+ print(f" CAS {cas}: HTTP error {e.code}", file=sys.stderr)
131
+ except urllib.error.URLError as e:
132
+ print(f" CAS {cas}: connection error — {e.reason}", file=sys.stderr)
133
+ except Exception as e:
134
+ print(f" CAS {cas}: unexpected error — {e}", file=sys.stderr)
135
+
136
+ return None
137
+
138
+
139
+ def _fetch_2d_coords(cid: int) -> Optional[Dict[str, Any]]:
140
+ """
141
+ Fetch 2D atom coordinates from PubChem SDF and parse into a dict.
142
+
143
+ Returns:
144
+ Dict with 'atoms' list of {symbol, x, y} and 'bonds' list of
145
+ {atom1, atom2, order}.
146
+ """
147
+ url = f"{PUBCHEM_BASE}/compound/cid/{cid}/record/SDF/?record_type=2d"
148
+
149
+ try:
150
+ req = urllib.request.Request(url)
151
+ req.add_header("User-Agent", "chem-tools/1.0 (cas_resolver.py)")
152
+ with urllib.request.urlopen(req, timeout=15) as resp:
153
+ sdf_text = resp.read().decode("utf-8")
154
+ return _parse_sdf_coords(sdf_text)
155
+ except Exception as e:
156
+ print(f" CID {cid}: could not fetch 2D coords — {e}", file=sys.stderr)
157
+ return None
158
+
159
+
160
+ def _parse_sdf_coords(sdf_text: str) -> Optional[Dict[str, Any]]:
161
+ """
162
+ Parse atom coordinates and bonds from an SDF/MOL block.
163
+ Handles both V2000 and V3000 formats.
164
+ """
165
+ lines = sdf_text.strip().split("\n")
166
+
167
+ atoms = []
168
+ bonds = []
169
+
170
+ # Find counts line (line 4 in V2000, or look for V3000 marker)
171
+ if any("V3000" in line for line in lines[:10]):
172
+ return _parse_v3000_sdf(lines)
173
+
174
+ # V2000 parsing
175
+ counts_line = None
176
+ counts_idx = None
177
+ for i, line in enumerate(lines):
178
+ if "V2000" in line:
179
+ counts_line = line
180
+ counts_idx = i
181
+ break
182
+
183
+ if counts_line is None or counts_idx is None:
184
+ return None
185
+
186
+ # Parse counts
187
+ num_atoms = int(counts_line[:3].strip())
188
+ num_bonds = int(counts_line[3:6].strip())
189
+
190
+ # Atom block starts right after counts line
191
+ for i in range(counts_idx + 1, counts_idx + 1 + num_atoms):
192
+ if i >= len(lines):
193
+ break
194
+ parts = lines[i].split()
195
+ if len(parts) >= 4:
196
+ x = float(parts[0])
197
+ y = float(parts[1])
198
+ symbol = parts[3]
199
+ atoms.append({"symbol": symbol, "x": round(x, 4), "y": round(y, 4)})
200
+
201
+ # Bond block
202
+ bond_start = counts_idx + 1 + num_atoms
203
+ for i in range(bond_start, bond_start + num_bonds):
204
+ if i >= len(lines):
205
+ break
206
+ parts = lines[i].split()
207
+ if len(parts) >= 3:
208
+ a1 = int(parts[0])
209
+ a2 = int(parts[1])
210
+ order = int(parts[2])
211
+ bonds.append({"atom1": a1, "atom2": a2, "order": order})
212
+
213
+ if not atoms:
214
+ return None
215
+
216
+ return {"atoms": atoms, "bonds": bonds}
217
+
218
+
219
+ def _parse_v3000_sdf(lines: List[str]) -> Optional[Dict[str, Any]]:
220
+ """Parse V3000 format SDF for 2D coords."""
221
+ atoms = []
222
+ bonds = []
223
+ in_atom = False
224
+ in_bond = False
225
+
226
+ for line in lines:
227
+ stripped = line.strip()
228
+ if "BEGIN ATOM" in stripped:
229
+ in_atom = True
230
+ continue
231
+ elif "END ATOM" in stripped:
232
+ in_atom = False
233
+ continue
234
+ elif "BEGIN BOND" in stripped:
235
+ in_bond = True
236
+ continue
237
+ elif "END BOND" in stripped:
238
+ in_bond = False
239
+ continue
240
+
241
+ if in_atom and stripped.startswith("M V30"):
242
+ parts = stripped[6:].split()
243
+ if len(parts) >= 5:
244
+ symbol = parts[1]
245
+ x = float(parts[2])
246
+ y = float(parts[3])
247
+ atoms.append({"symbol": symbol, "x": round(x, 4), "y": round(y, 4)})
248
+
249
+ elif in_bond and stripped.startswith("M V30"):
250
+ parts = stripped[6:].split()
251
+ if len(parts) >= 4:
252
+ order = int(parts[1])
253
+ a1 = int(parts[2])
254
+ a2 = int(parts[3])
255
+ bonds.append({"atom1": a1, "atom2": a2, "order": order})
256
+
257
+ if not atoms:
258
+ return None
259
+ return {"atoms": atoms, "bonds": bonds}
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Batch resolution
264
+ # ---------------------------------------------------------------------------
265
+
266
+ def resolve_batch(cas_list: List[str], include_coords: bool = False,
267
+ delay: float = REQUEST_DELAY) -> List[Dict[str, Any]]:
268
+ """
269
+ Resolve a list of CAS numbers with rate limiting.
270
+
271
+ Args:
272
+ cas_list: List of CAS numbers.
273
+ include_coords: Whether to fetch 2D coordinates.
274
+ delay: Seconds between API requests.
275
+
276
+ Returns:
277
+ List of result dicts (None entries for failed lookups are excluded).
278
+ """
279
+ results = []
280
+ for i, cas in enumerate(cas_list):
281
+ cas = cas.strip()
282
+ if not cas:
283
+ continue
284
+ print(f"Resolving {cas} ({i+1}/{len(cas_list)})...", file=sys.stderr)
285
+ result = resolve_cas(cas, include_coords=include_coords)
286
+ if result:
287
+ results.append(result)
288
+ else:
289
+ results.append({"cas": cas, "error": "not found or lookup failed"})
290
+ if i < len(cas_list) - 1:
291
+ time.sleep(delay)
292
+ return results
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Name → SMILES lookup (common name, abbreviation, or IUPAC)
297
+ # ---------------------------------------------------------------------------
298
+
299
+ _last_request_time: float = 0.0
300
+
301
+
302
+ def _rate_limit() -> None:
303
+ """Enforce PubChem rate limit (max 5 req/sec) across all calls."""
304
+ global _last_request_time
305
+ elapsed = time.time() - _last_request_time
306
+ if elapsed < REQUEST_DELAY:
307
+ time.sleep(REQUEST_DELAY - elapsed)
308
+ _last_request_time = time.time()
309
+
310
+
311
+ def resolve_name_to_smiles(name: str) -> Optional[str]:
312
+ """
313
+ Resolve a chemical name to a canonical SMILES via PubChem PUG REST.
314
+
315
+ Works with common names, trade names, and abbreviations
316
+ (e.g. "BINAP", "Cs2CO3", "dioxane", "triethylamine").
317
+
318
+ Returns a SMILES string, or None on failure.
319
+ """
320
+ import urllib.parse
321
+
322
+ _rate_limit()
323
+ encoded = urllib.parse.quote(name, safe="")
324
+ # Request IsomericSMILES (preserves isotopes, stereochemistry) with
325
+ # CanonicalSMILES as fallback.
326
+ url = (f"{PUBCHEM_BASE}/compound/name/{encoded}"
327
+ f"/property/IsomericSMILES,CanonicalSMILES/JSON")
328
+
329
+ try:
330
+ req = urllib.request.Request(url)
331
+ req.add_header("User-Agent", "chem-tools/1.0 (cas_resolver.py)")
332
+ with urllib.request.urlopen(req, timeout=15) as resp:
333
+ data = json.loads(resp.read().decode("utf-8"))
334
+ props = data.get("PropertyTable", {}).get("Properties", [])
335
+ if props:
336
+ p = props[0]
337
+ # Prefer IsomericSMILES — preserves isotope labels (e.g.
338
+ # deuterium in deucravacitinib) and stereochemistry.
339
+ smiles = (p.get("IsomericSMILES")
340
+ or p.get("CanonicalSMILES")
341
+ or p.get("SMILES")
342
+ or p.get("ConnectivitySMILES"))
343
+ if smiles:
344
+ return smiles
345
+ except urllib.error.HTTPError as e:
346
+ if e.code != 404:
347
+ print(f" PubChem name lookup '{name}': HTTP {e.code}",
348
+ file=sys.stderr)
349
+ except urllib.error.URLError as e:
350
+ print(f" PubChem name lookup '{name}': connection error — {e.reason}",
351
+ file=sys.stderr)
352
+ except Exception as e:
353
+ print(f" PubChem name lookup '{name}': error — {e}", file=sys.stderr)
354
+
355
+ return None
356
+
357
+
358
+ # ---------------------------------------------------------------------------
359
+ # CLI
360
+ # ---------------------------------------------------------------------------
361
+
362
+ def main(argv=None) -> int:
363
+ parser = argparse.ArgumentParser(
364
+ description="Resolve CAS numbers to compound info via PubChem API."
365
+ )
366
+ parser.add_argument(
367
+ "cas_numbers",
368
+ nargs="*",
369
+ help="One or more CAS numbers to resolve (e.g. 534-17-8 123-91-1)",
370
+ )
371
+ parser.add_argument(
372
+ "--batch", "-b",
373
+ help="Text file with one CAS number per line",
374
+ )
375
+ parser.add_argument(
376
+ "--coords",
377
+ action="store_true",
378
+ help="Also fetch 2D atom coordinates from PubChem",
379
+ )
380
+ parser.add_argument(
381
+ "--output", "-o",
382
+ help="Output JSON file (default: print to stdout)",
383
+ )
384
+ parser.add_argument(
385
+ "--pretty",
386
+ action="store_true",
387
+ help="Pretty-print JSON output",
388
+ )
389
+ args = parser.parse_args(argv)
390
+
391
+ # Collect CAS numbers from args and/or batch file
392
+ cas_list = list(args.cas_numbers) if args.cas_numbers else []
393
+
394
+ if args.batch:
395
+ with open(args.batch, "r") as f:
396
+ for line in f:
397
+ cas = line.strip()
398
+ if cas and not cas.startswith("#"):
399
+ cas_list.append(cas)
400
+
401
+ if not cas_list:
402
+ parser.error("No CAS numbers provided. Use positional args or --batch.")
403
+
404
+ # Resolve
405
+ if len(cas_list) == 1:
406
+ result = resolve_cas(cas_list[0], include_coords=args.coords)
407
+ if result is None:
408
+ print(f"Could not resolve CAS {cas_list[0]}", file=sys.stderr)
409
+ return 1
410
+ output = result
411
+ else:
412
+ output = resolve_batch(cas_list, include_coords=args.coords)
413
+
414
+ # Output
415
+ indent = 2 if args.pretty else None
416
+ json_str = json.dumps(output, indent=indent, ensure_ascii=False)
417
+
418
+ if args.output:
419
+ with open(args.output, "w", encoding="utf-8") as f:
420
+ f.write(json_str)
421
+ f.write("\n")
422
+ print(f"Written to {args.output}", file=sys.stderr)
423
+ else:
424
+ print(json_str)
425
+
426
+ return 0
427
+
428
+
429
+ if __name__ == "__main__":
430
+ sys.exit(main())