Skip to content

Commit 1ec35e7

Browse files
add API key + endpoint validation
1 parent f968cdb commit 1ec35e7

4 files changed

Lines changed: 75 additions & 20 deletions

File tree

mp_api/client/core/settings.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from multiprocessing import cpu_count
33
from typing import List
44

5-
from pydantic import Field
5+
from pydantic import Field, field_validator
66
from pydantic_settings import BaseSettings, SettingsConfigDict
77
from pymatgen.core import _load_pmg_settings
88

@@ -14,6 +14,7 @@
1414
_MUTE_PROGRESS_BAR = PMG_SETTINGS.get("MPRESTER_MUTE_PROGRESS_BARS", False)
1515
_MAX_HTTP_URL_LENGTH = PMG_SETTINGS.get("MPRESTER_MAX_HTTP_URL_LENGTH", 2000)
1616
_MAX_LIST_LENGTH = min(PMG_SETTINGS.get("MPRESTER_MAX_LIST_LENGTH", 10000), 10000)
17+
_DEFAULT_ENDPOINT = "https://api.materialsproject.org/"
1718

1819
try:
1920
CPU_COUNT = cpu_count()
@@ -80,11 +81,21 @@ class MAPIClientSettings(BaseSettings):
8081
)
8182

8283
MIN_EMMET_VERSION: str = Field(
83-
"0.54.0", description="Minimum compatible version of emmet-core for the client."
84+
"0.86.3rc0",
85+
description="Minimum compatible version of emmet-core for the client.",
8486
)
8587

8688
MAX_LIST_LENGTH: int = Field(
8789
_MAX_LIST_LENGTH, description="Maximum length of query parameter list"
8890
)
8991

92+
ENDPOINT: str = Field(
93+
_DEFAULT_ENDPOINT, description="The default API endpoint to use."
94+
)
95+
9096
model_config = SettingsConfigDict(env_prefix="MPRESTER_")
97+
98+
@field_validator("ENDPOINT", mode="before")
99+
def _get_endpoint_from_env(cls, v: str | None) -> str:
100+
"""Support setting endpoint via MP_API_ENDPOINT environment variable."""
101+
return v or os.environ.get("MP_API_ENDPOINT") or _DEFAULT_ENDPOINT

mp_api/client/core/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from typing import TYPE_CHECKING, Literal
45

