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.
- pubchem_finder-2.0.0/.github/CONTRIBUTING.md +58 -0
- pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- pubchem_finder-2.0.0/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
- pubchem_finder-2.0.0/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- pubchem_finder-2.0.0/.github/dependabot.yml +16 -0
- pubchem_finder-2.0.0/.github/workflows/ci.yml +94 -0
- pubchem_finder-2.0.0/.github/workflows/e2e-real.yml +133 -0
- pubchem_finder-2.0.0/.github/workflows/live.yml +34 -0
- pubchem_finder-2.0.0/.github/workflows/release.yml +126 -0
- pubchem_finder-2.0.0/.gitignore +30 -0
- pubchem_finder-2.0.0/.pre-commit-config.yaml +14 -0
- pubchem_finder-2.0.0/.python-version +1 -0
- pubchem_finder-2.0.0/CHANGELOG.md +55 -0
- pubchem_finder-2.0.0/CITATION.cff +18 -0
- pubchem_finder-2.0.0/LICENSE +21 -0
- pubchem_finder-2.0.0/Makefile +26 -0
- pubchem_finder-2.0.0/PKG-INFO +346 -0
- pubchem_finder-2.0.0/README.md +311 -0
- pubchem_finder-2.0.0/ROADMAP.md +87 -0
- pubchem_finder-2.0.0/SECURITY.md +12 -0
- pubchem_finder-2.0.0/docs/images/analogues-diff.png +0 -0
- pubchem_finder-2.0.0/docs/images/similarity-map.png +0 -0
- pubchem_finder-2.0.0/docs/images/social-preview.png +0 -0
- pubchem_finder-2.0.0/docs/images/sulfonamides-smarts.png +0 -0
- pubchem_finder-2.0.0/pyproject.toml +114 -0
- pubchem_finder-2.0.0/release-notes.md +42 -0
- pubchem_finder-2.0.0/scripts/smoke.py +326 -0
- pubchem_finder-2.0.0/src/pubchem_finder/__init__.py +10 -0
- pubchem_finder-2.0.0/src/pubchem_finder/__main__.py +8 -0
- pubchem_finder-2.0.0/src/pubchem_finder/batch.py +131 -0
- pubchem_finder-2.0.0/src/pubchem_finder/chemistry/__init__.py +6 -0
- pubchem_finder-2.0.0/src/pubchem_finder/chemistry/properties.py +42 -0
- pubchem_finder-2.0.0/src/pubchem_finder/chemistry/standardizer.py +71 -0
- pubchem_finder-2.0.0/src/pubchem_finder/cli.py +883 -0
- pubchem_finder-2.0.0/src/pubchem_finder/config.py +42 -0
- pubchem_finder-2.0.0/src/pubchem_finder/core/__init__.py +21 -0
- pubchem_finder-2.0.0/src/pubchem_finder/core/exceptions.py +29 -0
- pubchem_finder-2.0.0/src/pubchem_finder/core/fingerprint.py +121 -0
- pubchem_finder-2.0.0/src/pubchem_finder/core/molecule.py +93 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/__init__.py +19 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/cache.py +116 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/downloads.py +168 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/parsers.py +300 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/pug_rest.py +420 -0
- pubchem_finder-2.0.0/src/pubchem_finder/data/sdf.py +112 -0
- pubchem_finder-2.0.0/src/pubchem_finder/drawing.py +250 -0
- pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/__init__.py +17 -0
- pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/base.py +115 -0
- pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/maccs.py +33 -0
- pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/morgan.py +58 -0
- pubchem_finder-2.0.0/src/pubchem_finder/fingerprints/registry.py +80 -0
- pubchem_finder-2.0.0/src/pubchem_finder/index/__init__.py +16 -0
- pubchem_finder-2.0.0/src/pubchem_finder/index/builder.py +533 -0
- pubchem_finder-2.0.0/src/pubchem_finder/index/engine.py +258 -0
- pubchem_finder-2.0.0/src/pubchem_finder/index/layout.py +201 -0
- pubchem_finder-2.0.0/src/pubchem_finder/mcp_server.py +339 -0
- pubchem_finder-2.0.0/src/pubchem_finder/medchem.py +146 -0
- pubchem_finder-2.0.0/src/pubchem_finder/models.py +329 -0
- pubchem_finder-2.0.0/src/pubchem_finder/service.py +539 -0
- pubchem_finder-2.0.0/tests/__init__.py +1 -0
- pubchem_finder-2.0.0/tests/conftest.py +308 -0
- pubchem_finder-2.0.0/tests/e2e/__init__.py +0 -0
- pubchem_finder-2.0.0/tests/e2e/test_stdio.py +71 -0
- pubchem_finder-2.0.0/tests/fixtures/sample_molecules.smi +10 -0
- pubchem_finder-2.0.0/tests/integration/__init__.py +0 -0
- pubchem_finder-2.0.0/tests/integration/test_pipeline.py +223 -0
- pubchem_finder-2.0.0/tests/integration/test_workflow.py +75 -0
- pubchem_finder-2.0.0/tests/pubchem_fixtures.py +356 -0
- pubchem_finder-2.0.0/tests/unit/__init__.py +1 -0
- pubchem_finder-2.0.0/tests/unit/test_batch.py +92 -0
- pubchem_finder-2.0.0/tests/unit/test_cache.py +98 -0
- pubchem_finder-2.0.0/tests/unit/test_cli.py +452 -0
- pubchem_finder-2.0.0/tests/unit/test_downloads.py +177 -0
- pubchem_finder-2.0.0/tests/unit/test_drawing.py +108 -0
- pubchem_finder-2.0.0/tests/unit/test_fingerprints.py +233 -0
- pubchem_finder-2.0.0/tests/unit/test_index.py +286 -0
- pubchem_finder-2.0.0/tests/unit/test_mcp_server.py +238 -0
- pubchem_finder-2.0.0/tests/unit/test_medchem.py +74 -0
- pubchem_finder-2.0.0/tests/unit/test_molecule.py +97 -0
- pubchem_finder-2.0.0/tests/unit/test_parsers.py +142 -0
- pubchem_finder-2.0.0/tests/unit/test_properties.py +70 -0
- pubchem_finder-2.0.0/tests/unit/test_pubchem_tools.py +309 -0
- pubchem_finder-2.0.0/tests/unit/test_pug_rest.py +188 -0
- pubchem_finder-2.0.0/tests/unit/test_sdf.py +47 -0
- pubchem_finder-2.0.0/tests/unit/test_service.py +63 -0
- pubchem_finder-2.0.0/tests/unit/test_similarity.py +101 -0
- pubchem_finder-2.0.0/tests/unit/test_standardizer.py +113 -0
- pubchem_finder-2.0.0/tests/unit/test_substructure.py +289 -0
- 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,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,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
|