Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
"cbrt",
"cheminformatics",
"cofactors",
"CONECT",
"dataframe",
"dateutil",
"deeporigin",
"docstrings",
"drugability",
"dtos",
"ebysf",
"emeq",
"ensembl",
"fasta",
Expand All @@ -46,6 +48,7 @@
"hbond",
"herg",
"HETATM",
"hetatms",
"inchi",
"interpro",
"isin",
Expand Down
1 change: 1 addition & 0 deletions src/drug_discovery/abfe.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ def from_dto(
"solvation_xml_ligand_file_path", ""
),
system_pdb_path="",
solute_pdb_path=prepared_system_input.get("solute_pdb_file_path"),
protein_id=prepared_system_input.get("protein_id")
or metadata.get("protein_id"),
ligand1_id=prepared_system_input.get("ligand1_id")
Expand Down
45 changes: 35 additions & 10 deletions src/drug_discovery/structures/prepared_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class PreparedSystem:
binding_xml_path: Remote path to the binding XML file.
solvation_xml_path: Remote path to the solvation XML file.
system_pdb_path: Remote path to the system PDB file.
solute_pdb_path: Remote path to the solute-only PDB file, if present.
protein_id: Protein ID used for preparation.
ligand1_id: First ligand ID (ABFE or RBFE).
ligand2_id: Second ligand ID (RBFE only); None for ABFE.
Expand All @@ -37,6 +38,7 @@ class PreparedSystem:
binding_xml_path: str
solvation_xml_path: str
system_pdb_path: str
solute_pdb_path: Optional[str] = None
id: Optional[str] = None
protein_id: Optional[str] = None
ligand1_id: Optional[str] = None
Expand All @@ -56,28 +58,46 @@ def __repr__(self) -> str:
return "PreparedSystem(" + ", ".join(parts) + ")"

@beartype
def show(self) -> Any:
def show(self, *, solute: bool = False) -> Any:
"""Visualize the prepared system structure in a Jupyter notebook using MolStar.

Downloads the system PDB from the platform and renders it with the same
protein-only viewer as :meth:`Protein.show` when called without optional
pocket or ligand arguments.
By default, downloads the full system PDB from the platform. Pass
``solute=True`` to visualize the solute-only structure instead (requires
:attr:`solute_pdb_path` to be set).

Renders with the same protein-only viewer as :meth:`Protein.show` when
called without optional pocket or ligand arguments.

Args:
solute: If true, use :attr:`solute_pdb_path`; otherwise
:attr:`system_pdb_path`.

Returns:
Output from :func:`deeporigin.utils.notebook.render_html` (Jupyter
``display`` or marimo HTML wrapper, depending on environment).

Raises:
DeepOriginException: If ``system_pdb_path`` is missing or empty.
DeepOriginException: If the chosen PDB path is missing or empty, or
``solute=True`` but :attr:`solute_pdb_path` is not available.
"""
if not self.system_pdb_path:
raise DeepOriginException(
"Cannot show PreparedSystem: system_pdb_path is empty.",
) from None
if solute:
if not self.solute_pdb_path:
raise DeepOriginException(
"Cannot show PreparedSystem: solute_pdb_path is not set or empty "
"(use solute=False for the full system, or re-fetch results that "
"include solute_pdb_file_path).",
) from None
remote = self.solute_pdb_path
else:
if not self.system_pdb_path:
raise DeepOriginException(
"Cannot show PreparedSystem: system_pdb_path is empty.",
) from None
remote = self.system_pdb_path

client = DeepOriginClient()
local_pdb = client.files.download(
remote_path=self.system_pdb_path,
remote_path=remote,
lazy=True,
)

