pubchem-finder 2.0.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 (90) hide show
  1. pubchem_finder-2.0.0/.github/CONTRIBUTING.md +58 -0
  2. pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
  3. pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
  5. pubchem_finder-2.0.0/.github/PULL_REQUEST_TEMPLATE.md +7 -0
  6. pubchem_finder-2.0.0/.github/dependabot.yml +16 -0
  7. pubchem_finder-2.0.0/.github/workflows/ci.yml +94 -0
  8. pubchem_finder-2.0.0/.github/workflows/e2e-real.yml +133 -0
  9. pubchem_finder-2.0.0/.github/workflows/live.yml +34 -0
  10. pubchem_finder-2.0.0/.github/workflows/release.yml +126 -0
  11. pubchem_finder-2.0.0/.gitignore +30 -0
  12. pubchem_finder-2.0.0/.pre-commit-config.yaml +14 -0
  13. pubchem_finder-2.0.0/.python-version +1 -0
  14. pubchem_finder-2.0.0/CHANGELOG.md +55 -0
  15. pubchem_finder-2.0.0/CITATION.cff +18 -0
  16. pubchem_finder-2.0.0/LICENSE +21 -0
  17. pubchem_finder-2.0.0/Makefile +26 -0
  18. pubchem_finder-2.0.0/PKG-INFO +346 -0
  19. pubchem_finder-2.0.0/README.md +311 -0
  20. pubchem_finder-2.0.0/ROADMAP.md +87 -0
  21. pubchem_finder-2.0.0/SECURITY.md +12 -0
  22. pubchem_finder-2.0.0/docs/images/analogues-diff.png +0 -0
  23. pubchem_finder-2.0.0/docs/images/similarity-map.png +0 -0
  24. pubchem_finder-2.0.0/docs/images/social-preview.png +0 -0
  25. pubchem_finder-2.0.0/docs/images/sulfonamides-smarts.png +0 -0
  26. pubchem_finder-2.0.0/pyproject.toml +114 -0
  27. pubchem_finder-2.0.0/release-notes.md +42 -0
  28. pubchem_finder-2.0.0/scripts/smoke.py +326 -0
  29. pubchem_finder-2.0.0/src/pubchem_finder/__init__.py +10 -0
  30. pubchem_finder-2.0.0/src/pubchem_finder/__main__.py +8 -0
  31. pubchem_finder-2.0.0/src/pubchem_finder/batch.py +131 -0
  32. pubchem_finder-2.0.0/src/pubchem_finder/chemistry/__init__.py +6 -0
  33. pubchem_finder-2.0.0/src/pubchem_finder/chemistry/properties.py +42 -0
  34. pubchem_finder-2.0.0/src/pubchem_finder/chemistry/standardizer.py +71 -0
  35. pubchem_finder-2.0.0/src/pubchem_finder/cli.py +883 -0
  36. pubchem_finder-2.0.0/src/pubchem_finder/config.py +42 -0
  37. pubchem_finder-2.0.0/src/pubchem_finder/core/__init__.py +21 -0
  38. pubchem_finder-2.0.0/src/pubchem_finder/core/exceptions.py +29 -0
  39. pubchem_finder-2.0.0/src/pubchem_finder/core/fingerprint.py +121 -0
  40. pubchem_finder-2.0.0/src/pubchem_finder/core/molecule.py +93 -0
  41. pubchem_finder-2.0.0/src/pubchem_finder/data/__init__.py +19 -0
  42. pubchem_finder-2.0.0/src/pubchem_finder/data/cache.py +116 -0
  43. pubchem_finder-2.0.0/src/pubchem_finder/data/downloads.py +168 -0
  44. pubchem_finder-2.0.0/src/pubchem_finder/data/parsers.py +300 -0
  45. pubchem_finder-2.0.0/src/pubchem_finder/data/pug_rest.py +420 -0
  46. pubchem_finder-2.0.0/src/pubchem_finder/data/sdf.py +112 -0
  47. pubchem_finder-2.0.0/src/pubchem_finder/drawing.py +250 -0
  48. pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/__init__.py +17 -0
  49. pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/base.py +115 -0
  50. pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/maccs.py +33 -0
  51. pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/morgan.py +58 -0
  52. pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/registry.py +80 -0
  53. pubchem_finder-2.0.0/src/pubchem_finder/index/__init__.py +16 -0
  54. pubchem_finder-2.0.0/src/pubchem_finder/index/builder.py +533 -0
  55. pubchem_finder-2.0.0/src/pubchem_finder/index/engine.py +258 -0
  56. pubchem_finder-2.0.0/src/pubchem_finder/index/layout.py +201 -0
  57. pubchem_finder-2.0.0/src/pubchem_finder/mcp_server.py +339 -0
  58. pubchem_finder-2.0.0/src/pubchem_finder/medchem.py +146 -0
  59. pubchem_finder-2.0.0/src/pubchem_finder/models.py +329 -0
  60. pubchem_finder-2.0.0/src/pubchem_finder/service.py +539 -0
  61. pubchem_finder-2.0.0/tests/__init__.py +1 -0
  62. pubchem_finder-2.0.0/tests/conftest.py +308 -0
  63. pubchem_finder-2.0.0/tests/e2e/__init__.py +0 -0
  64. pubchem_finder-2.0.0/tests/e2e/test_stdio.py +71 -0
  65. pubchem_finder-2.0.0/tests/fixtures/sample_molecules.smi +10 -0
  66. pubchem_finder-2.0.0/tests/integration/__init__.py +0 -0
  67. pubchem_finder-2.0.0/tests/integration/test_pipeline.py +223 -0
  68. pubchem_finder-2.0.0/tests/integration/test_workflow.py +75 -0
  69. pubchem_finder-2.0.0/tests/pubchem_fixtures.py +356 -0
  70. pubchem_finder-2.0.0/tests/unit/__init__.py +1 -0
  71. pubchem_finder-2.0.0/tests/unit/test_batch.py +92 -0
  72. pubchem_finder-2.0.0/tests/unit/test_cache.py +98 -0
  73. pubchem_finder-2.0.0/tests/unit/test_cli.py +452 -0
  74. pubchem_finder-2.0.0/tests/unit/test_downloads.py +177 -0
  75. pubchem_finder-2.0.0/tests/unit/test_drawing.py +108 -0
  76. pubchem_finder-2.0.0/tests/unit/test_fingerprints.py +233 -0
  77. pubchem_finder-2.0.0/tests/unit/test_index.py +286 -0
  78. pubchem_finder-2.0.0/tests/unit/test_mcp_server.py +238 -0
  79. pubchem_finder-2.0.0/tests/unit/test_medchem.py +74 -0
  80. pubchem_finder-2.0.0/tests/unit/test_molecule.py +97 -0
  81. pubchem_finder-2.0.0/tests/unit/test_parsers.py +142 -0
  82. pubchem_finder-2.0.0/tests/unit/test_properties.py +70 -0
  83. pubchem_finder-2.0.0/tests/unit/test_pubchem_tools.py +309 -0
  84. pubchem_finder-2.0.0/tests/unit/test_pug_rest.py +188 -0
  85. pubchem_finder-2.0.0/tests/unit/test_sdf.py +47 -0
  86. pubchem_finder-2.0.0/tests/unit/test_service.py +63 -0
  87. pubchem_finder-2.0.0/tests/unit/test_similarity.py +101 -0
  88. pubchem_finder-2.0.0/tests/unit/test_standardizer.py +113 -0
  89. pubchem_finder-2.0.0/tests/unit/test_substructure.py +289 -0
  90. pubchem_finder-2.0.0/uv.lock +2418 -0
