Source code for tooluniverse.pepcalc_tool

"""Pep-Calc.com peptide property calculator tool.

Pep-Calc.com (https://pep-calc.com) computes physicochemical properties for
peptides that may carry N-terminal and C-terminal chemical modifications
(e.g. N-terminal acetylation, C-terminal amidation) and non-standard /
peptoid residues. This is the synthetic / therapeutic-peptide case that a
bare-sequence calculator (such as ExPASy ProtParam) cannot model, because the
terminal group changes the molecular formula, monoisotopic / average
molecular weight, isoelectric point (pI), and extinction coefficient.

API: https://api.pep-calc.com
  GET /peptide?seq={SEQ}&N_term={N}&C_term={C}
      -> seqString, seqList, nString, cString, nName, cName, nModified,
         cModified, seqLength, formula, molecularWeight (monoisotopic),
         molecularWeightAverage
  GET /peptide/iso?seq={SEQ}&N_term={N}&C_term={C}   -> {pI}
  GET /peptide/extinction?seq={SEQ}                  -> {oxidized, reduced}

Errors are returned as JSON {message, status, errorCode} with HTTP 400.
No API key required.
"""

from __future__ import annotations

from typing import Any, Dict

import requests

from .base_tool import BaseTool
from .http_utils import request_with_retry
from .tool_registry import register_tool

_TIMEOUT = 30


[docs] @register_tool("PepCalcTool") class PepCalcTool(BaseTool): """Terminal-modification-aware peptide physicochemical properties.""" BASE_URL = "https://api.pep-calc.com" # Fields copied verbatim from the /peptide endpoint into the output record. _CORE_FIELDS = ( "seqString", "seqList", "seqLength", "nString", "cString", "nName", "cName", "nModified", "cModified", "formula", "molecularWeight", "molecularWeightAverage", )
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: seq = (arguments.get("seq") or arguments.get("sequence") or "").strip() if not seq: return {"status": "error", "error": "seq is required"} # N_term / C_term are the chemical group strings Pep-Calc expects. # Defaults match an unmodified peptide: free amine (H) and free acid (OH). n_term = str(arguments.get("N_term", "H")).strip() or "H" c_term = str(arguments.get("C_term", "OH")).strip() or "OH" params = {"seq": seq, "N_term": n_term, "C_term": c_term} # Core properties (formula, MW, modification flags) — required. core = self._get_json("/peptide", params) if core.get("status") == "error": return core core_data = core["data"] # isoelectric point — best effort (do not fail the whole call if absent). iso_data = self._get_json("/peptide/iso", params) pi_value = None if iso_data.get("status") == "success": pi_value = iso_data["data"].get("pI") # extinction coefficient — only needs the sequence. ext = self._get_json("/peptide/extinction", {"seq": seq}) ext_value = ext["data"] if ext.get("status") == "success" else None data = {field: core_data.get(field) for field in self._CORE_FIELDS} data["isoelectricPoint"] = pi_value data["extinctionCoefficient"] = ext_value return { "status": "success", "data": data, "metadata": { "source": "Pep-Calc.com", "endpoint": f"{self.BASE_URL}/peptide", "N_term": n_term, "C_term": c_term, "molecular_weight_units": "Da (monoisotopic)", "molecular_weight_average_units": "Da (average)", }, }
[docs] def _get_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]: url = f"{self.BASE_URL}{path}" try: resp = request_with_retry( requests, "GET", url, params=params, timeout=_TIMEOUT ) except Exception as exc: return {"status": "error", "error": f"Request failed: {exc}"} try: payload = resp.json() except Exception: return { "status": "error", "error": "Failed to parse JSON response", "detail": resp.text[:500], } # Pep-Calc signals errors with a JSON {message, status, errorCode} body. if isinstance(payload, dict) and "errorCode" in payload: return { "status": "error", "error": payload.get("message", "Pep-Calc API error"), "errorCode": payload.get("errorCode"), } if resp.status_code != 200: return { "status": "error", "error": f"HTTP {resp.status_code}", "detail": resp.text[:500], } return {"status": "success", "data": payload}