curlyarrows 0.2.0__tar.gz
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-0.2.0/PKG-INFO +294 -0
- curlyarrows-0.2.0/README.md +270 -0
- curlyarrows-0.2.0/pyproject.toml +45 -0
- curlyarrows-0.2.0/pyproject.toml.orig +46 -0
- curlyarrows-0.2.0/src/curlyarrows/__init__.py +148 -0
- curlyarrows-0.2.0/src/curlyarrows/__main__.py +4 -0
- curlyarrows-0.2.0/src/curlyarrows/arrows.py +1090 -0
- curlyarrows-0.2.0/src/curlyarrows/cli.py +101 -0
- curlyarrows-0.2.0/src/curlyarrows/graph.py +733 -0
- curlyarrows-0.2.0/src/curlyarrows/guide.py +256 -0
- curlyarrows-0.2.0/src/curlyarrows/mcp_server.py +123 -0
- curlyarrows-0.2.0/src/curlyarrows/names.py +298 -0
- curlyarrows-0.2.0/src/curlyarrows/parse.py +253 -0
- curlyarrows-0.2.0/src/curlyarrows/render/__init__.py +4 -0
- curlyarrows-0.2.0/src/curlyarrows/render/graph_backend.py +245 -0
- curlyarrows-0.2.0/src/curlyarrows/render/indigo_backend.py +41 -0
- curlyarrows-0.2.0/src/curlyarrows/render/layout.py +224 -0
- curlyarrows-0.2.0/src/curlyarrows/render/raster.py +52 -0
- curlyarrows-0.2.0/src/curlyarrows/render/rdkit_backend.py +124 -0
- curlyarrows-0.2.0/src/curlyarrows/render/scene_layout.py +265 -0
- curlyarrows-0.2.0/src/curlyarrows/render/svg.py +262 -0
- curlyarrows-0.2.0/src/curlyarrows/resolve.py +285 -0
- curlyarrows-0.2.0/src/curlyarrows/scene.py +227 -0
- curlyarrows-0.2.0/src/curlyarrows/spec.py +250 -0
- curlyarrows-0.2.0/src/curlyarrows/style.py +52 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: curlyarrows
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Arrow-pushing mechanisms, reactions and molecules from a JSON spec an LLM can write: explicit atom/bond graphs, routed curly arrows, RDKit rendering
|
|
5
|
+
Keywords: chemistry,arrow pushing,mechanism,rdkit,llm,mcp
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Requires-Dist: cairosvg>=2.9.1
|
|
12
|
+
Requires-Dist: pillow>=12.3.0
|
|
13
|
+
Requires-Dist: rdkit>=2026.3.6
|
|
14
|
+
Requires-Dist: requests>=2.34.2
|
|
15
|
+
Requires-Dist: epam-indigo>=1.46.0 ; extra == 'indigo'
|
|
16
|
+
Requires-Dist: mcp>=1.0 ; extra == 'mcp'
|
|
17
|
+
Requires-Dist: py2opsin>=1.2.0 ; extra == 'opsin'
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Project-URL: Homepage, https://github.com/dyang/curlyarrows
|
|
20
|
+
Provides-Extra: indigo
|
|
21
|
+
Provides-Extra: mcp
|
|
22
|
+
Provides-Extra: opsin
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# curlyarrows
|
|
26
|
+
|
|
27
|
+
Draw molecules, reaction schemes and **arrow-pushing mechanisms** from a JSON
|
|
28
|
+
spec an LLM can write directly: molecules as explicit atom/bond graphs (or
|
|
29
|
+
SMILES), and curly arrows between any two atoms or bonds, routed automatically
|
|
30
|
+
around bonds and labels. Natural language and reaction SMILES still work for
|
|
31
|
+
plain schemes.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{"title": "SN2",
|
|
35
|
+
"frames": [
|
|
36
|
+
{"species": [{"atoms": [{"id": "O1", "element": "O", "charge": -1, "hydrogens": 1}], "bonds": []},
|
|
37
|
+
"[CH3:2][Br:3]"],
|
|
38
|
+
"arrows": ["O1 -> 2", "2-3 -> 3"],
|
|
39
|
+
"caption": "backside attack"},
|
|
40
|
+
{"species": ["[CH3:2][OH:1]", "[Br-:3]"]}
|
|
41
|
+
]}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install curlyarrows # library + CLI (rdkit, cairosvg, requests)
|
|
53
|
+
pip install "curlyarrows[mcp]" # + the MCP server
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From a checkout: `uv sync` (or `uv sync --all-extras` for the MCP server, the Indigo backend
|
|
57
|
+
and OPSIN, which needs Java).
|
|
58
|
+
|
|
59
|
+
On a minimal Linux (Docker `python:*-slim`, some sandboxes) RDKit's drawing module needs four
|
|
60
|
+
system libraries: `apt-get install libx11-6 libxext6 libxrender1 libexpat1`. PNG/PDF output
|
|
61
|
+
additionally needs the cairo library (`libcairo2`); without it SVG still works and
|
|
62
|
+
`curlyarrows.png_available()` tells you which case you are in.
|
|
63
|
+
|
|
64
|
+
### Using it from claude.ai (Skill)
|
|
65
|
+
|
|
66
|
+
`dist/curlyarrows-skill.zip` (built by `uv run python tools/build_skill.py`) is a claude.ai
|
|
67
|
+
Skill: upload it under Settings > Capabilities > Skills. It teaches Claude the frames spec,
|
|
68
|
+
tells it to `pip install curlyarrows` in its sandbox, and ships three worked examples. The
|
|
69
|
+
sandbox needs network access set to allow package managers (the default).
|
|
70
|
+
|
|
71
|
+
## Use
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import curlyarrows
|
|
75
|
+
|
|
76
|
+
curlyarrows.draw("ethanol + acetic acid -(cat. H2SO4)-> ethyl acetate + water", "ester.png")
|
|
77
|
+
curlyarrows.draw("aspirin", "aspirin.svg")
|
|
78
|
+
svg = curlyarrows.draw("c1ccccc1Br.OB(O)c1ccccc1>[Pd]>c1ccc(-c2ccccc2)cc1") # returns SVG text
|
|
79
|
+
|
|
80
|
+
curlyarrows.draw({
|
|
81
|
+
"title": "Acid-catalysed hydration",
|
|
82
|
+
"steps": [
|
|
83
|
+
{"species": ["[CH2:1]=[CH:2][CH3:3]", "[H:5][OH2+:4]"], "arrows": ["1-2 -> 5", "5-4 -> 4"]},
|
|
84
|
+
{"species": ["[CH3:1][CH+:2][CH3:3]", "[OH2:4]"], "arrows": ["4 -> 2"]},
|
|
85
|
+
]}, "hydration.png")
|
|
86
|
+
|
|
87
|
+
spec = curlyarrows.parse("cyclohexanone + NaBH4 in MeOH -> cyclohexanol") # inspect / edit before drawing
|
|
88
|
+
spec.to_dict()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
CLI:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
curlyarrows "benzene -[Br2, FeBr3]-> bromobenzene" -o out.png
|
|
95
|
+
curlyarrows mech.json -o mech.svg
|
|
96
|
+
curlyarrows "..." --json # show the parsed spec instead of drawing
|
|
97
|
+
curlyarrows resolve "sodium borohydride" "sertraline"
|
|
98
|
+
curlyarrows guide # the prompt text that teaches an LLM the spec
|
|
99
|
+
curlyarrows tools # Claude tool definitions (JSON)
|
|
100
|
+
curlyarrows serve # MCP server
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### For agents (MCP)
|
|
104
|
+
|
|
105
|
+
`.mcp.json` in this repo registers the server for Claude Code. Tools:
|
|
106
|
+
`draw_chemistry`, `draw_mechanism`, `resolve_name`, `parse_chemistry`.
|
|
107
|
+
Each draw tool returns the output path, the parsed spec and the PNG as an
|
|
108
|
+
image block, so the agent can look at what it drew.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
claude mcp add curlyarrows -- uv run --project /path/to/chem_draw curlyarrows serve
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For direct tool use with the Claude API: `curlyarrows.guide.anthropic_tools()`
|
|
115
|
+
(or `openai_tools()`), and `curlyarrows.GUIDE` is a system-prompt snippet.
|
|
116
|
+
|
|
117
|
+
## Input forms
|
|
118
|
+
|
|
119
|
+
| Input | Example | Becomes |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| name / formula / SMILES | `ibuprofen`, `NaBH4`, `CC(=O)O`, `smiles:C1CC1` | `Species` |
|
|
122
|
+
| reaction SMILES | `A.B>agents>C.D` | `Reaction` |
|
|
123
|
+
| prose | `A + B with X in Y at 0 °C gives C` | `Reaction` |
|
|
124
|
+
| chained prose | `A -> B [reagents] -> C [reagents]`, `A -[X]-> B`, `A <=> B` | `Scheme` |
|
|
125
|
+
| JSON | `{"reactants":..}`, `{"steps":[..]}` | `Reaction` / `Scheme` / `Mechanism` |
|
|
126
|
+
|
|
127
|
+
Prose rules: reactants are split on `+`, `and`, commas (not inside `1,2-`);
|
|
128
|
+
`with/using X` goes above the arrow, `in X` / `at X` / temperatures go below;
|
|
129
|
+
reagent-like species (bases, reductants, oxidants, catalysts) listed as
|
|
130
|
+
reactants are moved onto the arrow when a real substrate remains. Anything
|
|
131
|
+
that cannot be resolved raises `ResolveError` naming the token, so an agent can
|
|
132
|
+
retry with `smiles:...`.
|
|
133
|
+
|
|
134
|
+
Name resolution order: built-in dictionary (~1100 solvents, reagents, ions,
|
|
135
|
+
common organics) → the text is SMILES → OPSIN (if installed + Java) → PubChem →
|
|
136
|
+
NCI CIR. Network hits are cached in `~/.cache/curlyarrows/`. `offline=True`
|
|
137
|
+
disables the network.
|
|
138
|
+
|
|
139
|
+
## The v2 model: graphs, frames, arrows
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from curlyarrows import Molecule, Frame, Mechanism, draw
|
|
143
|
+
|
|
144
|
+
hydroxide = Molecule(name="hydroxide")
|
|
145
|
+
hydroxide.add_atom("O1", "O", charge=-1, hydrogens=1)
|
|
146
|
+
|
|
147
|
+
mebr = Molecule.from_smiles("[CH3:2][Br:3]") # atom maps become ids "2", "3"
|
|
148
|
+
|
|
149
|
+
mech = Mechanism(title="SN2", frames=[
|
|
150
|
+
Frame(species=[hydroxide, mebr], arrows=["O1 -> 2", "2-3 -> 3"], caption="backside attack"),
|
|
151
|
+
Frame(species=["[CH3:2][OH:1]", "[Br-:3]"]),
|
|
152
|
+
])
|
|
153
|
+
draw(mech, "sn2.png")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Molecule** (`curlyarrows.graph`): `add_atom(id, element, charge=, radical=, hydrogens=, lone_pairs=, label=)`,
|
|
157
|
+
`add_bond(a, b, order=1|2|3|1.5, stereo=)`, `from_smiles(smiles, ids="map"|"index")`,
|
|
158
|
+
`from_dict`/`to_dict`, `layout(rotate=, flip_x=, flip_y=)`, `set_coords({...})`.
|
|
159
|
+
Hydrogen counts and lone pairs are inferred when omitted and never second-guessed
|
|
160
|
+
when given, odd valences are allowed (mechanisms need them), and an unknown
|
|
161
|
+
element string like `"Nu"` or `"Ph"` becomes a placeholder atom drawn with that text.
|
|
162
|
+
|
|
163
|
+
**Arrow** (`curlyarrows.arrows`): `"O1 -> C1"` (two electrons), `"C1-Br ~> Br"` (one electron, fishhook).
|
|
164
|
+
Endpoints are atom ids or `a-b` bonds; a bond that does not exist yet is a forming bond.
|
|
165
|
+
Dict form `{"from", "to", "electrons", "side", "bend", "label"}` forces the side or curvature.
|
|
166
|
+
|
|
167
|
+
**Routing**: every arrow gets a family of cubic Bézier candidates (both sides, several bends,
|
|
168
|
+
several departure and arrival angles, the lone-pair free side of the atom). Each is sampled and
|
|
169
|
+
scored for bond crossings, grazing other atoms' labels, overlapping captions, and crossing
|
|
170
|
+
earlier arrows, plus small shape penalties. Lowest score wins. Short arrows (lone pair to the
|
|
171
|
+
adjacent bond, bond onto its own atom) get a minimum arc length so they read as arrows, two
|
|
172
|
+
arrows leaving the same bond take opposite sides, and arrows sharing a target get staggered tips.
|
|
173
|
+
`tools/route_debug.py scene.json out.svg` draws all candidates for a scene to debug a route.
|
|
174
|
+
|
|
175
|
+
After the greedy pass, two refinement sweeps re-route each arrow against all the others and keep
|
|
176
|
+
a change only if it helps that arrow without hurting another, which undoes most bad choices the
|
|
177
|
+
order forced. If the best route in some frame still grazes a label or crosses a bond (four
|
|
178
|
+
arrows on a four-membered ring, say), the mechanism is re-rendered once with 1.45× longer bonds
|
|
179
|
+
and the cleaner version is kept (`Style(auto_scale=False)` or `"style": {"auto_scale": false}` to disable,
|
|
180
|
+
`"style": {"bond_length": 45}` to force a size).
|
|
181
|
+
|
|
182
|
+
**Frame / Mechanism** (`curlyarrows.scene`): a frame is species side by side with `+` and routed
|
|
183
|
+
arrows; `caption`, `next_arrow` (forward, equilibrium, resonance, retro, none), `next_label`,
|
|
184
|
+
`plus=False`. Ids must be unique within a frame, or qualify them as `"0:O1"`.
|
|
185
|
+
Species can be `Molecule`, a molecule dict, `{"smiles": ..., "rotate": 30}`, or a SMILES string.
|
|
186
|
+
An intramolecular attack (`"N1 -> C7"` inside one species) shapes the layout: the molecule is
|
|
187
|
+
drawn as if that bond existed, so the nucleophile sits next to its target and the arrow is short,
|
|
188
|
+
the way a chemist draws a ring closure (`forming_layout=False` on the frame turns it off;
|
|
189
|
+
`"forming": [["N1", "C7"]]` on a molecule adds pairs by hand).
|
|
190
|
+
|
|
191
|
+
Errors are written for an agent: an unknown endpoint lists the ids available in that frame.
|
|
192
|
+
|
|
193
|
+
### v1 spec (still supported)
|
|
194
|
+
The older atom-map-only mechanism spec (`{"steps": [{"species": [...], "arrows": ["1 -> 2-3"]}]}`)
|
|
195
|
+
and the natural-language / reaction-SMILES paths are unchanged; the v1 mechanism class now lives at
|
|
196
|
+
`curlyarrows.spec.Mechanism`.
|
|
197
|
+
|
|
198
|
+
`examples/v2_staudinger.py` is a seven-frame Bertozzi–Staudinger ligation written cold against the
|
|
199
|
+
frames spec (placeholder Ph/R/Me groups, a four-membered ring with four arrows, an intramolecular
|
|
200
|
+
acyl transfer); `examples/v2_staudinger.png` is its output.
|
|
201
|
+
|
|
202
|
+
## Styling
|
|
203
|
+
|
|
204
|
+
`curlyarrows.Style(bond_length=30, atom_colors=True, curly_color="#7b1fa2", curly_opacity=0.6,
|
|
205
|
+
max_row_width=1100, show_map_numbers=False, ...)` and pass `style=` to any draw
|
|
206
|
+
call, or put the overrides in the mechanism dict as `"style": {...}`. Curly arrows are purple (a colour RDKit does not use for any element) and 60 % opaque by default,
|
|
207
|
+
so they read against black bonds and coloured heteroatoms without hiding anything they cross. `--bw`, `--bond-length`, `--max-width`, `--show-maps`, `--arrow-color`, `--arrow-opacity` on the CLI.
|
|
208
|
+
|
|
209
|
+
## The landscape: ways to draw chemistry programmatically
|
|
210
|
+
|
|
211
|
+
What exists, what it is good for, and what this package uses.
|
|
212
|
+
|
|
213
|
+
**2D depiction engines (structure → image)**
|
|
214
|
+
|
|
215
|
+
| Tool | Language | Notes |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| **RDKit** `rdMolDraw2D` | Python/C++ | Used here. SVG/PNG, atom pixel coords available (`GetDrawCoords`), reactions via `DrawReaction`, highlights, no curly arrows natively |
|
|
218
|
+
| **Indigo** (EPAM) | Python/Java/.NET | Optional backend here (`curlyarrows.render.indigo_backend`). Very good automatic reaction layout, SVG/PNG/PDF, no curly arrows |
|
|
219
|
+
| **CDK / CDK Depict** | Java (web service) | `https://www.simolecule.com/cdkdepict/depict/bow/svg?smi=...` renders molecules and reactions incl. reaction SMILES with agents; excellent abbreviation handling. Needs Java or the hosted service |
|
|
220
|
+
| **Open Babel** `obabel -osvg/-opng` | C++/CLI | Quick depictions, many formats; style is dated |
|
|
221
|
+
| **OpenChemLib** | Java/JS | Actelion's toolkit; used by many web tools |
|
|
222
|
+
| **Ketcher** (EPAM) | JS | Full web editor; headless depiction via its API, molfile/rxnfile in/out |
|
|
223
|
+
| **ChemDoodle Web / Kekule.js / JSME / Marvin JS** | JS | Browser editors and renderers |
|
|
224
|
+
| **mol2chemfig + chemfig (LaTeX)** | TeX | Publication-quality; `chemfig` natively supports curly arrows (`\chemmove`) and `\schemestart ... \arrow` schemes. Needs a TeX install |
|
|
225
|
+
| **ChemDraw (PerkinElmer/Revvity)** | desktop | Industry standard; CDXML is readable (`rdkit` can read CDXML), scriptable only via its own JS/Apple events |
|
|
226
|
+
| **Marvin (ChemAxon)** `molconvert` | Java | High quality, commercial |
|
|
227
|
+
| **PyMOL / 3D tools** | – | 3D only; not for schemes |
|
|
228
|
+
|
|
229
|
+
**Name ↔ structure**
|
|
230
|
+
|
|
231
|
+
| Tool | Notes |
|
|
232
|
+
|---|---|
|
|
233
|
+
| OPSIN | Open-source IUPAC parser (Java); `py2opsin` wraps it. Best for systematic names |
|
|
234
|
+
| PubChem PUG-REST | Trivial names, drugs, abbreviations; network |
|
|
235
|
+
| NCI/CADD CIR | Fallback resolver; network |
|
|
236
|
+
| STOUT / ChemBERTa-style models | ML name→SMILES; not used |
|
|
237
|
+
| RDKit `MolFromSmiles` | SMILES/SMARTS/InChI/molfile/CDXML parsing |
|
|
238
|
+
|
|
239
|
+
**Reaction / mechanism representations**
|
|
240
|
+
|
|
241
|
+
| Representation | Notes |
|
|
242
|
+
|---|---|
|
|
243
|
+
| Reaction SMILES `A.B>agents>C` | Universal, used by RDKit/Indigo/CDK; atom maps `[C:1]` carry identity across steps |
|
|
244
|
+
| RXN / RDF / CML / CDXML | File formats for schemes; RDKit reads RXN and CDXML |
|
|
245
|
+
| Atom-mapped SMILES + arrow list | What this package uses for curly arrows: `{"species": [...], "arrows": ["1 -> 2-3"]}` |
|
|
246
|
+
| chemfig `\chemmove` | TeX way to draw electron-pushing arrows |
|
|
247
|
+
| Hand-drawn SVG overlays | What `curlyarrows.render.svg.curly_arrow` does over RDKit atom coordinates |
|
|
248
|
+
|
|
249
|
+
**Where this package sits:** RDKit for molecule depiction (fonts are converted
|
|
250
|
+
to paths, so SVGs are self-contained), its own SVG compositor for `+` signs,
|
|
251
|
+
reaction arrows (forward, equilibrium, resonance, retro), reagent/condition
|
|
252
|
+
text with sub/superscripts, row wrapping, titles and captions, and
|
|
253
|
+
quadratic-Bézier curly arrows (full head or fishhook) anchored on RDKit's atom
|
|
254
|
+
pixel coordinates. cairosvg turns the SVG into PNG/PDF.
|
|
255
|
+
|
|
256
|
+
## Layout of the code
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
src/curlyarrows/
|
|
260
|
+
__init__.py draw(), draw_svg(), parse() – public API (v1 inputs and v2 objects)
|
|
261
|
+
graph.py v2: Atom / Bond / Molecule (inference, RDKit bridge, layout)
|
|
262
|
+
arrows.py v2: Arrow, SceneGeom, candidate routing + scoring, arrow SVG
|
|
263
|
+
scene.py v2: Frame / Mechanism
|
|
264
|
+
spec.py Species / Reaction / Scheme / Mechanism / MechStep / CurlyArrow
|
|
265
|
+
parse.py natural language → spec
|
|
266
|
+
resolve.py name → SMILES (dictionary, OPSIN, PubChem, CIR, cache)
|
|
267
|
+
names.py the built-in dictionary
|
|
268
|
+
guide.py GUIDE prompt text + JSON schemas + tool definitions
|
|
269
|
+
style.py Style dataclass
|
|
270
|
+
render/
|
|
271
|
+
rdkit_backend.py molecule → SVG body + atom pixel coords
|
|
272
|
+
svg.py Block compositor, arrows, text, curly arrows
|
|
273
|
+
layout.py v1 reactions, schemes, mechanisms
|
|
274
|
+
graph_backend.py v2 molecule -> SVG using the graph's own coordinates
|
|
275
|
+
scene_layout.py v2 frames and mechanisms
|
|
276
|
+
raster.py SVG → PNG/PDF
|
|
277
|
+
indigo_backend.py optional Indigo renderer
|
|
278
|
+
cli.py `curlyarrows` command
|
|
279
|
+
mcp_server.py `curlyarrows serve`
|
|
280
|
+
examples/gallery.py v1 examples; examples/v2_gallery.py v2 examples
|
|
281
|
+
tools/route_debug.py visualise arrow routing candidates
|
|
282
|
+
tests/ pytest
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Known limits
|
|
286
|
+
|
|
287
|
+
* Prose parsing is heuristic. Agents should prefer the frames spec; the parser is for convenience.
|
|
288
|
+
* Routing is heuristic: four arrows on a four-membered ring or three around one atom are legible only
|
|
289
|
+
after the automatic scale-up, and a lone pair attacking an atom one bond length away is a short
|
|
290
|
+
S-curve. `side`/`bend` hints, a `rotate` on the species, or a larger `bond_length` fix the rest.
|
|
291
|
+
* Arrows anchor on atom stand-off circles and bond midpoints; a specific lone pair or π-face is not chosen.
|
|
292
|
+
* Routing is pure Python at roughly 0.1 s per arrow, fine for mechanisms, slow for hundreds of arrows.
|
|
293
|
+
* Stereochemistry wedges come from RDKit; relative (cis/trans) descriptors in
|
|
294
|
+
names are only honoured when the resolver returns stereo-marked SMILES.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# curlyarrows
|
|
2
|
+
|
|
3
|
+
Draw molecules, reaction schemes and **arrow-pushing mechanisms** from a JSON
|
|
4
|
+
spec an LLM can write directly: molecules as explicit atom/bond graphs (or
|
|
5
|
+
SMILES), and curly arrows between any two atoms or bonds, routed automatically
|
|
6
|
+
around bonds and labels. Natural language and reaction SMILES still work for
|
|
7
|
+
plain schemes.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{"title": "SN2",
|
|
11
|
+
"frames": [
|
|
12
|
+
{"species": [{"atoms": [{"id": "O1", "element": "O", "charge": -1, "hydrogens": 1}], "bonds": []},
|
|
13
|
+
"[CH3:2][Br:3]"],
|
|
14
|
+
"arrows": ["O1 -> 2", "2-3 -> 3"],
|
|
15
|
+
"caption": "backside attack"},
|
|
16
|
+
{"species": ["[CH3:2][OH:1]", "[Br-:3]"]}
|
|
17
|
+
]}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install curlyarrows # library + CLI (rdkit, cairosvg, requests)
|
|
29
|
+
pip install "curlyarrows[mcp]" # + the MCP server
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
From a checkout: `uv sync` (or `uv sync --all-extras` for the MCP server, the Indigo backend
|
|
33
|
+
and OPSIN, which needs Java).
|
|
34
|
+
|
|
35
|
+
On a minimal Linux (Docker `python:*-slim`, some sandboxes) RDKit's drawing module needs four
|
|
36
|
+
system libraries: `apt-get install libx11-6 libxext6 libxrender1 libexpat1`. PNG/PDF output
|
|
37
|
+
additionally needs the cairo library (`libcairo2`); without it SVG still works and
|
|
38
|
+
`curlyarrows.png_available()` tells you which case you are in.
|
|
39
|
+
|
|
40
|
+
### Using it from claude.ai (Skill)
|
|
41
|
+
|
|
42
|
+
`dist/curlyarrows-skill.zip` (built by `uv run python tools/build_skill.py`) is a claude.ai
|
|
43
|
+
Skill: upload it under Settings > Capabilities > Skills. It teaches Claude the frames spec,
|
|
44
|
+
tells it to `pip install curlyarrows` in its sandbox, and ships three worked examples. The
|
|
45
|
+
sandbox needs network access set to allow package managers (the default).
|
|
46
|
+
|
|
47
|
+
## Use
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import curlyarrows
|
|
51
|
+
|
|
52
|
+
curlyarrows.draw("ethanol + acetic acid -(cat. H2SO4)-> ethyl acetate + water", "ester.png")
|
|
53
|
+
curlyarrows.draw("aspirin", "aspirin.svg")
|
|
54
|
+
svg = curlyarrows.draw("c1ccccc1Br.OB(O)c1ccccc1>[Pd]>c1ccc(-c2ccccc2)cc1") # returns SVG text
|
|
55
|
+
|
|
56
|
+
curlyarrows.draw({
|
|
57
|
+
"title": "Acid-catalysed hydration",
|
|
58
|
+
"steps": [
|
|
59
|
+
{"species": ["[CH2:1]=[CH:2][CH3:3]", "[H:5][OH2+:4]"], "arrows": ["1-2 -> 5", "5-4 -> 4"]},
|
|
60
|
+
{"species": ["[CH3:1][CH+:2][CH3:3]", "[OH2:4]"], "arrows": ["4 -> 2"]},
|
|
61
|
+
]}, "hydration.png")
|
|
62
|
+
|
|
63
|
+
spec = curlyarrows.parse("cyclohexanone + NaBH4 in MeOH -> cyclohexanol") # inspect / edit before drawing
|
|
64
|
+
spec.to_dict()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
CLI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curlyarrows "benzene -[Br2, FeBr3]-> bromobenzene" -o out.png
|
|
71
|
+
curlyarrows mech.json -o mech.svg
|
|
72
|
+
curlyarrows "..." --json # show the parsed spec instead of drawing
|
|
73
|
+
curlyarrows resolve "sodium borohydride" "sertraline"
|
|
74
|
+
curlyarrows guide # the prompt text that teaches an LLM the spec
|
|
75
|
+
curlyarrows tools # Claude tool definitions (JSON)
|
|
76
|
+
curlyarrows serve # MCP server
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### For agents (MCP)
|
|
80
|
+
|
|
81
|
+
`.mcp.json` in this repo registers the server for Claude Code. Tools:
|
|
82
|
+
`draw_chemistry`, `draw_mechanism`, `resolve_name`, `parse_chemistry`.
|
|
83
|
+
Each draw tool returns the output path, the parsed spec and the PNG as an
|
|
84
|
+
image block, so the agent can look at what it drew.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
claude mcp add curlyarrows -- uv run --project /path/to/chem_draw curlyarrows serve
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For direct tool use with the Claude API: `curlyarrows.guide.anthropic_tools()`
|
|
91
|
+
(or `openai_tools()`), and `curlyarrows.GUIDE` is a system-prompt snippet.
|
|
92
|
+
|
|
93
|
+
## Input forms
|
|
94
|
+
|
|
95
|
+
| Input | Example | Becomes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| name / formula / SMILES | `ibuprofen`, `NaBH4`, `CC(=O)O`, `smiles:C1CC1` | `Species` |
|
|
98
|
+
| reaction SMILES | `A.B>agents>C.D` | `Reaction` |
|
|
99
|
+
| prose | `A + B with X in Y at 0 °C gives C` | `Reaction` |
|
|
100
|
+
| chained prose | `A -> B [reagents] -> C [reagents]`, `A -[X]-> B`, `A <=> B` | `Scheme` |
|
|
101
|
+
| JSON | `{"reactants":..}`, `{"steps":[..]}` | `Reaction` / `Scheme` / `Mechanism` |
|
|
102
|
+
|
|
103
|
+
Prose rules: reactants are split on `+`, `and`, commas (not inside `1,2-`);
|
|
104
|
+
`with/using X` goes above the arrow, `in X` / `at X` / temperatures go below;
|
|
105
|
+
reagent-like species (bases, reductants, oxidants, catalysts) listed as
|
|
106
|
+
reactants are moved onto the arrow when a real substrate remains. Anything
|
|
107
|
+
that cannot be resolved raises `ResolveError` naming the token, so an agent can
|
|
108
|
+
retry with `smiles:...`.
|
|
109
|
+
|
|
110
|
+
Name resolution order: built-in dictionary (~1100 solvents, reagents, ions,
|
|
111
|
+
common organics) → the text is SMILES → OPSIN (if installed + Java) → PubChem →
|
|
112
|
+
NCI CIR. Network hits are cached in `~/.cache/curlyarrows/`. `offline=True`
|
|
113
|
+
disables the network.
|
|
114
|
+
|
|
115
|
+
## The v2 model: graphs, frames, arrows
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from curlyarrows import Molecule, Frame, Mechanism, draw
|
|
119
|
+
|
|
120
|
+
hydroxide = Molecule(name="hydroxide")
|
|
121
|
+
hydroxide.add_atom("O1", "O", charge=-1, hydrogens=1)
|
|
122
|
+
|
|
123
|
+
mebr = Molecule.from_smiles("[CH3:2][Br:3]") # atom maps become ids "2", "3"
|
|
124
|
+
|
|
125
|
+
mech = Mechanism(title="SN2", frames=[
|
|
126
|
+
Frame(species=[hydroxide, mebr], arrows=["O1 -> 2", "2-3 -> 3"], caption="backside attack"),
|
|
127
|
+
Frame(species=["[CH3:2][OH:1]", "[Br-:3]"]),
|
|
128
|
+
])
|
|
129
|
+
draw(mech, "sn2.png")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Molecule** (`curlyarrows.graph`): `add_atom(id, element, charge=, radical=, hydrogens=, lone_pairs=, label=)`,
|
|
133
|
+
`add_bond(a, b, order=1|2|3|1.5, stereo=)`, `from_smiles(smiles, ids="map"|"index")`,
|
|
134
|
+
`from_dict`/`to_dict`, `layout(rotate=, flip_x=, flip_y=)`, `set_coords({...})`.
|
|
135
|
+
Hydrogen counts and lone pairs are inferred when omitted and never second-guessed
|
|
136
|
+
when given, odd valences are allowed (mechanisms need them), and an unknown
|
|
137
|
+
element string like `"Nu"` or `"Ph"` becomes a placeholder atom drawn with that text.
|
|
138
|
+
|
|
139
|
+
**Arrow** (`curlyarrows.arrows`): `"O1 -> C1"` (two electrons), `"C1-Br ~> Br"` (one electron, fishhook).
|
|
140
|
+
Endpoints are atom ids or `a-b` bonds; a bond that does not exist yet is a forming bond.
|
|
141
|
+
Dict form `{"from", "to", "electrons", "side", "bend", "label"}` forces the side or curvature.
|
|
142
|
+
|
|
143
|
+
**Routing**: every arrow gets a family of cubic Bézier candidates (both sides, several bends,
|
|
144
|
+
several departure and arrival angles, the lone-pair free side of the atom). Each is sampled and
|
|
145
|
+
scored for bond crossings, grazing other atoms' labels, overlapping captions, and crossing
|
|
146
|
+
earlier arrows, plus small shape penalties. Lowest score wins. Short arrows (lone pair to the
|
|
147
|
+
adjacent bond, bond onto its own atom) get a minimum arc length so they read as arrows, two
|
|
148
|
+
arrows leaving the same bond take opposite sides, and arrows sharing a target get staggered tips.
|
|
149
|
+
`tools/route_debug.py scene.json out.svg` draws all candidates for a scene to debug a route.
|
|
150
|
+
|
|
151
|
+
After the greedy pass, two refinement sweeps re-route each arrow against all the others and keep
|
|
152
|
+
a change only if it helps that arrow without hurting another, which undoes most bad choices the
|
|
153
|
+
order forced. If the best route in some frame still grazes a label or crosses a bond (four
|
|
154
|
+
arrows on a four-membered ring, say), the mechanism is re-rendered once with 1.45× longer bonds
|
|
155
|
+
and the cleaner version is kept (`Style(auto_scale=False)` or `"style": {"auto_scale": false}` to disable,
|
|
156
|
+
`"style": {"bond_length": 45}` to force a size).
|
|
157
|
+
|
|
158
|
+
**Frame / Mechanism** (`curlyarrows.scene`): a frame is species side by side with `+` and routed
|
|
159
|
+
arrows; `caption`, `next_arrow` (forward, equilibrium, resonance, retro, none), `next_label`,
|
|
160
|
+
`plus=False`. Ids must be unique within a frame, or qualify them as `"0:O1"`.
|
|
161
|
+
Species can be `Molecule`, a molecule dict, `{"smiles": ..., "rotate": 30}`, or a SMILES string.
|
|
162
|
+
An intramolecular attack (`"N1 -> C7"` inside one species) shapes the layout: the molecule is
|
|
163
|
+
drawn as if that bond existed, so the nucleophile sits next to its target and the arrow is short,
|
|
164
|
+
the way a chemist draws a ring closure (`forming_layout=False` on the frame turns it off;
|
|
165
|
+
`"forming": [["N1", "C7"]]` on a molecule adds pairs by hand).
|
|
166
|
+
|
|
167
|
+
Errors are written for an agent: an unknown endpoint lists the ids available in that frame.
|
|
168
|
+
|
|
169
|
+
### v1 spec (still supported)
|
|
170
|
+
The older atom-map-only mechanism spec (`{"steps": [{"species": [...], "arrows": ["1 -> 2-3"]}]}`)
|
|
171
|
+
and the natural-language / reaction-SMILES paths are unchanged; the v1 mechanism class now lives at
|
|
172
|
+
`curlyarrows.spec.Mechanism`.
|
|
173
|
+
|
|
174
|
+
`examples/v2_staudinger.py` is a seven-frame Bertozzi–Staudinger ligation written cold against the
|
|
175
|
+
frames spec (placeholder Ph/R/Me groups, a four-membered ring with four arrows, an intramolecular
|
|
176
|
+
acyl transfer); `examples/v2_staudinger.png` is its output.
|
|
177
|
+
|
|
178
|
+
## Styling
|
|
179
|
+
|
|
180
|
+
`curlyarrows.Style(bond_length=30, atom_colors=True, curly_color="#7b1fa2", curly_opacity=0.6,
|
|
181
|
+
max_row_width=1100, show_map_numbers=False, ...)` and pass `style=` to any draw
|
|
182
|
+
call, or put the overrides in the mechanism dict as `"style": {...}`. Curly arrows are purple (a colour RDKit does not use for any element) and 60 % opaque by default,
|
|
183
|
+
so they read against black bonds and coloured heteroatoms without hiding anything they cross. `--bw`, `--bond-length`, `--max-width`, `--show-maps`, `--arrow-color`, `--arrow-opacity` on the CLI.
|
|
184
|
+
|
|
185
|
+
## The landscape: ways to draw chemistry programmatically
|
|
186
|
+
|
|
187
|
+
What exists, what it is good for, and what this package uses.
|
|
188
|
+
|
|
189
|
+
**2D depiction engines (structure → image)**
|
|
190
|
+
|
|
191
|
+
| Tool | Language | Notes |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| **RDKit** `rdMolDraw2D` | Python/C++ | Used here. SVG/PNG, atom pixel coords available (`GetDrawCoords`), reactions via `DrawReaction`, highlights, no curly arrows natively |
|
|
194
|
+
| **Indigo** (EPAM) | Python/Java/.NET | Optional backend here (`curlyarrows.render.indigo_backend`). Very good automatic reaction layout, SVG/PNG/PDF, no curly arrows |
|
|
195
|
+
| **CDK / CDK Depict** | Java (web service) | `https://www.simolecule.com/cdkdepict/depict/bow/svg?smi=...` renders molecules and reactions incl. reaction SMILES with agents; excellent abbreviation handling. Needs Java or the hosted service |
|
|
196
|
+
| **Open Babel** `obabel -osvg/-opng` | C++/CLI | Quick depictions, many formats; style is dated |
|
|
197
|
+
| **OpenChemLib** | Java/JS | Actelion's toolkit; used by many web tools |
|
|
198
|
+
| **Ketcher** (EPAM) | JS | Full web editor; headless depiction via its API, molfile/rxnfile in/out |
|
|
199
|
+
| **ChemDoodle Web / Kekule.js / JSME / Marvin JS** | JS | Browser editors and renderers |
|
|
200
|
+
| **mol2chemfig + chemfig (LaTeX)** | TeX | Publication-quality; `chemfig` natively supports curly arrows (`\chemmove`) and `\schemestart ... \arrow` schemes. Needs a TeX install |
|
|
201
|
+
| **ChemDraw (PerkinElmer/Revvity)** | desktop | Industry standard; CDXML is readable (`rdkit` can read CDXML), scriptable only via its own JS/Apple events |
|
|
202
|
+
| **Marvin (ChemAxon)** `molconvert` | Java | High quality, commercial |
|
|
203
|
+
| **PyMOL / 3D tools** | – | 3D only; not for schemes |
|
|
204
|
+
|
|
205
|
+
**Name ↔ structure**
|
|
206
|
+
|
|
207
|
+
| Tool | Notes |
|
|
208
|
+
|---|---|
|
|
209
|
+
| OPSIN | Open-source IUPAC parser (Java); `py2opsin` wraps it. Best for systematic names |
|
|
210
|
+
| PubChem PUG-REST | Trivial names, drugs, abbreviations; network |
|
|
211
|
+
| NCI/CADD CIR | Fallback resolver; network |
|
|
212
|
+
| STOUT / ChemBERTa-style models | ML name→SMILES; not used |
|
|
213
|
+
| RDKit `MolFromSmiles` | SMILES/SMARTS/InChI/molfile/CDXML parsing |
|
|
214
|
+
|
|
215
|
+
**Reaction / mechanism representations**
|
|
216
|
+
|
|
217
|
+
| Representation | Notes |
|
|
218
|
+
|---|---|
|
|
219
|
+
| Reaction SMILES `A.B>agents>C` | Universal, used by RDKit/Indigo/CDK; atom maps `[C:1]` carry identity across steps |
|
|
220
|
+
| RXN / RDF / CML / CDXML | File formats for schemes; RDKit reads RXN and CDXML |
|
|
221
|
+
| Atom-mapped SMILES + arrow list | What this package uses for curly arrows: `{"species": [...], "arrows": ["1 -> 2-3"]}` |
|
|
222
|
+
| chemfig `\chemmove` | TeX way to draw electron-pushing arrows |
|
|
223
|
+
| Hand-drawn SVG overlays | What `curlyarrows.render.svg.curly_arrow` does over RDKit atom coordinates |
|
|
224
|
+
|
|
225
|
+
**Where this package sits:** RDKit for molecule depiction (fonts are converted
|
|
226
|
+
to paths, so SVGs are self-contained), its own SVG compositor for `+` signs,
|
|
227
|
+
reaction arrows (forward, equilibrium, resonance, retro), reagent/condition
|
|
228
|
+
text with sub/superscripts, row wrapping, titles and captions, and
|
|
229
|
+
quadratic-Bézier curly arrows (full head or fishhook) anchored on RDKit's atom
|
|
230
|
+
pixel coordinates. cairosvg turns the SVG into PNG/PDF.
|
|
231
|
+
|
|
232
|
+
## Layout of the code
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
src/curlyarrows/
|
|
236
|
+
__init__.py draw(), draw_svg(), parse() – public API (v1 inputs and v2 objects)
|
|
237
|
+
graph.py v2: Atom / Bond / Molecule (inference, RDKit bridge, layout)
|
|
238
|
+
arrows.py v2: Arrow, SceneGeom, candidate routing + scoring, arrow SVG
|
|
239
|
+
scene.py v2: Frame / Mechanism
|
|
240
|
+
spec.py Species / Reaction / Scheme / Mechanism / MechStep / CurlyArrow
|
|
241
|
+
parse.py natural language → spec
|
|
242
|
+
resolve.py name → SMILES (dictionary, OPSIN, PubChem, CIR, cache)
|
|
243
|
+
names.py the built-in dictionary
|
|
244
|
+
guide.py GUIDE prompt text + JSON schemas + tool definitions
|
|
245
|
+
style.py Style dataclass
|
|
246
|
+
render/
|
|
247
|
+
rdkit_backend.py molecule → SVG body + atom pixel coords
|
|
248
|
+
svg.py Block compositor, arrows, text, curly arrows
|
|
249
|
+
layout.py v1 reactions, schemes, mechanisms
|
|
250
|
+
graph_backend.py v2 molecule -> SVG using the graph's own coordinates
|
|
251
|
+
scene_layout.py v2 frames and mechanisms
|
|
252
|
+
raster.py SVG → PNG/PDF
|
|
253
|
+
indigo_backend.py optional Indigo renderer
|
|
254
|
+
cli.py `curlyarrows` command
|
|
255
|
+
mcp_server.py `curlyarrows serve`
|
|
256
|
+
examples/gallery.py v1 examples; examples/v2_gallery.py v2 examples
|
|
257
|
+
tools/route_debug.py visualise arrow routing candidates
|
|
258
|
+
tests/ pytest
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Known limits
|
|
262
|
+
|
|
263
|
+
* Prose parsing is heuristic. Agents should prefer the frames spec; the parser is for convenience.
|
|
264
|
+
* Routing is heuristic: four arrows on a four-membered ring or three around one atom are legible only
|
|
265
|
+
after the automatic scale-up, and a lone pair attacking an atom one bond length away is a short
|
|
266
|
+
S-curve. `side`/`bend` hints, a `rotate` on the species, or a larger `bond_length` fix the rest.
|
|
267
|
+
* Arrows anchor on atom stand-off circles and bond midpoints; a specific lone pair or π-face is not chosen.
|
|
268
|
+
* Routing is pure Python at roughly 0.1 s per arrow, fine for mechanisms, slow for hundreds of arrows.
|
|
269
|
+
* Stereochemistry wedges come from RDKit; relative (cis/trans) descriptors in
|
|
270
|
+
names are only honoured when the resolver returns stereo-marked SMILES.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "curlyarrows"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Arrow-pushing mechanisms, reactions and molecules from a JSON spec an LLM can write: explicit atom/bond graphs, routed curly arrows, RDKit rendering"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = [
|
|
9
|
+
"chemistry",
|
|
10
|
+
"arrow pushing",
|
|
11
|
+
"mechanism",
|
|
12
|
+
"rdkit",
|
|
13
|
+
"llm",
|
|
14
|
+
"mcp",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"cairosvg>=2.9.1",
|
|
24
|
+
"pillow>=12.3.0",
|
|
25
|
+
"rdkit>=2026.3.6",
|
|
26
|
+
"requests>=2.34.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/dyang/curlyarrows"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
curlyarrows = "curlyarrows.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
indigo = ["epam-indigo>=1.46.0"]
|
|
37
|
+
mcp = ["mcp>=1.0"]
|
|
38
|
+
opsin = ["py2opsin>=1.2.0"]
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
42
|
+
build-backend = "uv_build"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = ["pytest>=9.1.1"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "curlyarrows"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Arrow-pushing mechanisms, reactions and molecules from a JSON spec an LLM can write: explicit atom/bond graphs, routed curly arrows, RDKit rendering"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = ["chemistry", "arrow pushing", "mechanism", "rdkit", "llm", "mcp"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"cairosvg>=2.9.1",
|
|
17
|
+
"pillow>=12.3.0",
|
|
18
|
+
"rdkit>=2026.3.6",
|
|
19
|
+
"requests>=2.34.2",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/dyang/curlyarrows"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
curlyarrows = "curlyarrows.cli:main"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
indigo = [
|
|
30
|
+
"epam-indigo>=1.46.0",
|
|
31
|
+
]
|
|
32
|
+
mcp = [
|
|
33
|
+
"mcp>=1.0",
|
|
34
|
+
]
|
|
35
|
+
opsin = [
|
|
36
|
+
"py2opsin>=1.2.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=9.1.1",
|
|
46
|
+
]
|