Skip to content

Commit 51e749f

Browse files
major refactor + sort + pagination + tests to other explorers
1 parent 3879c32 commit 51e749f

41 files changed

Lines changed: 323 additions & 406 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
MP_API_KEY: ${{ secrets[env.API_KEY_NAME] }}
6363
# MP_API_ENDPOINT: https://api-preview.materialsproject.org/
6464
run: |
65-
pytest -n auto -x --cov=mp_api --cov-report=xml
65+
pytest -n auto -x --cov=mp_api --omit=mp_api/_test_utils.py --cov-report=xml
6666
- uses: codecov/codecov-action@v1
6767
with:
6868
token: ${{ secrets.CODECOV_TOKEN }}

mp_api/_test_utils.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Define testing utils that need to imported."""
2+
3+
from __future__ import annotations
4+
5+
try:
6+
import pytest
7+
except ImportError as exc:
8+
raise ImportError(
9+
"You must `pip install 'mp-api[test]' to use these testing utilities."
10+
) from exc
11+
12+
import os
13+
from typing import TYPE_CHECKING
14+
15+
if TYPE_CHECKING:
16+
from collections.abc import Callable, Sequence
17+
from typing import Any
18+
19+
requires_api_key = pytest.mark.skipif(
20+
os.getenv("MP_API_KEY") is None,
21+
reason="No API key found.",
22+
)
23+
24+
NUM_DOCS = 5
25+
26+
def client_search_testing(
27+
search_method: Callable,
28+
excluded_params: list[str],
29+
alt_name_dict: dict[str, str],
30+
custom_field_tests: dict[str, Any],
31+
sub_doc_fields: list[str],
32+
int_bounds : tuple[int,int] = (-100, 100),
33+
float_bounds : tuple[float,float] = (-100.12, 100.12),
34+
):
35+
"""
36+
Function to test a client using its search method.
37+
Each parameter is used to query for data, which is then checked.
38+
39+
Args:
40+
search_method (Callable): Client search method
41+
excluded_params (list[str]): List of parameters to exclude from testing
42+
alt_name_dict (dict[str, str]): Alternative names for parameters used in the projection and subsequent data checking
43+
custom_field_tests (dict[str, Any]): Custom queries for specific fields.
44+
sub_doc_fields (list[str]): Prefixes for fields to check in resulting data. Useful when data to be tested is nested.
45+
int_bounds (tuple[int,int]) : integer bounds to use in testing int-type query arguments
46+
float_boudns (tuple[float,float]) : float bounds to use in testing float-type query arguments
47+
"""
48+
if search_method is None:
49+
return
50+
# Get list of parameters
51+
param_tuples = list(search_method.__annotations__.items())
52+
53+
# Query API for each numeric and boolean parameter and check if returned
54+
for entry in param_tuples:
55+
param = entry[0]
56+
57+
if param not in excluded_params + ["return"]:
58+
param_type = entry[1]
59+
q = {"chunk_size": 1, "num_chunks": 1}
60+
61+
if "tuple[int, int]" in param_type:
62+
q[param] = int_bounds
63+
elif "tuple[float, float]" in param_type:
64+
q[param] = float_bounds
65+
elif "bool" in param_type:
66+
q[param] = False
67+
elif param in custom_field_tests:
68+
q[param] = custom_field_tests[param]
69+
else:
70+
raise ValueError(
71+
f"Parameter '{param}' with type '{param_type}' was not "
72+
"properly identified in the generic search method test."
73+
)
74+
75+
if len(docs := search_method(**q)) > 0:
76+
doc = docs[0].model_dump()
77+
else:
78+
raise ValueError("No documents returned")
79+
80+
for sub_field in sub_doc_fields:
81+
if sub_field in doc:
82+
doc = doc[sub_field]
83+
84+
assert doc[alt_name_dict.get(param, param)] is not None
85+
86+
87+
def client_pagination(search_method : Callable, id_name : str):
88+
89+
page_1 = search_method(_page=1, chunk_size=NUM_DOCS, fields = [id_name])
90+
page_2 = search_method(_page=2, chunk_size=NUM_DOCS, fields = [id_name])
91+
assert all(
92+
len(results) == NUM_DOCS for results in (page_1, page_2)
93+
)
94+
assert {str(getattr(doc, id_name)) for doc in page_1}.intersection(
95+
{str(getattr(doc,id_name)) for doc in page_2}
96+
) == set()
97+
98+
99+
def client_sort(search_method : Callable, sort_fields : str | Sequence[str]):
100+
101+
for sort_field in ([sort_fields] if isinstance(sort_fields,str) else sort_fields):
102+
asc = search_method(_page=1, _sort_fields=sort_field, chunk_size=NUM_DOCS, fields = [sort_field])
103+
desc = search_method(_page=1, _sort_fields=f"-{sort_field}", chunk_size=NUM_DOCS, fields = [sort_field])
104+
105+
idxs = list(range(NUM_DOCS))
106+
assert sorted(
107+
idxs, key=lambda idx: getattr(asc[idx],sort_field)
108+
) == idxs
109+
110+
assert sorted(
111+
idxs,
112+
key=lambda idx: getattr(desc[idx],sort_field),
113+
reverse=True,
114+
) == idxs

mp_api/client/routes/materials/electrodes.py

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -87,57 +87,33 @@ def search(
8787
"""
8888
query_params: dict = defaultdict(dict)
8989

