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.
- curlyarrows/__init__.py +148 -0
- curlyarrows/__main__.py +4 -0
- curlyarrows/arrows.py +1090 -0
- curlyarrows/cli.py +101 -0
- curlyarrows/graph.py +733 -0
- curlyarrows/guide.py +256 -0
- curlyarrows/mcp_server.py +123 -0
- curlyarrows/names.py +298 -0
- curlyarrows/parse.py +253 -0
- curlyarrows/render/__init__.py +4 -0
- curlyarrows/render/graph_backend.py +245 -0
- curlyarrows/render/indigo_backend.py +41 -0
- curlyarrows/render/layout.py +224 -0
- curlyarrows/render/raster.py +52 -0
- curlyarrows/render/rdkit_backend.py +124 -0
- curlyarrows/render/scene_layout.py +265 -0
- curlyarrows/render/svg.py +262 -0
- curlyarrows/resolve.py +285 -0
- curlyarrows/scene.py +227 -0
- curlyarrows/spec.py +250 -0
- curlyarrows/style.py +52 -0
- curlyarrows-0.2.0.dist-info/METADATA +294 -0
- curlyarrows-0.2.0.dist-info/RECORD +25 -0
- curlyarrows-0.2.0.dist-info/WHEEL +4 -0
- curlyarrows-0.2.0.dist-info/entry_points.txt +3 -0
curlyarrows/__init__.py
ADDED
|
@@ -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
|
curlyarrows/__main__.py
ADDED