Expand Down Expand Up @@ -110,6 +130,7 @@ def _from_record(cls, record: dict) -> Self:
binding = data.get("binding_xml_file_path")
solvation = data.get("solvation_xml_ligand_file_path")
system_pdb = data.get("system_pdb_file_path")
solute_pdb = data.get("solute_pdb_file_path")
if not (binding and solvation and system_pdb):
raise ValueError(
"Record missing required paths (binding_xml_file_path, "
Expand All @@ -120,6 +141,7 @@ def _from_record(cls, record: dict) -> Self:
binding_xml_path=binding,
solvation_xml_path=solvation,
system_pdb_path=system_pdb,
solute_pdb_path=solute_pdb,
protein_id=data.get("protein_id"),
ligand1_id=data.get("ligand1_id"),
ligand2_id=data.get("ligand2_id"),
Expand All @@ -137,6 +159,7 @@ def from_result(
protein_id: str | None = None,
ligand1_id: str | None = None,
ligand2_id: str | None = None,
compute_job_id: str | None = None,
padding: int | float | None = None,
add_H_atoms: bool | None = None, # NOSONAR
retain_waters: bool | None = None,
Expand All @@ -152,6 +175,7 @@ def from_result(
protein_id: Optional protein ID to filter by.
ligand1_id: Optional first ligand ID to filter by.
ligand2_id: Optional second ligand ID to filter by (RBFE).
compute_job_id: Optional compute job ID to filter by.
padding: Optional padding value to filter by.
add_H_atoms: Optional add_H_atoms flag to filter by.
retain_waters: Optional retain_waters flag to filter by.
Expand All @@ -176,6 +200,7 @@ def from_result(
protein_id=protein_id,
ligand1_id=ligand1_id,
ligand2_id=ligand2_id,
compute_job_id=compute_job_id,
padding=padding_int,
add_H_atoms=add_H_atoms,
retain_waters=retain_waters,
Expand Down
3 changes: 3 additions & 0 deletions src/drug_discovery/system_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,12 @@ def run(self) -> PreparedSystem:
binding_xml_path: str | None = None
solvation_xml_path: str | None = None
system_pdb_path: str | None = None
solute_pdb_path: str | None = None
if isinstance(system, dict):
binding_xml_path = system.get("binding_xml_file_path")
solvation_xml_path = system.get("solvation_xml_ligand_file_path")
system_pdb_path = system.get("system_pdb_file_path")
solute_pdb_path = system.get("solute_pdb_file_path")

if not (binding_xml_path and solvation_xml_path and system_pdb_path):
raise ValueError(SYSPREP_NO_OUTPUT_PATHS_MSG)
Expand All @@ -306,6 +308,7 @@ def run(self) -> PreparedSystem:
binding_xml_path=binding_xml_path,
solvation_xml_path=solvation_xml_path,
system_pdb_path=system_pdb_path,
solute_pdb_path=solute_pdb_path,
protein_id=self.protein.id,
ligand1_id=self._ligand_ids()[0],
ligand2_id=self._ligand_ids()[1],
Expand Down
11 changes: 10 additions & 1 deletion src/platform/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This module provides a minimal synchronous HTTP client for interacting with the
DeepOrigin Platform API. The client includes built-in authentication, singleton
caching for connection reuse, and convenient access to platform resources like
tools, functions, clusters, files, and executions.
tools, functions, clusters, files, executions, and user logs.

Construct a client using the no-arg constructor or one of three factory methods:

Expand Down Expand Up @@ -62,6 +62,7 @@
from deeporigin.platform.projects import Projects
from deeporigin.platform.results import Results
from deeporigin.platform.tools import Tools
from deeporigin.platform.user_logs import UserLogs

# Cache for local token to ensure consistency across calls
_LOCAL_TOKEN_CACHE: str | None = None
Expand Down Expand Up @@ -315,6 +316,7 @@ class DeepOriginClient(metaclass=_DeepOriginMeta):
datasets: Datasets | None
files: Files # client always has files
executions: Executions | None
user_logs: UserLogs | None
organizations: Organizations | None
billing: Billing | None
entities: Entities | None
Expand Down Expand Up @@ -470,6 +472,13 @@ def __init__(
except ImportError:
self.executions = None

try:
from deeporigin.platform.user_logs import UserLogs

self.user_logs = UserLogs(_client)
except ImportError:
self.user_logs = None

try:
from deeporigin.platform.organizations import Organizations

Expand Down
4 changes: 2 additions & 2 deletions src/platform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
TOOL_KEYS_AND_VERSIONS: dict[str, dict[str, str]] = {
"docking": {
"tool_key": "deeporigin.docking",
"tool_version": "3.0.0-30",
"tool_version": "3.0.0-32",
"function_key": "deeporigin.docking",
"function_version": "2.0.1",
},
Expand All @@ -79,6 +79,6 @@
},
"sysprep": {
"function_key": "deeporigin.system-prep",
"function_version": "0.9.0",
"function_version": "0.10.0",
},
}
72 changes: 72 additions & 0 deletions src/platform/user_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""user_logs entity API wrapper for DeepOriginClient (data-platform user_logs table)."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from deeporigin.platform.client import DeepOriginClient


class UserLogs:
"""Data-platform ``user_logs`` entity (search by compute job, etc.).

Hits ``POST /data-platform/{orgKey}/user_logs/search`` with the standard
data-platform filter grammar (``filter.props`` with ``eq`` / ...).
"""

def __init__(self, client: DeepOriginClient) -> None:
"""Initialize UserLogs wrapper.

Args:
client: The DeepOriginClient instance to use for API calls.
"""
self._c = client

def search(
self,
compute_job_id: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
select: list[str] | None = None,
with_total_count: bool = False,
) -> dict:
"""Search user log rows, optionally scoped to a compute job.

Calls ``POST /data-platform/{orgKey}/user_logs/search`` with
an optional ``compute_job_id`` ``eq`` filter (same pattern as
:meth:`deeporigin.platform.executions.Executions.search` for
executions) when provided.

Args:
compute_job_id: If set, restrict results to this compute job.
limit: Max rows to return.
offset: Skip offset.
select: Columns to select; all columns by default.
with_total_count: When True, the server may return a total
count alongside the page (may be slower).

Returns:
The raw response dict, typically ``{"data": [...], "meta": {...}}``
(exact keys depend on the service).
"""
props: list[dict[str, Any]] = []
if compute_job_id is not None:
props.append(
{"column": "compute_job_id", "op": "eq", "value": compute_job_id}
)
body: dict[str, Any] = {"filter": {"props": props}}
if limit is not None:
body["limit"] = limit
if offset is not None:
body["offset"] = offset
if select is not None:
body["select"] = select
if with_total_count:
body["with_total_count"] = True

return self._c.post_json(
f"/data-platform/{self._c.org_key}/user_logs/search",
body=body,
)
6 changes: 5 additions & 1 deletion src/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ def create(
if client is None:
client = DeepOriginClient()

existing = client.projects.search(name=name, limit=1)
# Use exact name match, not icontains (``name=`` in search is substring match).
existing = client.projects.search(
filter_dict={"name": {"eq": name}},
limit=100,
)
rows = [r for r in existing.get("data") or [] if r.get("name") == name]
if rows:
row = rows[0]
Expand Down
10 changes: 10 additions & 0 deletions tests/mock_server/routers/data_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,20 @@ def create_data_platform_router(

_datasets: dict[str, dict[str, Any]] = datasets if datasets is not None else {}

# Pre-seed one user_logs row for SDK tests (compute_job_id filter).
_user_logs_store: dict[str, dict[str, Any]] = {
"ul-mock-1": {
"id": "ul-mock-1",
"compute_job_id": "MOCK-USER-LOGS-CJ-ID",
"line": "mock user log line",
}
}

_entity_stores: dict[str, dict[str, dict[str, Any]]] = {
"ligands": ligands,
"proteins": proteins,
"projects": projects,
"user_logs": _user_logs_store,
}

# Reverse index: (canonical_smiles, variant_name_tag) → ligand_id.
Expand Down
Loading
Loading