56
import orjson
@@ -50,6 +51,25 @@ def load_json(
5051
return MontyDecoder().process_decoded(data) if deser else data
5152

5253

54+
def validate_api_key(api_key: str | None = None) -> str:
55+
"""Find and validate an API key."""
56+
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
57+
api_key = api_key or os.getenv("MP_API_KEY")
58+
if not api_key:
59+
from pymatgen.core import SETTINGS
60+
61+
api_key = SETTINGS.get("PMG_MAPI_KEY")
62+
63+
if not api_key or len(api_key) != 32:
64+
addendum = " Valid API keys are 32 characters." if api_key else ""
65+
raise ValueError(
66+
"Please obtain a valid API key from https://materialsproject.org/api "
67+
f"and export it as an environment variable `MP_API_KEY`.{addendum}"
68+
)
69+
70+
return api_key
71+
72+
5373
def validate_ids(id_list: list[str]) -> list[str]:
5474
"""Function to validate material and task IDs.
5575

mp_api/client/mprester.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import itertools
4-
import os
54
import warnings
65
from collections import defaultdict
76
from functools import cache, lru_cache
@@ -16,7 +15,7 @@
1615
from packaging import version
1716
from pymatgen.analysis.phase_diagram import PhaseDiagram
1817
from pymatgen.analysis.pourbaix_diagram import IonEntry
19-
from pymatgen.core import SETTINGS, Composition, Element, Structure
18+
from pymatgen.core import Composition, Element, Structure
2019
from pymatgen.core.ion import Ion
2120
from pymatgen.entries.computed_entries import ComputedStructureEntry
2221
from pymatgen.io.vasp import Chgcar
@@ -26,7 +25,7 @@
2625
from mp_api.client.core import BaseRester, MPRestError
2726
from mp_api.client.core._oxygen_evolution import OxygenEvolution
2827
from mp_api.client.core.settings import MAPIClientSettings
29-
from mp_api.client.core.utils import load_json, validate_ids
28+
from mp_api.client.core.utils import load_json, validate_api_key, validate_ids
3029
from mp_api.client.routes import GeneralStoreRester, MessagesRester, UserSettingsRester
3130
from mp_api.client.routes.materials import (
3231
AbsorptionRester,
@@ -165,20 +164,12 @@ def __init__(
165164
mute_progress_bars: Whether to mute progress bars.
166165
**kwargs: access to legacy kwargs that may be in the process of being deprecated
167166
"""
168-
# SETTINGS tries to read API key from ~/.config/.pmgrc.yaml
169-
api_key = api_key or os.getenv("MP_API_KEY") or SETTINGS.get("PMG_MAPI_KEY")
167+
self.api_key = validate_api_key(api_key)
170168

171-
if api_key and len(api_key) != 32:
172-
raise ValueError(
173-
"Please use a new API key from https://materialsproject.org/api "
174-
"Keys for the new API are 32 characters, whereas keys for the legacy "
175-
"API are 16 characters."
176-
)
169+
self.endpoint = endpoint or _MAPI_SETTINGS.ENDPOINT
170+
if not self.endpoint.endswith("/"):
171+
self.endpoint += "/"
177172

178-
self.api_key = api_key
179-
self.endpoint = endpoint or os.getenv(
180-
"MP_API_ENDPOINT", "https://api.materialsproject.org/"
181-
)
182173
self.headers = headers or {}
183174
self.session = session or BaseRester._create_session(
184175
api_key=self.api_key,
@@ -219,9 +210,6 @@ def __init__(
219210
"chemenv",
220211
]
221212

222-
if not self.endpoint.endswith("/"):
223-
self.endpoint += "/"
224-
225213
if "monty_decode" in kwargs:
226214
warnings.warn(
227215
"Ignoring `monty_decode`, as it is no longer a supported option in `mp_api`."

tests/core/test_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,39 @@ def test_id_validation():
4646
isinstance(x, str) and AlphaID(x).string == x
4747
for x in validate_ids([y + AlphaID._cut_point for y in range(max_num_idxs)])
4848
)
49+
50+
51+
def test_api_key_validation(monkeypatch: pytest.MonkeyPatch):
52+
from mp_api.client.core.utils import validate_api_key
53+
import pymatgen.core
54+
55+
# Ensure any user settings are ignored
56+
monkeypatch.setenv("MP_API_KEY", "")
57+
monkeypatch.setenv("PMG_MAPI_KEY", "")
58+
non_api_key_settings = {
59+
k: v for k, v in pymatgen.core.SETTINGS.items() if k != "PMG_MAPI_KEY"
60+
}
61+
monkeypatch.setattr(pymatgen.core, "SETTINGS", non_api_key_settings)
62+
63+
with pytest.raises(ValueError, match="32 characters"):
64+
validate_api_key("invalid_key")
65+
66+
with pytest.raises(ValueError, match="Please obtain a valid"):
67+
validate_api_key()
68+
69+
junk_api_key = "a" * 32
70+
monkeypatch.setenv("MP_API_KEY", junk_api_key)
71+
assert validate_api_key() == junk_api_key
72+
assert validate_api_key(junk_api_key) == junk_api_key
73+
74+
other_junk_api_key = "b" * 32
75+
monkeypatch.setattr(
76+
pymatgen.core,
77+
"SETTINGS",
78+
{**non_api_key_settings, "PMG_MAPI_KEY": other_junk_api_key},
79+
)
80+
# MP API environment variable takes precedence
81+
assert validate_api_key() == junk_api_key
82+
83+
monkeypatch.setenv("MP_API_KEY", "")
84+
assert validate_api_key() == other_junk_api_key

0 commit comments

Comments
 (0)