curlyarrows 0.2.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.
@@ -0,0 +1,148 @@
1
+ """curlyarrows – draw molecules, reactions and arrow-pushing mechanisms from
2
+ natural language, SMILES, or a JSON spec. Built for LLM agents.
3
+
4
+ Quick start::
5
+
6
+ import curlyarrows
7
+ curlyarrows.draw("ethanol + acetic acid -> ethyl acetate + water", "ester.svg")
8
+ curlyarrows.draw("CC(=O)O.OCC>>CC(=O)OCC.O", "ester.png")
9
+ curlyarrows.draw({"steps": [
10
+ {"species": ["[OH-:1]", "[CH3:2][Br:3]"], "arrows": ["1 -> 2", "2-3 -> 3"]},
11
+ {"species": ["[CH3:2][OH:1]", "[Br-:3]"]},
12
+ ]}, "sn2.svg")
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from .spec import Species, Reaction, Scheme, MechStep, CurlyArrow, load_spec
21
+ from .spec import Mechanism as _SpecMechanism
22
+ from .style import Style, DEFAULT
23
+ from .resolve import resolve, to_smiles, try_smiles, is_smiles, is_reaction_smiles, ResolveError
24
+ from .parse import parse_reaction
25
+ from .render import render_reaction, render_scheme, render_mechanism, render_species, save, svg_to_png, png_available
26
+ from .render.rdkit_backend import render_species as _render_species_image
27
+
28
+ __all__ = [
29
+ "draw", "draw_svg", "draw_molecule", "draw_reaction", "draw_mechanism", "parse", "parse_reaction",
30
+ "resolve", "to_smiles", "try_smiles", "is_smiles", "is_reaction_smiles", "ResolveError",
31
+ "Species", "Reaction", "Scheme", "MechStep", "CurlyArrow", "Style", "load_spec",
32
+ "render_reaction", "render_scheme", "render_mechanism", "render_species", "save", "svg_to_png", "png_available",
33
+ "GUIDE", "__version__",
34
+ # v2 graph API (see CONTRACT.md): explicit molecule graphs + routed curly arrows
35
+ "Molecule", "Atom", "Bond", "GraphError", "Arrow", "Frame", "Mechanism", "draw_v2", "EndpointError",
36
+ ]
37
+ __version__ = "0.2.0"
38
+
39
+ SpecLike = "str | dict | Species | Reaction | Scheme | Mechanism"
40
+
41
+
42
+ def parse(inp: Any, **resolve_kw) -> Species | Reaction | Scheme | Mechanism:
43
+ """Turn any input into a spec object.
44
+
45
+ * dict / JSON text -> :func:`curlyarrows.spec.load_spec`
46
+ * reaction SMILES -> Reaction
47
+ * SMILES or name -> Species
48
+ * free text -> Reaction / Scheme via the NL parser
49
+ """
50
+ if isinstance(inp, (Species, Reaction, Scheme, _SpecMechanism, Molecule, Frame, Mechanism)):
51
+ return inp
52
+ if isinstance(inp, Path):
53
+ inp = inp.read_text()
54
+ if isinstance(inp, str) and inp.strip().startswith("{"):
55
+ inp = json.loads(inp.strip())
56
+ if isinstance(inp, dict):
57
+ if _is_v2(inp): # frames spec (v2) -> scene objects
58
+ from .scene import coerce as _coerce_v2
59
+ return _coerce_v2(inp)
60
+ return load_spec(inp)
61
+ s = str(inp).strip()
62
+ if is_reaction_smiles(s):
63
+ return parse_reaction(s)
64
+ if s.lower().startswith("smiles:"):
65
+ return Species(s[7:].strip())
66
+ if is_smiles(s) and " " not in s:
67
+ return Species(s)
68
+ # a bare name with no reaction arrow -> single molecule
69
+ from .parse import _ARROW_SPLIT, _SPLIT_LIST
70
+ if not _ARROW_SPLIT.search(s) and not _SPLIT_LIST.search(s):
71
+ r = resolve(s, **resolve_kw)
72
+ return Species(r.smiles, name=s, label=s if r.source != "smiles" else None)
73
+ return parse_reaction(s, **resolve_kw)
74
+
75
+
76
+ def _is_v2(inp: Any) -> bool:
77
+ if isinstance(inp, (Molecule, Frame, Mechanism)):
78
+ return True
79
+ return isinstance(inp, dict) and ("frames" in inp or "species" in inp)
80
+
81
+
82
+ def draw_svg(inp: Any, style: Style = DEFAULT, **resolve_kw) -> str:
83
+ """Render any input to an SVG string (v1 specs / text, or v2 graph objects)."""
84
+ if _is_v2(inp):
85
+ from .scene import draw_svg as _draw_v2_svg
86
+ return _draw_v2_svg(inp, style)
87
+ spec = parse(inp, **resolve_kw)
88
+ if isinstance(spec, Species):
89
+ return render_species(spec, style)
90
+ if isinstance(spec, Reaction):
91
+ return render_reaction(spec, style)
92
+ if isinstance(spec, Scheme):
93
+ return render_scheme(spec, style)
94
+ return render_mechanism(spec, style)
95
+
96
+
97
+ def draw(inp: Any, out: str | Path | None = None, *, style: Style = DEFAULT, scale: float = 2.0, **resolve_kw):
98
+ """Render any input. With *out* the file is written (.svg/.png/.pdf) and its
99
+ Path returned; without it the SVG string is returned."""
100
+ svg = draw_svg(inp, style, **resolve_kw)
101
+ if out is None:
102
+ return svg
103
+ return save(svg, out, scale=scale)
104
+
105
+
106
+ def draw_molecule(smiles_or_name: str, out=None, *, label: str | None = None, style: Style = DEFAULT, **kw):
107
+ r = resolve(smiles_or_name, **kw)
108
+ sp = Species(r.smiles, label=label if label is not None else (smiles_or_name if r.source != "smiles" else None))
109
+ svg = render_species(sp, style)
110
+ return save(svg, out) if out else svg
111
+
112
+
113
+ def draw_reaction(text_or_spec, out=None, *, style: Style = DEFAULT, **kw):
114
+ spec = parse(text_or_spec, **kw)
115
+ if isinstance(spec, Species):
116
+ spec = Reaction(reactants=[spec], arrow="none")
117
+ svg = render_scheme(spec, style) if isinstance(spec, Scheme) else render_reaction(spec, style)
118
+ return save(svg, out) if out else svg
119
+
120
+
121
+ def draw_mechanism(spec, out=None, *, style: Style = DEFAULT):
122
+ if _is_v2(spec):
123
+ from .scene import draw as _draw_v2
124
+ return _draw_v2(spec, out, style=style)
125
+ spec = load_spec(spec) if not isinstance(spec, _SpecMechanism) else spec
126
+ if not isinstance(spec, _SpecMechanism):
127
+ raise TypeError("draw_mechanism needs a Mechanism spec: {'steps': [{'species': [...], 'arrows': [...]}]}")
128
+ svg = render_mechanism(spec, style)
129
+ return save(svg, out) if out else svg
130
+
131
+
132
+ from .guide import GUIDE # noqa: E402 (string constant for LLM prompts)
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # v2 API: explicit molecule graphs (curlyarrows.graph), routed electron-pushing
136
+ # arrows (curlyarrows.arrows) and scenes (curlyarrows.scene). ``curlyarrows.Mechanism``
137
+ # is the v2 scene class; the v1 SMILES/NL spec class is ``curlyarrows.spec.Mechanism``.
138
+ # ---------------------------------------------------------------------------
139
+ try:
140
+ from .graph import Molecule, Atom, Bond, GraphError # noqa: E402
141
+ from .arrows import Arrow, EndpointError # noqa: E402
142
+ from .scene import Frame, Mechanism, draw as draw_v2 # noqa: E402
143
+ except ModuleNotFoundError as _e: # pragma: no cover - only while the v2 modules are being written
144
+ if _e.name not in ("curlyarrows.graph", "curlyarrows.arrows", "curlyarrows.scene"):
145
+ raise
146
+ import warnings as _w
147
+ _w.warn(f"curlyarrows v2 API unavailable: {_e}", stacklevel=1)
148
+ Molecule = Frame = Mechanism = () # type: ignore[assignment] # isinstance() against () is always False
@@ -0,0 +1,4 @@
1
+ """`python -m curlyarrows ...` == the `curlyarrows` console script."""
2
+ from .cli import main
3
+
4
+ raise SystemExit(main())