Skip to content
Open
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
52 changes: 52 additions & 0 deletions docs/platform/ref/data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Data Platform API.

The DeepOriginClient can be used to access the data platform API using:

```{.python notest}
from deeporigin.platform.client import DeepOriginClient

client = DeepOriginClient()
```

Then, the following methods can be used, for example:

```{.python notest}
# Check the health status of the data platform
health_status = client.data.health()

# Search ligands joined with tool results
results = client.data.search_ligands_with_results(
limit=10,
experiments=[{"toolId": "deeporigin.docking"}],
)

# Search an entity (e.g., ligands)
results = client.data.search("ligands")

# Search ligands using convenience method
results = client.data.search_ligands(limit=10)

# Search proteins using convenience method
results = client.data.search_proteins(limit=10)

# List public models
models = client.data.list_models()
```


::: src.platform.data.Data
options:
heading_level: 2
docstring_style: google
show_root_heading: true
show_category_heading: true
show_object_full_path: false
show_root_toc_entry: false
inherited_members: true
members_order: alphabetical
filters:
- "!^_" # Exclude private members (names starting with "_")
show_signature: true
show_signature_annotations: true
show_if_no_docstring: true
group_by_category: true
1 change: 1 addition & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ nav:
- functions: platform/ref/functions.md
- organizations: platform/ref/organizations.md
- billing: platform/ref/billing.md
- data: platform/ref/data.md
- Developing:
- Installing: dev/install.md
- Clients: dev/clients.md
Expand Down
2 changes: 2 additions & 0 deletions src/platform/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from deeporigin.exceptions import DeepOriginException
from deeporigin.platform.billing import Billing
from deeporigin.platform.clusters import Clusters
from deeporigin.platform.data import Data
from deeporigin.platform.executions import Executions
from deeporigin.platform.files import Files
from deeporigin.platform.functions import Functions
Expand Down Expand Up @@ -306,6 +307,7 @@ def __init__(
self.executions = Executions(self)
self.organizations = Organizations(self)
self.billing = Billing(self)
self.data = Data(self)

# Retry configuration
self.max_retries = max_retries
Expand Down
300 changes: 300 additions & 0 deletions src/platform/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
"""Data Platform API wrapper for DeepOriginClient."""

from __future__ import annotations

from functools import lru_cache
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from deeporigin.platform.client import DeepOriginClient


class Data:
"""Data Platform API wrapper.

Provides access to data platform-related endpoints through the DeepOriginClient.
"""

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

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

def health(self) -> dict:
"""Check the health status of the data platform.

Returns:
Dictionary containing the health status response.
"""
return self._c.get_json("/data-platform/health")

@lru_cache(maxsize=1) # noqa: B019
def list_models(self) -> dict:
"""List public models.

Returns:
Dictionary containing the list of models.
"""
return self._c.get_json(f"/data-platform/{self._c.org_key}/meta/models")

def search_ligands_with_results(
self,
*,
cursor: str | None = None,
experiments: list[dict[str, str]] | None = None,
filter: dict[str, Any] | None = None,
limit: int | None = None,
offset: int | None = None,
select: list[str] | None = None,
sort: dict[str, str] | None = None,
) -> dict:
"""Search ligands joined with tool results (wide pivot view).

Args:
cursor: Cursor for pagination.
experiments: List of experiment filters, each containing toolId and
optionally toolVersion.
filter: Additional filter criteria as a dictionary.
limit: Maximum number of results to return. Defaults to 100.
offset: Number of results to skip.
select: List of fields to select in the response.
sort: Dictionary mapping field names to sort order ("asc" or "desc").

Returns:
Dictionary containing the search results.
"""
body: dict[str, Any] = {}
if cursor is not None:
body["cursor"] = cursor
if experiments is not None:
body["experiments"] = experiments
if filter is not None:
body["filter"] = filter
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 sort is not None:
body["sort"] = sort

return self._c.post_json(
f"/data-platform/{self._c.org_key}/ligands_with_results/search",
body=body,
)

def search(
self,
entity: str,
*,
cursor: str | None = None,
filter_dict: dict[str, Any] | None = None,
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name is inconsistent between search_ligands_with_results which uses 'filter' (line 48, 157) and the internal search method which uses 'filter_dict' (line 95). This inconsistency makes the API confusing. Consider renaming 'filter_dict' to 'filter' in the search method signature to maintain consistency across the API, or rename 'filter' to 'filter_dict' in search_ligands_with_results and search_ligands for consistency.

Copilot uses AI. Check for mistakes.
limit: int | None = None,
offset: int | None = None,
select: list[str] | None = None,
sort: dict[str, str] | None = None,
) -> dict:
"""Search an entity (table).

Args:
entity: Entity (table) name to search (e.g., "ligands").
cursor: Cursor for pagination.
filter_dict: Additional filter criteria as a dictionary.
limit: Maximum number of results to return. Defaults to 100.
offset: Number of results to skip.
select: List of fields to select in the response.
sort: Dictionary mapping field names to sort order ("asc" or "desc").

Returns:
Dictionary containing the search results.

Raises:
ValueError: If the entity is not a valid table name.
"""
# Validate entity against list of available models
models_response = self.list_models()
valid_table_names = {
model["tableName"] for model in models_response.get("models", [])
}
if entity not in valid_table_names:
raise ValueError(
f"Invalid entity '{entity}'. Valid entities are: {', '.join(sorted(valid_table_names))}"
)

if filter_dict is None:
filter_dict = {"deleted": False}
else:
filter_dict = filter_dict.copy()
filter_dict["deleted"] = False

body: dict[str, Any] = {}
if cursor is not None:
body["cursor"] = cursor

body["filter"] = filter_dict
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 sort is not None:
body["sort"] = sort

return self._c.post_json(
f"/data-platform/{self._c.org_key}/{entity}/search",
body=body,
)

def search_ligands(
self,
*,
cursor: str | None = None,
filter: dict[str, Any] | None = None,
min_molecular_weight: float | int | None = None,
max_molecular_weight: float | int | None = None,
limit: int | None = None,
offset: int | None = None,
select: list[str] | None = None,
sort: dict[str, str] | None = None,
) -> dict:
"""Search ligands entity.

Convenience method that calls search(entity="ligands").

Args:
cursor: Cursor for pagination.
filter: Additional filter criteria as a dictionary.
min_molecular_weight: Minimum molecular weight filter (inclusive).
max_molecular_weight: Maximum molecular weight filter (inclusive).
limit: Maximum number of results to return. Defaults to 100.
offset: Number of results to skip.
select: List of fields to select in the response.
sort: Dictionary mapping field names to sort order ("asc" or "desc").

Returns:
Dictionary containing the search results.

Raises:
ValueError: If ligands is not a valid table name (should not happen).
"""
# Build filter dict, starting with provided filter or empty dict
filter_dict = filter.copy() if filter is not None else {}
filter_dict.setdefault("deleted", False)
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search_ligands method uses filter_dict.setdefault("deleted", False) which will not override an existing "deleted" key if the user passes one in the filter. In contrast, search_proteins and the search method unconditionally set filter_dict["deleted"] = False, which overrides any user-provided value. This inconsistency in handling the "deleted" field could lead to unexpected behavior. Consider using a consistent approach across all methods - either always override or always preserve user values.

Suggested change
filter_dict.setdefault("deleted", False)
# Ensure deleted records are excluded consistently with other search methods
filter_dict["deleted"] = False

Copilot uses AI. Check for mistakes.

# Build molecular weight filters
props = []
if min_molecular_weight is not None:
props.append(
{
"column": "molecular_weight",
"op": "gte",
"value": min_molecular_weight,
}
)
if max_molecular_weight is not None:
props.append(
{
"column": "molecular_weight",
"op": "lte",
"value": max_molecular_weight,
}
)

if props:
# Merge with existing props if any
existing_props = filter_dict.get("props", [])
filter_dict["props"] = existing_props + props

return self.search(
"ligands",
cursor=cursor,
filter_dict=filter_dict,
limit=limit,
offset=offset,
select=select,
sort=sort,
)

def search_proteins(
self,
*,
cursor: str | None = None,
pdb_id: str | None = None,
min_molecular_weight: float | int | None = None,
max_molecular_weight: float | int | None = None,
sequence: str | None = None,
limit: int | None = None,
offset: int | None = None,
select: list[str] | None = None,
sort: dict[str, str] | None = None,
) -> dict:
Comment on lines +223 to +235
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search_proteins method does not accept a filter parameter like search_ligands does (line 157). Users cannot pass custom filters to search_proteins, which limits flexibility. Consider adding a filter parameter to maintain consistency with search_ligands and allow users to specify custom filter criteria beyond the convenience parameters (pdb_id, molecular_weight, sequence).

Copilot uses AI. Check for mistakes.
"""Search proteins entity.

Convenience method that calls search(entity="proteins").

Args:
cursor: Cursor for pagination.
pdb_id: Filter by PDB ID.
min_molecular_weight: Minimum molecular weight filter (inclusive).
max_molecular_weight: Maximum molecular weight filter (inclusive).
sequence: Filter by FASTA sequence (exact match).
limit: Maximum number of results to return. Defaults to 100.
offset: Number of results to skip.
select: List of fields to select in the response.
sort: Dictionary mapping field names to sort order ("asc" or "desc").

Returns:
Dictionary containing the search results.

Raises:
ValueError: If proteins is not a valid table name (should not happen).
"""

filter_dict = {"deleted": False}
if pdb_id is not None:
filter_dict["pdb_id"] = pdb_id

# Build molecular weight filters
props = []
if min_molecular_weight is not None:
props.append(
{
"column": "molecular_weight",
"op": "gte",
"value": min_molecular_weight,
}
)
if max_molecular_weight is not None:
props.append(
{
"column": "molecular_weight",
"op": "lte",
"value": max_molecular_weight,
}
)
if sequence is not None:
props.append(
{
"column": "fasta_sequence",
"op": "eq",
"value": sequence,
}
)

if props:
filter_dict["props"] = props

return self.search(
"proteins",
cursor=cursor,
filter_dict=filter_dict,
limit=limit,
offset=offset,
select=select,
sort=sort,
)
Loading
Loading