90-
if battery_ids:
91-
if isinstance(battery_ids, str):
92-
battery_ids = [battery_ids]
93-
94-
query_params.update({"battery_ids": ",".join(validate_ids(battery_ids))})
95-
96-
if working_ion:
97-
if isinstance(working_ion, (str, Element)):
98-
working_ion = [working_ion] # type: ignore
99-
100-
query_params.update(
101-
{"working_ion": ",".join([str(ele) for ele in working_ion])} # type: ignore
102-
)
103-
104-
if formula:
105-
if isinstance(formula, str):
106-
formula = [formula]
107-
108-
query_params.update({"formula": ",".join(formula)})
109-
110-
if elements:
111-
query_params.update({"elements": ",".join(elements)})
112-
113-
if num_elements:
114-
if isinstance(num_elements, int):
115-
num_elements = (num_elements, num_elements)
116-
query_params.update(
117-
{"nelements_min": num_elements[0], "nelements_max": num_elements[1]}
118-
)
119-
120-
if exclude_elements:
121-
query_params.update({"exclude_elements": ",".join(exclude_elements)})
122-
12390
for param, value in locals().items():
12491
if (
125-
param
126-
not in [
127-
"__class__",
128-
"self",
129-
"working_ion",
130-
"query_params",
131-
"num_elements",
132-
]
133-
and value
134-
):
135-
if isinstance(value, tuple):
92+
param not in {"__class__", "self", "query_params"}
93+
and value is not None
94+
):
95+
96+
if param == "num_elements": # this must come first
97+
if isinstance(num_elements, int):
98+
num_elements = (num_elements, num_elements)
99+
query_params.update(
100+
{"nelements_min": num_elements[0], "nelements_max": num_elements[1]}
101+
)
102+
103+
elif isinstance(value, tuple):
136104
query_params.update(
137105
{f"{param}_min": value[0], f"{param}_max": value[1]}
138106
)
107+
elif param == "battery_ids":
108+
query_params[param] = ",".join(validate_ids(value))
109+
elif param == "working_ion":
110+
query_params["working_ion"] = ",".join(
111+
str(ele) for ele in ([value] if isinstance(value, str | Element) else value)
112+
)
113+
elif param in ("formula","elements","exclude_elements"):
114+
query_params[param] = ",".join([value] if isinstance(value,str) else value)
139115
else:
140-
query_params.update({param: value})
116+
query_params[param] = value
141117

142118
excluded_fields = self._exclude_search_fields or []
143119
ignored_fields = {
@@ -181,4 +157,6 @@ class ConversionElectrodeRester(BaseElectrodeRester):
181157
"stability_charge",
182158
"stability_discharge",
183159
"exclude_elements",
160+
"_page",
161+
"_sort_fields",
184162
]

mp_api/client/routes/materials/summary.py

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ def search( # noqa: D417
206206
mmnd_inv = {v: k for k, v in min_max_name_dict.items() if k != v}
207207

208208
# Set user query params from `locals`
209+
_locals = locals()
209210
user_settings = {
210-
k: v for k, v in locals().items() if k in min_max_name_dict and v
211+
k: v for k, v in _locals.items() if k in min_max_name_dict and v is not None
211212
}
212213

213214
# Check to see if user specified _search fields using **kwargs,
@@ -290,7 +291,7 @@ def _csrc(x):
290291
)
291292

292293
for param, value in user_settings.items():
293-
if param.startswith("_"):
294+
if param in {"_page", "_sort_fields"}:
294295
query_params[param] = value
295296
else:
296297
if isinstance(value, (int, float)):
@@ -308,30 +309,10 @@ def _csrc(x):
308309

309310
query_params.update({"material_ids": ",".join(validate_ids(material_ids))})
310311