@@ -0,0 +1,58 @@
1
+ # Contributing
2
+
3
+ Thanks for helping with PubChem Finder. The project is small; this page is the whole process.
4
+
5
+ ## Setup
6
+
7
+ Install [uv](https://docs.astral.sh/uv/), then:
8
+
9
+ ```bash
10
+ git clone https://github.com/MysterionRise/pubchem-finder.git
11
+ cd pubchem-finder
12
+ uv sync # creates .venv with the project and dev tools (RDKit comes from PyPI wheels)
13
+ make check # ruff lint + format check, mypy --strict, pytest with coverage
14
+ ```
15
+
16
+ Python 3.11, 3.12 and 3.13 are supported and tested in CI.
17
+
18
+ ## Workflow
19
+
20
+ 1. Branch from `master`.
21
+ 2. Make the change with tests. Unit tests go in `tests/unit/`, end-to-end flows in
22
+ `tests/integration/`, and tests that start the real MCP server process in `tests/e2e/`.
23
+ Tests must run offline; mark anything slow with `@pytest.mark.slow` and anything that
24
+ calls PubChem with `@pytest.mark.live`.
25
+ 3. Run `make check`. CI runs the same commands, so a green local run means a green PR.
26
+ 4. Open a pull request. Keep it focused and describe what changed and why.
27
+
28
+ ## What CI checks
29
+
30
+ - **CI** (every push and pull request): lint, format and `mypy --strict` on Linux; the test
31
+ suite on Linux (Python 3.11, 3.12, 3.13), macOS and Windows; and a `package` job on all
32
+ three that builds the wheel, installs it into a clean environment with the newest
33
+ dependencies from PyPI, and runs `scripts/smoke.py` against the installed command.
34
+ - **Live PubChem** (weekly and on demand): the `live` tests against the real PubChem
35
+ services. Run them locally with `PUBCHEM_LIVE=1 uv run pytest -m live`.
36
+ - **Real-data end-to-end** (on demand, from the Actions tab): downloads real PubChem SDF
37
+ chunks, builds an index with substructure search, runs every CLI command and every MCP
38
+ tool against it and against PubChem, and writes build time, file sizes, memory and query
39
+ timings to the job summary. Locally: `uv run pubchem-finder index build --limit 1
40
+ --with-substructure`, then `uv run python scripts/smoke.py --home <data dir> --live`.
41
+
42
+ ## Code style
43
+
44
+ - `ruff format` decides formatting; `ruff check` decides lint. Both run in CI and as
45
+ pre-commit hooks (`uv tool install pre-commit && uv run pre-commit install`).
46
+ - `mypy --strict` must pass. RDKit is untyped; keep its objects behind small, typed
47
+ wrappers rather than sprinkling `Any` through the codebase.
48
+ - Public functions get a short docstring. Comment the why, not the what.
49
+
50
+ ## Dependencies
51
+
52
+ Add runtime dependencies with `uv add <package>` and dev tools with `uv add --dev <package>`,
53
+ and commit the updated `uv.lock`. Do not add a dependency the code does not import yet.
54
+
55
+ ## Reporting problems
56
+
57
+ Open an issue with the SMILES or input that triggers it, the command you ran, and the
58
+ versions printed by `uv run python -c "import rdkit, pubchem_finder; print(rdkit.__version__, pubchem_finder.__version__)"`.
@@ -0,0 +1,55 @@
1
+ name: Bug report
2
+ description: Something does not work as documented
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened
9
+ description: What you ran, what you expected, and what happened instead.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: command
14
+ attributes:
15
+ label: Command or MCP request
16
+ description: The exact command (or the tool call and arguments), and the SMILES or input that triggers it.
17
+ render: shell
18
+ validations:
19
+ required: true
20
+ - type: textarea
21
+ id: output
22
+ attributes:
23
+ label: Output
24
+ description: The full error or output. For the MCP server, include what your client shows and any stderr log.
25
+ render: text
26
+ - type: input
27
+ id: version
28
+ attributes:
29
+ label: pubchem-finder version
30
+ description: Output of `pubchem-finder --version`
31
+ validations:
32
+ required: true
33
+ - type: dropdown
34
+ id: install
35
+ attributes:
36
+ label: How you installed it
37
+ options:
38
+ - uvx / uv tool install (PyPI)
39
+ - uvx --from git+https://...
40
+ - pip
41
+ - from a clone (uv sync)
42
+ validations:
43
+ required: true
44
+ - type: input
45
+ id: platform
46
+ attributes:
47
+ label: Operating system and Python
48
+ placeholder: macOS 15 (Apple Silicon), Python 3.12
49
+ validations:
50
+ required: true
51
+ - type: input
52
+ id: client
53
+ attributes:
54
+ label: MCP client, if relevant
55
+ placeholder: Claude Desktop 1.x, Claude Code 2.x, ...
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security issue
4
+ url: https://github.com/MysterionRise/pubchem-finder/security/advisories/new
5
+ about: Report security problems privately, not as a public issue.
@@ -0,0 +1,16 @@
1
+ name: Feature request
2
+ description: Suggest a tool, command or improvement
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: What are you trying to do?
9
+ description: The chemistry question or workflow, and where the current tools fall short.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: idea
14
+ attributes:
15
+ label: What would help?
16
+ description: The tool, command or option you have in mind, with an example input and the result you would expect.
@@ -0,0 +1,7 @@
1
+ ## What and why
2
+
3
+ <!-- What this changes and the problem it solves. Link the issue if there is one. -->
4
+
5
+ ## How it was tested
6
+
7
+ <!-- `make check` passes; anything else you ran (live tests, smoke script, a real index). -->
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ python-dependencies:
9
+ patterns: ["*"]
10
+ - package-ecosystem: github-actions
11
+ directory: /
12
+ schedule:
13
+ interval: weekly
14
+ groups:
15
+ github-actions:
16
+ patterns: ["*"]
@@ -0,0 +1,94 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main, "claude/**", "feature/**"]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: test (${{ matrix.os }}, ${{ matrix.python-version }})
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest]
21
+ python-version: ["3.11", "3.12", "3.13"]
22
+ include:
23
+ - os: macos-latest
24
+ python-version: "3.12"
25
+ - os: windows-latest
26
+ python-version: "3.12"
27
+
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v6
33
+ with:
34
+ enable-cache: true
35
+ python-version: ${{ matrix.python-version }}
36
+
37
+ - name: Install project
38
+ run: uv sync --locked
39
+
40
+ - name: Lint
41
+ if: runner.os == 'Linux'
42
+ run: uv run ruff check .
43
+
44
+ - name: Format check
45
+ if: runner.os == 'Linux'
46
+ run: uv run ruff format --check .
47
+
48
+ - name: Type check
49
+ if: runner.os == 'Linux'
50
+ run: uv run mypy
51
+
52
+ - name: Test
53
+ run: uv run pytest -m "not slow" --cov --cov-report=term-missing --cov-report=xml
54
+
55
+ - name: Upload coverage report
56
+ if: runner.os == 'Linux' && matrix.python-version == '3.12'
57
+ uses: actions/upload-artifact@v4
58
+ with:
59
+ name: coverage-xml
60
+ path: coverage.xml
61
+
62
+ # Build the wheel, install it the way a user would (fresh environment, newest
63
+ # compatible dependencies from PyPI, not the lockfile) and smoke-test the
64
+ # installed command and MCP server.
65
+ package:
66
+ name: package (${{ matrix.os }})
67
+ runs-on: ${{ matrix.os }}
68
+ strategy:
69
+ fail-fast: false
70
+ matrix:
71
+ os: [ubuntu-latest, macos-latest, windows-latest]
72
+
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+
76
+ - name: Install uv
77
+ uses: astral-sh/setup-uv@v6
78
+ with:
79
+ python-version: "3.12"
80
+
81
+ - name: Build sdist and wheel
82
+ run: uv build
83
+
84
+ - name: Install the wheel into a clean environment
85
+ shell: bash
86
+ run: |
87
+ uv venv .smoke --python 3.12
88
+ uv pip install --python .smoke dist/*.whl
89
+
90
+ - name: Smoke test
91
+ shell: bash
92
+ run: |
93
+ if [ "$RUNNER_OS" = "Windows" ]; then bin=.smoke/Scripts; else bin=.smoke/bin; fi
94
+ "$bin/python" scripts/smoke.py --command "$bin/pubchem-finder"
@@ -0,0 +1,133 @@
1
+ name: Real-data end-to-end
2
+
3
+ # Builds a real index from PubChem's own SDF files and exercises every CLI
4
+ # command and MCP tool against it and against the live PubChem services.
5
+ # The job summary records build time, file sizes, memory and query timings.
6
+
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ chunks:
11
+ description: "PubChem SDF chunks to index (about 500,000 compounds each)"
12
+ default: "1"
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ e2e:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 90
21
+ env:
22
+ CHUNKS: ${{ inputs.chunks }}
23
+ PUBCHEM_FINDER_CACHE: "0"
24
+ ASPIRIN: "CC(=O)Oc1ccccc1C(=O)O"
25
+ INDOLE: "c1ccc2[nH]ccc2c1"
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v6
31
+ with:
32
+ enable-cache: true
33
+ python-version: "3.12"
34
+
35
+ - name: Install project
36
+ run: uv sync --locked
37
+
38
+ - name: Prepare
39
+ run: |
40
+ echo "PUBCHEM_FINDER_HOME=$RUNNER_TEMP/pubchem-finder" >> "$GITHUB_ENV"
41
+ mkdir -p out
42
+
43
+ - name: Build the index from PubChem (download, fingerprints, substructure)
44
+ run: |
45
+ start=$(date +%s)
46
+ uv run pubchem-finder index build --limit "$CHUNKS" --with-substructure --workers 2 2>&1 | tee out/build.log
47
+ echo "BUILD_SECONDS=$(( $(date +%s) - start ))" >> "$GITHUB_ENV"
48
+ uv run pubchem-finder index status --json | tee out/status.json
49
+ jq -e '.state == "ready" and .substructure' out/status.json
50
+
51
+ - name: Local similarity and substructure search
52
+ run: |
53
+ uv run pubchem-finder search "$ASPIRIN" --threshold 0.7 --top 20 --json > out/search-aspirin.json
54
+ jq -e '.hits[0].cid == 2244 and .hits[0].similarity == 1' out/search-aspirin.json
55
+ uv run pubchem-finder search "$ASPIRIN" --threshold 0.6 --group-by-scaffold | tee out/search-scaffolds.txt
56
+ uv run pubchem-finder substructure "$INDOLE" --max 24 --json > out/substructure-indole.json
57
+ jq -e '.hits | length > 0' out/substructure-indole.json
58
+ uv run pubchem-finder substructure "[NX3][SX4](=O)(=O)c" --smarts --max 24 --json > out/substructure-sulfonamide.json
59
+ jq -e '.hits | length > 0' out/substructure-sulfonamide.json
60
+ printf '%s aspirin\nCN1C=NC2=C1C(=O)N(C(=O)N2C)C caffeine\nnot-a-smiles broken\n' "$ASPIRIN" > out/queries.smi
61
+ uv run pubchem-finder search -i out/queries.smi -o out/batch-hits.csv --threshold 0.7 --top 5
62
+ grep -q "^aspirin," out/batch-hits.csv
63
+
64
+ - name: Live PubChem commands
65
+ run: |
66
+ uv run pubchem-finder lookup caffeine --json > out/lookup-caffeine.json
67
+ jq -e '.compounds[0].cid == 2519' out/lookup-caffeine.json
68
+ uv run pubchem-finder pubchem synonyms 2244 --json > out/synonyms-2244.json
69
+ jq -e '.cas_numbers | index("50-78-2")' out/synonyms-2244.json
70
+ uv run pubchem-finder pubchem annotations 2244 --heading "GHS Classification" --json > out/ghs-2244.json
71
+ jq -e '.annotations | length > 0' out/ghs-2244.json
72
+ uv run pubchem-finder pubchem bioactivity 2244 --json > out/bioactivity-2244.json
73
+ jq -e '.n_assays > 0' out/bioactivity-2244.json
74
+ uv run pubchem-finder pubchem substructure "$INDOLE" --max 10 --json > out/pubchem-substructure-indole.json
75
+
76
+ - name: Pictures and triage
77
+ run: |
78
+ uv run pubchem-finder triage "$ASPIRIN" --json > out/triage-aspirin.json
79
+ uv run pubchem-finder draw $(jq -r '.hits[:8][].smiles' out/search-aspirin.json) \
80
+ --highlight diff --reference "$ASPIRIN" -o out/aspirin-analogues.png
81
+ uv run pubchem-finder draw $(jq -r '.hits[:8][].smiles' out/substructure-sulfonamide.json) \
82
+ --highlight smarts --smarts "[NX3][SX4](=O)(=O)c" -o out/sulfonamides.png
83
+
84
+ - name: Every MCP tool over stdio
85
+ run: uv run python scripts/smoke.py --home "$PUBCHEM_FINDER_HOME" --live
86
+
87
+ - name: Measure query timings
88
+ run: |
89
+ uv run python - <<'PY' | tee -a "$GITHUB_STEP_SUMMARY"
90
+ import json, os, time
91
+ from pathlib import Path
92
+ from pubchem_finder import service
93
+ from pubchem_finder.index.engine import LocalIndex
94
+
95
+ home = Path(os.environ["PUBCHEM_FINDER_HOME"])
96
+ status = json.loads(Path("out/status.json").read_text())
97
+ du = lambda p: sum(f.stat().st_size for f in p.rglob("*") if f.is_file())
98
+ mb = lambda n: f"{n / 1e6:,.0f} MB"
99
+ print("### Real-data index\n")
100
+ print("| | |\n|---|---|")
101
+ print(f"| PubChem chunks | {os.environ['CHUNKS']} |")
102
+ print(f"| Compounds indexed | {status['n_compounds']:,} ({status['n_failed']:,} skipped) |")
103
+ print(f"| Build (download, fingerprints, substructure, merge) | {int(os.environ['BUILD_SECONDS'])} s on 2 workers |")
104
+ print(f"| Similarity index file | {mb(status['size_bytes'])} |")
105
+ print(f"| Substructure index file | {mb(status['substructure_size_bytes'])} |")
106
+ print(f"| Chunk files (Parquet metadata + per-chunk fingerprints) | {mb(du(home / 'chunks'))} |")
107
+ print(f"| RAM for in-memory similarity / substructure | {mb(status['memory_bytes'])} / {mb(status['substructure_memory_bytes'])} |")
108
+ queries = {
109
+ "similarity, aspirin, top 20 >= 0.7": lambda idx: idx.search(
110
+ service.parse_smiles(os.environ["ASPIRIN"]), 0.7, 20),
111
+ "substructure, indole, 50 hits": lambda idx: idx.substructure(
112
+ service.parse_query(os.environ["INDOLE"]), 50, 100_000),
113
+ "substructure, aryl sulfonamide (SMARTS), 50 hits": lambda idx: idx.substructure(
114
+ service.parse_query("[NX3][SX4](=O)(=O)c", "smarts"), 50, 100_000),
115
+ }
116
+ print("\n| Query | Mode | First call (loads index) | Then, per query |\n|---|---|---|---|")
117
+ for mode in ("memory", "disk"):
118
+ idx = LocalIndex(home, mode=mode)
119
+ for name, run in queries.items():
120
+ t = time.perf_counter(); run(idx); first = time.perf_counter() - t
121
+ t = time.perf_counter()
122
+ for _ in range(5):
123
+ run(idx)
124
+ per = (time.perf_counter() - t) / 5
125
+ print(f"| {name} | {mode} | {first * 1000:,.0f} ms | {per * 1000:,.1f} ms |")
126
+ PY
127
+
128
+ - name: Upload outputs
129
+ if: always()
130
+ uses: actions/upload-artifact@v4
131
+ with:
132
+ name: e2e-real-outputs
133
+ path: out/
@@ -0,0 +1,34 @@
1
+ name: Live PubChem
2
+
3
+ # The unit tests run offline against recorded responses. This workflow runs the
4
+ # tests marked `live` against the real PubChem services, on demand and weekly,
5
+ # so a change on PubChem's side shows up here before users hit it.
6
+
7
+ on:
8
+ workflow_dispatch:
9
+ schedule:
10
+ - cron: "17 6 * * 1" # Mondays 06:17 UTC
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ live:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 20
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v6
24
+ with:
25
+ enable-cache: true
26
+ python-version: "3.12"
27
+
28
+ - name: Install project
29
+ run: uv sync --locked
30
+
31
+ - name: Live tests
32
+ env:
33
+ PUBCHEM_LIVE: "1"
34
+ run: uv run pytest -m live -v
@@ -0,0 +1,126 @@
1
+ name: Release
2
+
3
+ # Run from the Actions tab: "Release" -> "Run workflow".
4
+ # With dry_run (the default) it only builds and tests the package. Without it, it
5
+ # publishes to PyPI with trusted publishing, then tags the commit and creates the
6
+ # GitHub Release with the CHANGELOG section as notes.
7
+ #
8
+ # One-time setup on pypi.org (Account -> Publishing -> Add a pending publisher):
9
+ # project pubchem-finder, owner MysterionRise, repository pubchem-finder,
10
+ # workflow release.yml, environment pypi.
11
+
12
+ on:
13
+ workflow_dispatch:
14
+ inputs:
15
+ version:
16
+ description: "Version to release, without the v (must match pyproject.toml)"
17
+ required: true
18
+ dry_run:
19
+ description: "Build and test only; do not publish"
20
+ type: boolean
21
+ default: true
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ env:
30
+ VERSION: ${{ inputs.version }}
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - name: Install uv
35
+ uses: astral-sh/setup-uv@v6
36
+ with:
37
+ python-version: "3.12"
38
+
39
+ - name: Check the version and changelog
40
+ run: |
41
+ project=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
42
+ if [ "$project" != "$VERSION" ]; then
43
+ echo "::error::pyproject.toml says $project, not $VERSION"; exit 1
44
+ fi
45
+ if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" > /dev/null; then
46
+ echo "::error::tag v$VERSION already exists"; exit 1
47
+ fi
48
+ awk -v v="$VERSION" '
49
+ index($0, "## [" v "]") == 1 { on = 1; next }
50
+ on && /^## / { exit }
51
+ on { print }
52
+ ' CHANGELOG.md > release-notes.md
53
+ if [ ! -s release-notes.md ]; then
54
+ echo "::error::CHANGELOG.md has no section for $VERSION"; exit 1
55
+ fi
56
+
57
+ - name: Lint, type-check and test
58
+ run: |
59
+ uv sync --locked
60
+ uv run ruff check .
61
+ uv run ruff format --check .
62
+ uv run mypy
63
+ uv run pytest -m "not slow"
64
+
65
+ - name: Build sdist and wheel
66
+ run: uv build
67
+
68
+ - name: Smoke-test the wheel in a clean environment
69
+ run: |
70
+ uv venv .smoke --python 3.12
71
+ uv pip install --python .smoke dist/*.whl
72
+ .smoke/bin/python scripts/smoke.py --command .smoke/bin/pubchem-finder
73
+
74
+ - uses: actions/upload-artifact@v4
75
+ with:
76
+ name: dist
77
+ path: dist/
78
+
79
+ - uses: actions/upload-artifact@v4
80
+ with:
81
+ name: release-notes
82
+ path: release-notes.md
83
+
84
+ publish:
85
+ needs: build
86
+ if: ${{ !inputs.dry_run }}
87
+ runs-on: ubuntu-latest
88
+ environment:
89
+ name: pypi
90
+ url: https://pypi.org/project/pubchem-finder/${{ inputs.version }}/
91
+ permissions:
92
+ id-token: write # trusted publishing
93
+ steps:
94
+ - uses: actions/download-artifact@v4
95
+ with:
96
+ name: dist
97
+ path: dist/
98
+
99
+ - name: Publish to PyPI
100
+ uses: pypa/gh-action-pypi-publish@release/v1
101
+
102
+ github-release:
103
+ needs: publish
104
+ runs-on: ubuntu-latest
105
+ permissions:
106
+ contents: write
107
+ env:
108
+ VERSION: ${{ inputs.version }}
109
+ GH_TOKEN: ${{ github.token }}
110
+ steps:
111
+ - uses: actions/download-artifact@v4
112
+ with:
113
+ name: dist
114
+ path: dist/
115
+
116
+ - uses: actions/download-artifact@v4
117
+ with:
118
+ name: release-notes
119
+
120
+ - name: Tag and create the GitHub Release
121
+ run: |
122
+ gh release create "v$VERSION" dist/* \
123
+ --repo "$GITHUB_REPOSITORY" \
124
+ --target "$GITHUB_SHA" \
125
+ --title "v$VERSION" \
126
+ --notes-file release-notes.md
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Environments
9
+ .venv/
10
+ venv/
11
+ .env
12
+
13
+ # Tooling caches
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ .coverage.*
19
+ htmlcov/
20
+ coverage.xml
21
+
22
+ # IDE
23
+ .idea/
24
+ .vscode/
25
+
26
+ # Data and indexes (never commit PubChem dumps or fingerprint files)
27
+ *.sdf.gz
28
+ *.h5
29
+ *.parquet
30
+ /data/
@@ -0,0 +1,14 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.14.1
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/pre-commit-hooks
9
+ rev: v5.0.0
10
+ hooks:
11
+ - id: end-of-file-fixer
12
+ - id: trailing-whitespace
13
+ - id: check-yaml
14
+ - id: check-toml
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [2.0.0] - 2026-10-22
8
+
9
+ A complete rewrite. PubChem Finder is now a command-line tool, a Python library and an
10
+ MCP server that lets Claude (or any MCP client) search PubChem by chemical structure,
11
+ with a local index you can build on a laptop.
12
+
13
+ ### Added
14
+
15
+ - **MCP server over stdio** (`pubchem-finder mcp`) with 17 tools for Claude Desktop,
16
+ Claude Code and other MCP clients.
17
+ - **Local similarity search.** `index build` downloads PubChem's SDF files (or indexes
18
+ your own), fingerprints them with RDKit (ECFP4 by default) into an
19
+ [FPSim2](https://github.com/chembl/FPSim2) index with Parquet metadata, and searches
20
+ it exactly, in memory or from disk. Builds run in parallel and resume after an
21
+ interruption.
22
+ - **Local substructure search** (`index build --with-substructure`, `substructure`):
23
+ an RDKit pattern-fingerprint screen, then an exact atom-by-atom match. SMILES or
24
+ SMARTS queries, results in CID order.
25
+ - **PubChem knowledge tools:** lookup by name, CID, SMILES or InChIKey; PubChem's own
26
+ similarity, substructure and identity searches (hits re-scored with ECFP4); synonyms
27
+ and CAS numbers; cross-references (registry IDs, PubMed, patents); descriptions;
28
+ curated page sections such as GHS classification, pharmacology and toxicity, each
29
+ attributed to its source; bioassay summaries. Responses are cached for 7 days.
30
+ - **Chemistry tools:** RDKit descriptors and Lipinski check, standardization, ECFP4 and
31
+ MACCS comparison, med-chem triage (QED, synthetic accessibility, Lipinski/Veber,
32
+ PAINS/Brenk/NIH alerts, Murcko scaffold), and structure drawings that highlight a
33
+ SMARTS pattern, what changed relative to a reference, or a similarity map.
34
+ - **Batch mode** for `props`, `standardize`, `triage`, `search` and `substructure`:
35
+ `.smi`, `.csv` or `.tsv` in, CSV out, with an error column for bad rows.
36
+ - **Testing:** the suite runs on Linux, macOS and Windows; the built wheel is installed
37
+ and smoke-tested on all three; live PubChem tests run weekly; a real-data workflow
38
+ builds an index from PubChem's own files and calls every tool.
39
+
40
+ ### Changed
41
+
42
+ - Python 3.11 to 3.13, packaged with uv and hatchling, linted with ruff, type-checked
43
+ with mypy in strict mode.
44
+
45
+ ### Removed
46
+
47
+ - The 2020 Indigo + Elasticsearch indexer, the cookiecutter template and the Docker
48
+ files. They remain in the git history before commit `06619d9`.
49
+
50
+ ## 1.x (2020)
51
+
52
+ An Indigo + Elasticsearch indexer for PubChem SDF dumps. It was never released as a
53
+ package.
54
+
55
+ [2.0.0]: https://github.com/MysterionRise/pubchem-finder/releases/tag/v2.0.0
@@ -0,0 +1,18 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use PubChem Finder in your work, please cite it as below, and cite PubChem for the data (see the README)."
3
+ title: "PubChem Finder: search PubChem by chemical structure from the command line and from MCP clients"
4
+ type: software
5
+ authors:
6
+ - family-names: Perikov
7
+ given-names: Konstantin
8
+ version: 2.0.0
9
+ date-released: "2026-10-22"
10
+ license: MIT
11
+ repository-code: "https://github.com/MysterionRise/pubchem-finder"
12
+ keywords:
13
+ - cheminformatics
14
+ - PubChem
15
+ - RDKit
16
+ - similarity search
17
+ - substructure search
18
+ - Model Context Protocol