Source code for tooluniverse.cod_tool
"""
COD Tool - Crystallography Open Database
Extends BaseRESTTool with parameter aliases and client-side result limiting
for the COD REST API. The COD JSON API does not support server-side result
limiting, so truncation is applied after fetching.
API: https://www.crystallography.net/cod/result?format=json
No authentication required. CC0 license.
"""
from typing import Dict, Any
from .base_rest_tool import BaseRESTTool
from .tool_registry import register_tool
[docs]
@register_tool("CODTool")
class CODTool(BaseRESTTool):
"""
Tool for querying the Crystallography Open Database (COD).
Adds user-friendly aliases and client-side result limiting:
- query -> text
- spacegroup -> sg
- cod_id -> id
- max_results / results -> client-side truncation
- mineral / commonname -> free-text search, filtered client-side on the
record's own mineral / commonname field (COD's server ignores both
parameters: alone they match nothing, combined with an element filter
they are dropped and every structure comes back)
"""
NAME_FILTERS = ("mineral", "commonname")
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
args = dict(arguments)
# Resolve aliases before passing to BaseRESTTool
alias_map = {
"query": "text",
"spacegroup": "sg",
"cod_id": "id",
"max_results": "results",
}
for alias, canonical in alias_map.items():
if alias in args and canonical not in args:
args[canonical] = args.pop(alias)
elif alias in args:
args.pop(alias)
# Extract limit before calling parent (COD JSON API ignores it)
limit = args.pop("results", None)
name_filters = {}
for field in self.NAME_FILTERS:
value = args.pop(field, None)
if isinstance(value, str) and value.strip():
name_filters[field] = value.strip().lower()
if name_filters and not args.get("text"):
args["text"] = next(iter(name_filters.values()))
result = super().run(args)
if name_filters and result.get("status") == "success":
data = result.get("data")
if isinstance(data, list):
result["data"] = [
row
for row in data
if isinstance(row, dict)
and all(
needle in str(row.get(field) or "").lower()
for field, needle in name_filters.items()
)
]
result["count"] = len(result["data"])
result["note"] = (
"COD cannot filter on "
+ " / ".join(name_filters)
+ " server-side; rows come from a free-text search and are "
"kept when the record's own field contains the value"
)
# Apply client-side result limiting
if limit is not None and result.get("status") == "success":
data = result.get("data")
if isinstance(data, list) and len(data) > int(limit):
result["total_before_limit"] = len(data)
result["data"] = data[: int(limit)]
result["count"] = int(limit)
return result