Source code for tooluniverse.cluspro_tool

"""ClusPro peptide-protein docking tool (submit; requires a free academic key).

ClusPro (https://cluspro.bu.edu, Vajda/Kozakov lab) is the standard rigid-body
docking server and has a native **peptide docking mode**. It exposes a real HTTP
REST API (``/api.php`` submit) documented in the open-source ``cluspro-api``
package; this tool replicates that flow exactly (sorted ``key+value`` HMAC-MD5
signature). Docking is asynchronous (hours); this tool SUBMITS a job and returns
the ClusPro job id — retrieve results from the ClusPro account/results page.

Access: ClusPro is FREE for academic / government / non-profit use (PIPER is
licensed to Acpharis/Schrodinger for commercial use). Create a free account at
cluspro.bu.edu, then copy your username + API secret from the API tab and set
``CLUSPRO_USERNAME`` and ``CLUSPRO_API_SECRET`` in the environment.

NOTE: ClusPro peptide mode targets SHORT peptides (motif-based; typically up to
~30 residues). It docks the peptide against a receptor given as a 4-letter PDB
code (or an uploaded structure — not exposed here).
"""

import hashlib
import hmac
import os
from typing import Any, Dict

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

_API_URL = "https://cluspro.bu.edu/api.php"
_TIMEOUT = 30


def _err(message: str, **extra: Any) -> Dict[str, Any]:
    out: Dict[str, Any] = {"status": "error", "error": message}
    out.update(extra)
    return out


def _make_sig(form: Dict[str, Any], secret: str) -> str:
    """Replicate cluspro-api make_sig: sorted key+value concat, HMAC-MD5 hex."""
    msg = "".join(f"{k}{form[k]}" for k in sorted(form) if form[k] is not None)
    return hmac.new(
        secret.encode("utf-8"), msg.encode("utf-8"), hashlib.md5
    ).hexdigest()


[docs] @register_tool( "ClusProSubmitPeptideDockingTool", config={ "name": "ClusPro_submit_peptide_docking", "type": "ClusProSubmitPeptideDockingTool", "description": ( "Submit a peptide-protein docking job to ClusPro (peptide mode) and " "return the ClusPro job id. Docks a short peptide (motif + sequence) " "against a receptor given by 4-letter PDB code. Docking is " "asynchronous (hours); retrieve clustered poses + scores from your " "ClusPro results page. Requires a FREE academic ClusPro account: set " "CLUSPRO_USERNAME and CLUSPRO_API_SECRET. Peptide mode is for SHORT " "peptides (~<=30 residues)." ), "parameter": { "type": "object", "properties": { "receptor_pdb_id": { "type": "string", "description": "4-letter PDB code of the receptor protein, e.g. '1A2K'.", }, "peptide_sequence": { "type": "string", "description": "Peptide amino-acid sequence (1-letter), e.g. 'KGRRL'. Short peptides only.", }, "peptide_motif": { "type": ["string", "null"], "description": ( "Peptide motif for PDB fragment search (X = wildcard), e.g. " "'KXRRL'. Defaults to peptide_sequence if omitted." ), }, "jobname": { "type": ["string", "null"], "description": "Optional job name (defaults to a ClusPro job number).", }, }, "required": ["receptor_pdb_id", "peptide_sequence"], }, "required_api_keys": ["CLUSPRO_USERNAME", "CLUSPRO_API_SECRET"], "return_schema": { "oneOf": [ { "type": "object", "properties": { "status": {"type": "string", "enum": ["success"]}, "data": { "type": "object", "properties": {"job_id": {"type": ["string", "integer"]}}, }, "metadata": {"type": "object"}, }, "required": ["status", "data"], }, { "type": "object", "properties": { "status": {"type": "string", "enum": ["error"]}, "error": {"type": "string"}, }, "required": ["status", "error"], }, ] }, "test_examples": [ { "receptor_pdb_id": "1A2K", "peptide_sequence": "KGRRL", "peptide_motif": "KXRRL", } ], }, ) class ClusProSubmitPeptideDockingTool(BaseTool): """Submit a ClusPro peptide-docking job; returns the job id."""
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: username = os.environ.get("CLUSPRO_USERNAME") secret = os.environ.get("CLUSPRO_API_SECRET") if not username or not secret: return _err( "Requires a free academic ClusPro account: set CLUSPRO_USERNAME " "and CLUSPRO_API_SECRET (register at cluspro.bu.edu, copy from the API tab)." ) recpdb = (arguments.get("receptor_pdb_id") or "").strip() pepseq = (arguments.get("peptide_sequence") or "").strip().upper() if not recpdb: return _err("receptor_pdb_id (4-letter PDB code) is required") if not pepseq: return _err("peptide_sequence is required") pepmot = (arguments.get("peptide_motif") or pepseq).strip().upper() # Form replicates cluspro-api peptide-mode submission with a PDB-code receptor. form: Dict[str, Any] = { "username": username, "recpdb": recpdb, "userecpdbid": "1", "rec-input-type": "pdb", "useligpdbid": "1", "pepmot": pepmot, "pepseq": pepseq, "peptidemode": "1", "usepeptide": "1", "userecrepfile": "0", "useligrepfile": "0", "userestraints": "0", "usesaxs": "0", } jobname = arguments.get("jobname") if jobname: form["jobname"] = str(jobname) form["sig"] = _make_sig(form, secret) try: resp = requests.post(_API_URL, data=form, timeout=_TIMEOUT) resp.raise_for_status() result = resp.json() except requests.RequestException as exc: return _err(f"ClusPro submission failed: {exc}", url=_API_URL) except ValueError as exc: return _err(f"ClusPro returned non-JSON: {exc}", url=_API_URL) if isinstance(result, dict) and result.get("status") == "success": return { "status": "success", "data": {"job_id": result.get("id")}, "metadata": { "source": "ClusPro peptide docking", "url": _API_URL, "receptor_pdb_id": recpdb, "peptide_sequence": pepseq, "note": "Asynchronous job; retrieve poses+scores from your ClusPro results page.", }, } errors = result.get("errors") if isinstance(result, dict) else result return _err(f"ClusPro rejected the job: {errors}", url=_API_URL)