311-
if deprecated is not None:
312-
query_params.update({"deprecated": deprecated})
313-
314-
if formula:
315-
if isinstance(formula, str):
316-
formula = [formula]
317-
318-
query_params.update({"formula": ",".join(formula)})
319-
320-
if chemsys:
321-
if isinstance(chemsys, str):
322-
chemsys = [chemsys]
323-
324-
query_params.update({"chemsys": ",".join(chemsys)})
325-
326-
if elements:
327-
query_params.update({"elements": ",".join(elements)})
328-
329-
if exclude_elements is not None:
330-
query_params.update({"exclude_elements": ",".join(exclude_elements)})
331-
332-
if possible_species is not None:
333-
query_params.update({"possible_species": ",".join(possible_species)})
334-
312+
for k in ("formula", "chemsys", "elements", "exclude_elements", "possible_species"):
313+
if (v := _locals.get(k)) is not None:
314+
query_params[k] = ",".join([v] if isinstance(v,str) else v)
315+
335316
symm_cardinality = {
336317
"crystal_system": 7,
337318
"spacegroup_number": 230,
@@ -350,21 +331,13 @@ def _csrc(x):
350331
else:
351332
query_params.update({k: symm_vals})
352333

353-
if is_stable is not None:
354-
query_params.update({"is_stable": is_stable})
355-
356-
if is_gap_direct is not None:
357-
query_params.update({"is_gap_direct": is_gap_direct})
358-
359-
if is_metal is not None:
360-
query_params.update({"is_metal": is_metal})
334+
for k in ("deprecated", "is_stable", "is_gap_direct", "is_metal", "has_reconstructed", "theoretical"):
335+
if (v := _locals.get(k)) is not None:
336+
query_params[k] = v
361337

362338
if magnetic_ordering:
363339
query_params.update({"ordering": magnetic_ordering.value})
364340

365-
if has_reconstructed is not None:
366-
query_params.update({"has_reconstructed": has_reconstructed})
367-
368341
if has_props:
369342
has_props_clean = []
370343
for prop in has_props:
@@ -375,9 +348,6 @@ def _csrc(x):
375348

376349
query_params.update({"has_props": ",".join(has_props_clean)})
377350

378-
if theoretical is not None:
379-
query_params.update({"theoretical": theoretical})
380-
381351
if not include_gnome:
382352
query_params.update({"batch_id_not_eq": "gnome_r2scan_statics"})
383353

mp_api/client/routes/materials/xas.py

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def search(
3333
chunk_size: int = 1000,
3434
all_fields: bool = True,
3535
fields: list[str] | None = None,
36+
_page: int | None = None,
37+
_sort_fields: str | None = None,
3638
):
3739
"""Query core XAS docs using a variety of search criteria.
3840
@@ -54,14 +56,19 @@ def search(
5456
all_fields (bool): Whether to return all fields in the document. Defaults to True.
5557
fields (List[str]): List of fields in MaterialsCoreDoc to return data for.
5658
Default is material_id, last_updated, and formula_pretty if all_fields is False.
59+
_page (int or None) : Page of the results to skip to.
60+
_sort_fields (str or None) : Field to sort on. Including a leading "-" sign will reverse sort order.
5761
5862
Returns:
5963
([MaterialsDoc]) List of material documents
6064
"""
61-
query_params: dict[str, Any] = {}
6265

63-
if edge:
64-
query_params.update({"edge": edge})
66+
_locals = locals()
67+
query_params: dict[str, Any] = {
68+
k : _locals[k]
69+
for k in ("edge", "spectrum_type", "formula", "_page", "_sort_fields")
70+
if _locals.get(k) is not None
71+
}
6572

6673
if absorbing_element:
6774
query_params.update(
@@ -71,33 +78,10 @@ def search(
7178
else absorbing_element
7279
}
7380
)
74-
75-
if spectrum_type:
76-
query_params.update({"spectrum_type": spectrum_type})
77-
78-
if formula:
79-
query_params.update({"formula": formula})
80-
81-
if chemsys:
82-
if isinstance(chemsys, str):
83-
chemsys = [chemsys]
84-
85-
query_params.update({"chemsys": ",".join(chemsys)})
86-
87-
if elements:
88-
query_params.update({"elements": ",".join(elements)})
89-
90-
if material_ids:
91-
if isinstance(material_ids, str):
92-
material_ids = [material_ids]
93-
94-
query_params.update({"material_ids": ",".join(validate_ids(material_ids))})
95-
96-
if spectrum_ids:
97-
if isinstance(spectrum_ids, str):
98-
spectrum_ids = [spectrum_ids]
99-
100-
query_params.update({"spectrum_ids": ",".join(spectrum_ids)})
81+
for k in ("chemsys", "elements", "material_ids", "spectrum_ids"):
82+
if (v := _locals.get(k) ) is not None:
83+
query_params[k] = ",".join([v] if isinstance(v,str) else v)
84+
10185

10286
query_params = {
10387
entry: query_params[entry]

0 commit comments

Comments
 (0)