mcpymol 1.5.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.
Files changed (69) hide show
  1. mcpymol-1.5.0/.claude/skills/mcpymol-guide/SKILL.md +182 -0
  2. mcpymol-1.5.0/.coverage +0 -0
  3. mcpymol-1.5.0/.github/dependabot.yml +20 -0
  4. mcpymol-1.5.0/.github/workflows/ci.yml +123 -0
  5. mcpymol-1.5.0/.github/workflows/dependabot-auto-merge.yml +42 -0
  6. mcpymol-1.5.0/.github/workflows/release.yml +67 -0
  7. mcpymol-1.5.0/.gitignore +41 -0
  8. mcpymol-1.5.0/.pre-commit-config.yaml +86 -0
  9. mcpymol-1.5.0/.python-version +1 -0
  10. mcpymol-1.5.0/CHANGELOG.md +179 -0
  11. mcpymol-1.5.0/CONTRIBUTING.md +142 -0
  12. mcpymol-1.5.0/KNOWN-ISSUES.md +96 -0
  13. mcpymol-1.5.0/LICENSE +21 -0
  14. mcpymol-1.5.0/PKG-INFO +697 -0
  15. mcpymol-1.5.0/README.md +642 -0
  16. mcpymol-1.5.0/assets/cinematic_view.png +0 -0
  17. mcpymol-1.5.0/assets/conservation_view.png +0 -0
  18. mcpymol-1.5.0/assets/crosslink_view.png +0 -0
  19. mcpymol-1.5.0/assets/electrostatic_view.png +0 -0
  20. mcpymol-1.5.0/assets/hydrophobic_surface_view.png +0 -0
  21. mcpymol-1.5.0/assets/interface_view.png +0 -0
  22. mcpymol-1.5.0/assets/ligand_view.png +0 -0
  23. mcpymol-1.5.0/assets/mutation_view.png +0 -0
  24. mcpymol-1.5.0/assets/nucleosome.png +0 -0
  25. mcpymol-1.5.0/assets/plddt_view.png +0 -0
  26. mcpymol-1.5.0/assets/pocket_view.png +0 -0
  27. mcpymol-1.5.0/assets/poisson_boltzmann_view.png +0 -0
  28. mcpymol-1.5.0/assets/print_ribbon_view.png +0 -0
  29. mcpymol-1.5.0/assets/putty_view.png +0 -0
  30. mcpymol-1.5.0/assets/superposition_view.png +0 -0
  31. mcpymol-1.5.0/assets/textbook_view.png +0 -0
  32. mcpymol-1.5.0/assets/turntable.gif +0 -0
  33. mcpymol-1.5.0/pyproject.toml +116 -0
  34. mcpymol-1.5.0/src/mcpymol/__init__.py +3 -0
  35. mcpymol-1.5.0/src/mcpymol/__main__.py +6 -0
  36. mcpymol-1.5.0/src/mcpymol/analysis.py +538 -0
  37. mcpymol-1.5.0/src/mcpymol/app.py +11 -0
  38. mcpymol-1.5.0/src/mcpymol/bridge.py +183 -0
  39. mcpymol-1.5.0/src/mcpymol/cli.py +148 -0
  40. mcpymol-1.5.0/src/mcpymol/comparison.py +219 -0
  41. mcpymol-1.5.0/src/mcpymol/conservation.py +361 -0
  42. mcpymol-1.5.0/src/mcpymol/pdbtext.py +145 -0
  43. mcpymol-1.5.0/src/mcpymol/plugin.py +453 -0
  44. mcpymol-1.5.0/src/mcpymol/primitives.py +1315 -0
  45. mcpymol-1.5.0/src/mcpymol/printing.py +385 -0
  46. mcpymol-1.5.0/src/mcpymol/rendering.py +272 -0
  47. mcpymol-1.5.0/src/mcpymol/server.py +489 -0
  48. mcpymol-1.5.0/src/mcpymol/structures.py +919 -0
  49. mcpymol-1.5.0/src/mcpymol/style.py +27 -0
  50. mcpymol-1.5.0/src/mcpymol/views.py +923 -0
  51. mcpymol-1.5.0/tests/conftest.py +16 -0
  52. mcpymol-1.5.0/tests/test_alphafold.py +493 -0
  53. mcpymol-1.5.0/tests/test_auto_wrappers.py +259 -0
  54. mcpymol-1.5.0/tests/test_bridge_roundtrip.py +197 -0
  55. mcpymol-1.5.0/tests/test_cli.py +195 -0
  56. mcpymol-1.5.0/tests/test_comparison.py +310 -0
  57. mcpymol-1.5.0/tests/test_conservation.py +568 -0
  58. mcpymol-1.5.0/tests/test_contacts.py +451 -0
  59. mcpymol-1.5.0/tests/test_docs.py +116 -0
  60. mcpymol-1.5.0/tests/test_interface.py +290 -0
  61. mcpymol-1.5.0/tests/test_live.py +465 -0
  62. mcpymol-1.5.0/tests/test_measurements.py +170 -0
  63. mcpymol-1.5.0/tests/test_package_layout.py +165 -0
  64. mcpymol-1.5.0/tests/test_plugin.py +476 -0
  65. mcpymol-1.5.0/tests/test_rendering.py +431 -0
  66. mcpymol-1.5.0/tests/test_server.py +1181 -0
  67. mcpymol-1.5.0/tests/test_sessions.py +182 -0
  68. mcpymol-1.5.0/tests/test_structure_info.py +655 -0
  69. mcpymol-1.5.0/uv.lock +2806 -0
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: mcpymol-guide
3
+ description: >-
4
+ How to drive the mcpymol MCP server well — structure prep, the view-preset
5
+ catalogue, PyMOL selection syntax, rendering, and 3D-print STL export.
6
+ TRIGGER when the user asks to visualize/fetch/render a protein structure
7
+ through the mcpymol tools, apply a *_view preset, or produce a 3D-printable
8
+ model/STL. SKIP for unrelated PyMOL-less work.
9
+ ---
10
+
11
+ # Driving the mcpymol MCP server
12
+
13
+ The `mcp__mcpymol__*` tools talk to a running PyMOL over a socket. Prefer the
14
+ high-level tools (`fetch_structure`, the `*_view` presets, `show`/`color`/
15
+ `select`) over `execute_pymol_command` — the presets do coloring,
16
+ transparency, H-bonds and labels in one call. Reach for
17
+ `execute_pymol_command` only when no dedicated tool covers the need.
18
+
19
+ ## Report first, then draw
20
+
21
+ The presets make pictures; the report tools answer questions with numbers.
22
+ When the user asks *why* or *how much* — what holds this ligand, how big is
23
+ this interface, where do these differ — run the report and quote the numbers,
24
+ then apply the matching view if a picture helps. Answering a quantitative
25
+ question with only a rendering leaves the user to measure it themselves.
26
+
27
+ | The question | Reach for |
28
+ | --- | --- |
29
+ | What is this structure? | `structure_info` |
30
+ | What is the sequence / numbering / where are the gaps? | `get_sequence` |
31
+ | What contacts what, and how tightly? | `contact_report` -> `ligand_view` |
32
+ | How big is this interface, which residues matter? | `interface_report` -> `interface_view` |
33
+ | Where do two structures differ? | `superposition_view` |
34
+ | How far / what angle / how much area? | `distance`, `angle`, `dihedral`, `sasa`, `rms_cur` |
35
+ | Does my selection match anything? | `count_atoms` |
36
+ | Occupancy / altloc / per-atom B-factor? | `atom_properties` |
37
+
38
+ `contact_report` classification is heavy-atom geometry (no hydrogens assumed):
39
+ salt bridge <= 4.0 A, H-bond <= 3.5 A between N/O, hydrophobic <= 4.5 A,
40
+ ring centroids <= 5.5 A. Report it as such — a "hydrogen bond" here is a
41
+ donor-acceptor pair with plausible geometry, not one verified against a
42
+ hydrogen. Aromatic detection covers the standard aromatic amino acids only;
43
+ ligand rings appear as hydrophobic contacts.
44
+
45
+ `interface_report` gives buried surface area per side. Under ~400 A^2 usually
46
+ means crystal packing rather than a real interface; over ~1000 A^2 means a
47
+ substantial, likely specific association. Say which regime the number is in.
48
+
49
+ ## Core workflow
50
+
51
+ 0. **Predicted structures.** `fetch_structure` also accepts a UniProt
52
+ accession (`P69905`, `af-P69905`) and routes it to AlphaFold DB, colouring
53
+ by pLDDT confidence; `fetch_alphafold` is the same thing called directly,
54
+ and takes `model_version` / `fragment` for older or split entries. For predicted models use `plddt_view`, never
55
+ `bfactor_view`/`putty_view` — pLDDT lives in the B-factor column and those
56
+ presets read it backwards (they assume low = rigid; low pLDDT = unreliable).
57
+ 0b. **Local files.** `load_structure(file_path, obj_name)` takes a PDB/mmCIF
58
+ off disk — a built model, a docking pose, an MD frame — and applies the
59
+ same multimer heuristic and styling as a fetch. Use it whenever the user
60
+ has a file rather than an accession.
61
+ 1. **`fetch_structure(pdb_code)`** — grabs the biological assembly when one
62
+ exists, runs a BFS chain-contact heuristic (`multimer_cutoff`, default
63
+ 8.0 Å) so functional multimers stay whole while crystallographic copies
64
+ are dropped, hides waters/additives, and applies the default style. The
65
+ object is named after the PDB code unless `obj_name` is given.
66
+ 2. **Ground yourself before composing selections.** Use `list_objects`,
67
+ `list_chains(obj_name)`, `list_ligands(obj_name)` instead of guessing
68
+ object names, chain IDs, or 3-letter ligand codes.
69
+ 3. **Apply a view preset** (below) or build the scene manually.
70
+ 4. **Check a selection with `count_atoms`** before building a scene on it —
71
+ an empty selection is invisible until the render comes out blank.
72
+ 5. **Render with `render()`** — it ray-traces and returns the image directly,
73
+ so you can see the result and iterate. Prefer it over `ray` + `png`, which
74
+ only leave a file on disk. Pass `ray_trace=False` for a fast unshaded
75
+ check of a selection or orientation, and keep the width/height modest —
76
+ the image is inlined into the conversation.
77
+
78
+ ## View-preset catalogue
79
+
80
+ All take `obj_name`. Each is a one-call scene.
81
+
82
+ - **Binding / functional sites:** `ligand_view`, `pocket_view`,
83
+ `pharmacophore_view`, `interface_view` (chain–chain), `crosslink_view`
84
+ (disulfides & metal coordination), `mutation_view`.
85
+ - **Per-residue scalar coloring:** `bfactor_view`, `putty_view` (B-factor as
86
+ tube width), `conservation_view` (evolutionary; first call hits the
87
+ ColabFold MMseqs2 API and is slow, then cached).
88
+ - **Surface chemistry / electrostatics:** `hydrophobic_surface_view`,
89
+ `electrostatic_view` (approximate), `poisson_boltzmann_view` (true PB;
90
+ needs `apbs` + `pdb2pqr`).
91
+ - **Illustrative:** `textbook_view` (cel-shaded; needs `ray`),
92
+ `cinematic_view` (fog/shadows; needs `ray`), `pointillist_view`.
93
+ - **Predicted-model confidence:** `plddt_view` (AlphaFold pLDDT in the
94
+ official palette, plus a confidence breakdown).
95
+ - **Comparing two structures:** `superposition_view(mobile, target)` —
96
+ superposes and colours the mobile structure by per-residue shift, so you
97
+ see *where* it moved rather than just an RMSD. Names the worst-shifted
98
+ residues.
99
+ - **3D printing:** `print_ribbon_view` — see below.
100
+
101
+ ## Rendering, movies and sessions
102
+
103
+ - `render(width, height, ray_trace, filename)` — the one to reach for. Returns
104
+ the image so you can look at it.
105
+ - `turntable(obj_name, frames, out_dir)` — a 360° PNG sequence plus the
106
+ ffmpeg line to assemble it. Defaults to the fast renderer; `ray_trace=True`
107
+ is for finals only (36 traced frames of a big assembly can take an hour).
108
+ - `save_session(filename)` / `load_session(filename)` — `.pse` round-trips
109
+ the whole scene (objects, colours, camera, scenes). Save before
110
+ experimenting with a scene that took effort to build.
111
+
112
+ ## Selection syntax (PyMOL)
113
+
114
+ Selections are an algebra, not just residue lists:
115
+ `chain A`, `resi 10-50`, `resn ATP`, `polymer.protein`, `organic`,
116
+ `ss H` / `ss S` (helix/strand), `byres (... around 5)`, combined with
117
+ `and`/`or`/`not` and parentheses. Cross-object selections are fine:
118
+ `(1abc or 1abc_spine)`.
119
+
120
+ ## 3D-print STL export
121
+
122
+ Two-step: configure the scene, then export.
123
+
124
+ ```
125
+ print_ribbon_view(obj_name="1ema")
126
+ print_export(obj_name="1ema", groups="1ema=(1ema or 1ema_spine)",
127
+ representation="cartoon", method="voxel", voxel_pitch=0.2)
128
+ ```
129
+
130
+ `print_ribbon_view` builds chunky β-arrows + a fat helix and a separate
131
+ `<obj>_spine` (`cartoon tube`, ignores secondary structure) that threads
132
+ unbroken through every strand→loop junction — fused on export into one
133
+ watertight solid, with the spine acting as internal rebar for rigidity.
134
+
135
+ `print_export` key arguments:
136
+
137
+ - **`representation`** — `"surface"` (default) exports the molecular
138
+ surface; **`"cartoon"`** exports the *currently displayed* cartoon/tube
139
+ geometry (what `print_ribbon_view` sets up). Using the default with a
140
+ ribbon scene silently gives you a surface blob.
141
+ - **`method`** — `"voxel"` (robust for thin tubular ribbon geometry;
142
+ `voxel_pitch` ~0.2 keeps ribbon detail), `"poisson"` (bulky chains),
143
+ `"auto"`.
144
+ - **`groups`** — `label=selection` pairs, one STL per colour. In cartoon
145
+ mode each group should name whole object(s).
146
+
147
+ ### Hard-won verification rules
148
+
149
+ - **Verify the STL itself, not the viewport.** `ray`/`png` show the live
150
+ PyMOL scene, which can look like ribbons while the exported mesh is a
151
+ surface. PyMOL cannot load STL back, so inspect the file with the
152
+ project's own `trimesh` (already a `print`-extra dependency):
153
+ check `is_watertight`, `len(mesh.split())` (want **1** component), and
154
+ to tell ribbon from surface compare bbox-fill fraction and
155
+ area/volume — a ribbon is thin (low fill, high area/volume), a surface
156
+ is a fat shell.
157
+ - **A printable result is watertight AND a single component.** Fragmented,
158
+ non-watertight output won't slice.
159
+ - The STL is in the same coordinate frame across groups — in a slicer,
160
+ load the first then add others as *parts* (don't re-centre).
161
+
162
+ ### After editing anything under `src/mcpymol/`
163
+
164
+ The MCP server imports the package at startup. Code changes are **not live**
165
+ until the user reconnects it (`/mcp` → reconnect mcpymol, or restart Claude
166
+ Code) — you cannot restart it yourself. After a reconnect, confirm the new
167
+ behavior from tool output, don't assume it loaded (a stale server returns
168
+ the old result).
169
+
170
+ ## Troubleshooting
171
+
172
+ - *"Socket connection failed"* on every tool → PyMOL plugin not running /
173
+ server disconnected. Ask the user to reconnect mcpymol via `/mcp`.
174
+ - Long `get_fastastr`/`get_chains` JSON parse errors → pre-2026-05 build.
175
+ - `poisson_boltzmann_view` fails → missing `apbs`/`pdb2pqr`.
176
+ - 3D-print tools say the `print` extra is missing → `uv sync --extra print`.
177
+ - A tool reports a file "did not appear" → PyMOL and the bridge are not on the
178
+ same machine; they exchange files through the filesystem.
179
+ - `render` returns a size complaint instead of an image → ask for a smaller
180
+ width/height, or raise `MCPYMOL_MAX_IMAGE_BYTES`.
181
+ - A scene comes out blank → check the selection with `count_atoms` before
182
+ assuming the view preset failed.
Binary file
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ # Python dependencies (pyproject.toml + uv.lock).
4
+ - package-ecosystem: "uv"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ python-deps:
10
+ patterns: ["*"]
11
+ open-pull-requests-limit: 5
12
+
13
+ # Keep the SHA-pinned GitHub Actions current.
14
+ - package-ecosystem: "github-actions"
15
+ directory: "/"
16
+ schedule:
17
+ interval: "weekly"
18
+ groups:
19
+ actions:
20
+ patterns: ["*"]
@@ -0,0 +1,123 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # Cancel superseded runs on the same ref (saves CI minutes on rapid pushes).
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ lockfile:
18
+ name: Lockfile in sync
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
22
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
23
+ with:
24
+ enable-cache: true
25
+ # Fails fast and unambiguously if uv.lock drifts from pyproject.toml,
26
+ # instead of the error hiding inside every Tests (pyX.Y) install step.
27
+ - name: uv.lock matches pyproject.toml
28
+ run: uv lock --locked
29
+
30
+ lint:
31
+ name: Lint (ruff)
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
35
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
36
+ with:
37
+ enable-cache: true
38
+ - run: uv sync --locked --all-groups
39
+ - name: ruff check
40
+ run: uv run ruff check --output-format=github .
41
+ - name: ruff format
42
+ run: uv run ruff format --check --diff .
43
+
44
+ typecheck:
45
+ name: Type check (mypy)
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
49
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
50
+ with:
51
+ enable-cache: true
52
+ # The print extra must be installed or trimesh resolves to Any under
53
+ # ignore_missing_imports, and mypy silently skips the mesh-repair code —
54
+ # the one place in the tree doing real work against a third-party API.
55
+ - run: uv sync --locked --all-groups --extra print
56
+ - run: uv run mypy
57
+
58
+ test:
59
+ name: Tests (py${{ matrix.python-version }})
60
+ runs-on: ubuntu-latest
61
+ strategy:
62
+ fail-fast: false
63
+ matrix:
64
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
65
+ steps:
66
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
67
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
68
+ with:
69
+ enable-cache: true
70
+ - name: Set up Python ${{ matrix.python-version }}
71
+ run: uv python install ${{ matrix.python-version }}
72
+ # --locked fails if uv.lock is stale relative to pyproject.toml,
73
+ # catching dependency drift before it reaches a release.
74
+ - name: Install dependencies
75
+ run: uv sync --locked --all-groups
76
+ - name: Run pytest
77
+ run: >
78
+ uv run pytest tests/ -v
79
+ --cov=mcpymol --cov-report=term-missing --cov-report=xml
80
+
81
+ print-extra:
82
+ name: Print extra imports
83
+ runs-on: ubuntu-latest
84
+ steps:
85
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
86
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
87
+ with:
88
+ enable-cache: true
89
+ # The `print` extra pulls heavy native deps (pymeshlab, scipy). This
90
+ # job just proves that code path still imports — it is silent otherwise.
91
+ - name: Install with print extra
92
+ run: uv sync --locked --extra print
93
+ - name: Import print_export
94
+ run: uv run python -c "from mcpymol.server import print_export; print('print_export import OK')"
95
+
96
+ build:
97
+ name: Build & metadata check
98
+ runs-on: ubuntu-latest
99
+ steps:
100
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
101
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
102
+ with:
103
+ enable-cache: true
104
+ - name: Build sdist and wheel
105
+ run: uv build
106
+ - name: Validate package metadata
107
+ run: uv run --with twine twine check dist/*
108
+
109
+ security:
110
+ name: Dependency audit (advisory)
111
+ runs-on: ubuntu-latest
112
+ steps:
113
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
114
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
115
+ with:
116
+ enable-cache: true
117
+ - run: uv sync --locked --all-groups
118
+ # Advisory: a CVE in a transitive dep should be visible but should not
119
+ # block unrelated PRs. Step-level continue-on-error keeps the check
120
+ # green; triage via the run log / Dependabot alerts.
121
+ - name: pip-audit
122
+ run: uv run --with pip-audit pip-audit
123
+ continue-on-error: true
@@ -0,0 +1,42 @@
1
+ name: Dependabot auto-merge
2
+
3
+ # Queues Dependabot's patch and minor updates to merge themselves once every
4
+ # required check passes. Major bumps are left for a human, because those are
5
+ # the ones that change behaviour.
6
+ #
7
+ # Auto-merge does not bypass anything: main requires nine green checks and an
8
+ # up-to-date branch, so this only removes the manual click, never the gate.
9
+
10
+ on: pull_request_target
11
+
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+
16
+ jobs:
17
+ auto-merge:
18
+ name: Enable auto-merge
19
+ runs-on: ubuntu-latest
20
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
21
+ steps:
22
+ # NB: pull_request_target runs with write access in the base repo's
23
+ # context. Nothing here checks out the PR's code, which is what makes
24
+ # that safe — do not add actions/checkout of the head ref.
25
+ - name: Inspect the update
26
+ id: metadata
27
+ uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
28
+
29
+ - name: Enable auto-merge for patch and minor updates
30
+ if: steps.metadata.outputs.update-type != 'version-update:semver-major'
31
+ run: gh pr merge --auto --rebase "$PR_URL"
32
+ env:
33
+ PR_URL: ${{ github.event.pull_request.html_url }}
34
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+
36
+ - name: Leave major updates for review
37
+ if: steps.metadata.outputs.update-type == 'version-update:semver-major'
38
+ run: |
39
+ gh pr comment "$PR_URL" --body "Major version bump — not auto-merging. Worth reading the changelog before this lands."
40
+ env:
41
+ PR_URL: ${{ github.event.pull_request.html_url }}
42
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,67 @@
1
+ name: Release
2
+
3
+ # Publishing is driven by tags, not by pushes to main: the tag is the record
4
+ # of what was released, and a PyPI version is immutable once it exists.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ name: Build & verify
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
18
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
19
+ with:
20
+ enable-cache: true
21
+
22
+ - name: Build sdist and wheel
23
+ run: uv build
24
+
25
+ # A published version can never be replaced, only superseded by another
26
+ # release. Tagging v1.5.0 while pyproject still says 1.4.0 would upload
27
+ # 1.4.0 under a v1.5.0 tag and burn the version number permanently, so
28
+ # this checks the tag against the artefacts that are about to be
29
+ # uploaded rather than against the source it was built from.
30
+ - name: Tag matches built version
31
+ run: |
32
+ version="${GITHUB_REF_NAME#v}"
33
+ if [ ! -f "dist/mcpymol-${version}.tar.gz" ]; then
34
+ echo "::error::Tag ${GITHUB_REF_NAME} implies version ${version}, but that is not what was built. Bump the version in pyproject.toml, or retag. dist/ contains:"
35
+ ls -1 dist/
36
+ exit 1
37
+ fi
38
+
39
+ - name: Validate package metadata
40
+ run: uv run --with twine twine check dist/*
41
+
42
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ if-no-files-found: error
47
+
48
+ publish:
49
+ name: Publish to PyPI
50
+ needs: build
51
+ runs-on: ubuntu-latest
52
+ # Trusted Publishing (OIDC): PyPI verifies this exact repository, workflow
53
+ # filename and environment, and hands back a short-lived token. There is
54
+ # no API token stored in the repo to leak or rotate — but it does mean the
55
+ # three names below are load-bearing and must match the pending publisher
56
+ # registered on PyPI. Renaming this file breaks publishing.
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/p/mcpymol
60
+ permissions:
61
+ id-token: write
62
+ steps:
63
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,41 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+ env/
13
+ refresh/
14
+
15
+ # Editor / OS / local agent artifacts
16
+ .DS_Store
17
+ .idea/
18
+ .vscode/
19
+ .claude/*
20
+ !.claude/skills/
21
+ .gemini/
22
+
23
+ # Build artifacts
24
+ build/
25
+ dist/
26
+
27
+ # Project stubs / leftovers
28
+ main.py
29
+ *.mc
30
+
31
+ # Structural data (downloaded on demand)
32
+ *.pdb
33
+ *.cif
34
+ *.mmtf
35
+
36
+ # Media (keep README assets)
37
+ *.mp4
38
+ *.gif
39
+ *.png
40
+ !assets/*.png
41
+ !assets/*.gif
@@ -0,0 +1,86 @@
1
+ # Local gates that mirror CI, so a formatting slip is caught before it becomes
2
+ # a red pipeline (and, in a stacked branch, a rebase of everything above it).
3
+ #
4
+ # Run `pre-commit install` once after cloning. See CONTRIBUTING.md.
5
+ #
6
+ # Why the linters run through `uv run` as `language: system` rather than
7
+ # pre-commit's own ruff/mypy repos: those would pin their own tool versions in
8
+ # this file, giving the project two sources of truth that drift apart until a
9
+ # hook passes locally and CI fails (or worse, the reverse). Going through uv
10
+ # means the hooks use exactly the versions in uv.lock — the same ones CI
11
+ # installs.
12
+
13
+ default_install_hook_types: [pre-commit, pre-push]
14
+
15
+ repos:
16
+ - repo: https://github.com/pre-commit/pre-commit-hooks
17
+ rev: v6.0.0
18
+ hooks:
19
+ # Whitespace and newline hygiene, which otherwise shows up as noise in
20
+ # unrelated diffs.
21
+ - id: trailing-whitespace
22
+ # Markdown uses two trailing spaces as a hard line break.
23
+ args: [--markdown-linebreak-ext=md]
24
+ - id: end-of-file-fixer
25
+ - id: mixed-line-ending
26
+ args: [--fix=lf]
27
+ # Config files that break the build if malformed.
28
+ - id: check-yaml
29
+ - id: check-toml
30
+ # Things that should never reach a commit.
31
+ - id: check-merge-conflict
32
+ - id: check-case-conflict
33
+ - id: debug-statements
34
+ - id: check-added-large-files
35
+ # Structures and renders are gitignored; this catches the ones that
36
+ # slip past, since a stray PDB or PNG is easy to add and hard to remove.
37
+ # Sized to catch accidents rather than curated media: a downloaded
38
+ # assembly or a ray-traced PNG runs to several MB, while the largest
39
+ # deliberate asset here is an 830 KB turntable GIF.
40
+ args: [--maxkb=1024]
41
+
42
+ - repo: local
43
+ hooks:
44
+ - id: ruff-check
45
+ name: ruff check
46
+ entry: uv run ruff check --force-exclude --fix
47
+ language: system
48
+ types_or: [python, pyi]
49
+ require_serial: true
50
+
51
+ - id: ruff-format
52
+ name: ruff format
53
+ entry: uv run ruff format --force-exclude
54
+ language: system
55
+ types_or: [python, pyi]
56
+ require_serial: true
57
+
58
+ - id: mypy
59
+ name: mypy
60
+ # mypy reads `files` and its settings from pyproject.toml, so it runs
61
+ # over the whole package rather than the staged subset — passing
62
+ # filenames would silently override that config.
63
+ entry: uv run mypy
64
+ language: system
65
+ pass_filenames: false
66
+ files: ^(src/.*\.py|pyproject\.toml)$
67
+ require_serial: true
68
+
69
+ - id: uv-lock-check
70
+ name: uv.lock in sync with pyproject.toml
71
+ # Mirrors the CI "Lockfile in sync" job: blocks the commit if the
72
+ # lockfile is stale so it never reaches a push / the test matrix.
73
+ entry: uv lock --locked
74
+ language: system
75
+ files: ^(pyproject\.toml|uv\.lock)$
76
+ pass_filenames: false
77
+
78
+ - id: pytest
79
+ name: pytest
80
+ # Deliberately pre-push, not pre-commit: the suite takes ~16 s, which
81
+ # is too slow to pay on every commit but unremarkable on a push.
82
+ entry: uv run pytest -q
83
+ language: system
84
+ pass_filenames: false
85
+ stages: [pre-push]
86
+ require_serial: true
@@ -0,0 +1 @@
1
+ 3.13