mp-api 0.47.0rc0__tar.gz → 0.47.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {mp_api-0.47.0rc0 → mp_api-0.47.1}/PKG-INFO +5 -5
  2. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/_logger.py +2 -3
  3. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/client.py +37 -21
  4. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/_display.py +182 -24
  5. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/_entries.py +19 -2
  6. mp_api-0.47.1/mp_api/client/core/_external.py +352 -0
  7. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/client.py +63 -31
  8. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/settings.py +9 -2
  9. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/utils.py +91 -26
  10. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/mprester.py +150 -46
  11. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/electronic_structure.py +4 -7
  12. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/eos.py +2 -3
  13. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/materials.py +1 -1
  14. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/thermo.py +48 -0
  15. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/xas.py +2 -3
  16. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/_schemas.py +1 -1
  17. {mp_api-0.47.0rc0 → mp_api-0.47.1}/pyproject.toml +4 -4
  18. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_delta.py +13 -2
  19. mp_api-0.47.1/tests/client/core/test_external.py +675 -0
  20. mp_api-0.47.1/tests/client/core/test_ids.py +192 -0
  21. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_utils.py +3 -1
  22. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_thermo.py +34 -0
  23. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/test_entry_progress.py +55 -4
  24. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/test_mprester.py +155 -80
  25. {mp_api-0.47.0rc0 → mp_api-0.47.1}/.gitignore +0 -0
  26. {mp_api-0.47.0rc0 → mp_api-0.47.1}/LICENSE +0 -0
  27. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/__init__.py +0 -0
  28. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/_test_utils.py +0 -0
  29. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/__init__.py +0 -0
  30. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/_server_utils.py +0 -0
  31. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/__init__.py +0 -0
  32. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/_types.py +0 -0
  33. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/_units.py +0 -0
  34. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/schemas.py +0 -0
  35. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/settings.py +0 -0
  36. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/contribs/utils.py +0 -0
  37. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/__init__.py +0 -0
  38. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/_oxygen_evolution.py +0 -0
  39. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/delta.py +0 -0
  40. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/exceptions.py +0 -0
  41. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/core/schemas.py +0 -0
  42. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/__init__.py +0 -0
  43. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/_server.py +0 -0
  44. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/__init__.py +0 -0
  45. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/absorption.py +0 -0
  46. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/alloys.py +0 -0
  47. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/bonds.py +0 -0
  48. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/chemenv.py +0 -0
  49. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/dielectric.py +0 -0
  50. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/doi.py +0 -0
  51. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/elasticity.py +0 -0
  52. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/electrodes.py +0 -0
  53. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/grain_boundaries.py +0 -0
  54. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/magnetism.py +0 -0
  55. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/oxidation_states.py +0 -0
  56. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/phonon.py +0 -0
  57. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/piezo.py +0 -0
  58. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/provenance.py +0 -0
  59. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/robocrys.py +0 -0
  60. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/similarity.py +0 -0
  61. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/substrates.py +0 -0
  62. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/summary.py +0 -0
  63. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/surface_properties.py +0 -0
  64. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/synthesis.py +0 -0
  65. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/materials/tasks.py +0 -0
  66. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/molecules/__init__.py +0 -0
  67. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/molecules/jcesr.py +0 -0
  68. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/molecules/molecules.py +0 -0
  69. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/client/routes/molecules/summary.py +0 -0
  70. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/__init__.py +0 -0
  71. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/mp_mcp.py +0 -0
  72. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/server.py +0 -0
  73. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/tools.py +0 -0
  74. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/mcp/utils.py +0 -0
  75. {mp_api-0.47.0rc0 → mp_api-0.47.1}/mp_api/py.typed +0 -0
  76. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/__init__.py +0 -0
  77. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/__init__.py +0 -0
  78. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/conftest.py +0 -0
  79. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/test_client.py +0 -0
  80. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/test_contribs_schemas.py +0 -0
  81. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/test_contribs_utils.py +0 -0
  82. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/test_side_effects.py +0 -0
  83. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/contribs/test_types.py +0 -0
  84. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_client_progress.py +0 -0
  85. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_delta_versions.py +0 -0
  86. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_display.py +0 -0
  87. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_oxygen_evolution.py +0 -0
  88. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_quiet.py +0 -0
  89. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_schemas.py +0 -0
  90. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/core/test_session_headers.py +0 -0
  91. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/__init__.py +0 -0
  92. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_absorption.py +0 -0
  93. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_alloys.py +0 -0
  94. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_bonds.py +0 -0
  95. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_chemenv.py +0 -0
  96. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_dielectric.py +0 -0
  97. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_doi.py +0 -0
  98. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_elasticity.py +0 -0
  99. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_electrodes.py +0 -0
  100. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_electronic_structure.py +0 -0
  101. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_eos.py +0 -0
  102. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_grain_boundary.py +0 -0
  103. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_magnetism.py +0 -0
  104. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_materials.py +0 -0
  105. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_oxidation_states.py +0 -0
  106. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_phonon.py +0 -0
  107. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_piezo.py +0 -0
  108. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_provenance.py +0 -0
  109. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_robocrys.py +0 -0
  110. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_similarity.py +0 -0
  111. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_substrates.py +0 -0
  112. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_summary.py +0 -0
  113. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_surface_properties.py +0 -0
  114. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_synthesis.py +0 -0
  115. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_tasks.py +0 -0
  116. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/materials/test_xas.py +0 -0
  117. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/molecules/__init__.py +0 -0
  118. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/molecules/test_jcesr.py +0 -0
  119. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/molecules/test_molecules.py +0 -0
  120. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/molecules/test_summary.py +0 -0
  121. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/test_client.py +0 -0
  122. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/test_core_client.py +0 -0
  123. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/client/test_heartbeat.py +0 -0
  124. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/conftest.py +0 -0
  125. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/mcp/__init__.py +0 -0
  126. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/mcp/test_server.py +0 -0
  127. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/mcp/test_tools.py +0 -0
  128. {mp_api-0.47.0rc0 → mp_api-0.47.1}/tests/mcp/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mp-api
3
- Version: 0.47.0rc0
3
+ Version: 0.47.1
4
4
  Summary: API Client for the Materials Project
5
5
  Author-email: The Materials Project <feedback@materialsproject.org>
6
6
  License-Expression: BSD-3-Clause-LBNL
@@ -16,18 +16,18 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Topic :: Scientific/Engineering
18
18
  Requires-Python: >=3.12
19
- Requires-Dist: boto3
19
+ Requires-Dist: boto3>=1.34
20
20
  Requires-Dist: deltalake<1.6.0,>=1.4.0
21
- Requires-Dist: emmet-core<0.88.1,>=0.88.0rc
21
+ Requires-Dist: emmet-core>=0.88.0
22
22
  Requires-Dist: monty>=2024.12.10
23
23
  Requires-Dist: orjson<4,>=3.10
24
24
  Requires-Dist: pyarrow>=20.0.0
25
- Requires-Dist: pymatgen>2024.2.20
25
+ Requires-Dist: pymatgen>=2026.10.24
26
26
  Requires-Dist: requests>=2.23.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Provides-Extra: all
29
29
  Requires-Dist: custodian; extra == 'all'
30
- Requires-Dist: emmet-core[all]<0.88.1,>=0.88.0rc; extra == 'all'
30
+ Requires-Dist: emmet-core[all]>=0.88.0; extra == 'all'
31
31
  Requires-Dist: fastmcp; extra == 'all'
32
32
  Requires-Dist: flask; extra == 'all'
33
33
  Requires-Dist: ipywidgets; extra == 'all'
@@ -9,9 +9,9 @@ from __future__ import annotations
9
9
 
10
10
  import logging
11
11
  import os
12
- import warnings
13
12
 
14
13
  from mp_api.client.contribs.settings import MPCC_SETTINGS
14
+ from mp_api.client.core._display import mp_deprecation
15
15
 
16
16
 
17
17
  class CustomLoggerAdapter(logging.LoggerAdapter):
@@ -39,10 +39,9 @@ def get_logger(name: str = "mp_api.client.contribs") -> CustomLoggerAdapter:
39
39
 
40
40
 
41
41
  if MPCC_SETTINGS.CLIENT_LOG_LEVEL is not None:
42
- warnings.warn(
42
+ mp_deprecation(
43
43
  "MPCONTRIBS_CLIENT_LOG_LEVEL is deprecated and ignored, use MPRESTER_LOG_LEVEL "
44
44
  "(which sets the level for the whole client) instead.",
45
- FutureWarning,
46
45
  stacklevel=2,
47
46
  )
48
47
 
@@ -8,7 +8,6 @@ import importlib.metadata
8
8
  import itertools
9
9
  import sys
10
10
  import time
11
- import warnings
12
11
  from base64 import urlsafe_b64encode
13
12
  from collections import defaultdict
14
13
  from concurrent.futures import as_completed
@@ -67,7 +66,8 @@ from mp_api.client.contribs.utils import (
67
66
  get_md5,
68
67
  unflatten_dict,
69
68
  )
70
- from mp_api.client.core._display import progress_bar
69
+ from mp_api.client.core._display import mp_deprecation, progress_bar
70
+ from mp_api.client.core._external import track_calls
71
71
  from mp_api.client.core.exceptions import MPContribsClientError
72
72
  from mp_api.client.core.schemas import _convert_to_model
73
73
  from mp_api.client.core.settings import MAPI_CLIENT_SETTINGS
@@ -444,6 +444,7 @@ def _version(url):
444
444
  time.sleep(30)
445
445
 
446
446
 
447
+ @track_calls
447
448
  class ContribsClient(SwaggerClient):
448
449
  """client to connect to MPContribs API.
449
450
 
@@ -492,7 +493,7 @@ class ContribsClient(SwaggerClient):
492
493
  )
493
494
  else:
494
495
  api_key = kwargs.pop("apikey")
495
- warnings.warn(api_key_warn, FutureWarning, stacklevel=2)
496
+ mp_deprecation(api_key_warn, stacklevel=2)
496
497
 
497
498
  if api_key and len(api_key) != 32:
498
499
  raise MPContribsClientError(f"Invalid API key: {api_key}")
@@ -542,10 +543,9 @@ class ContribsClient(SwaggerClient):
542
543
  @property
543
544
  def apikey(self) -> str | None:
544
545
  """Handle deprecated `apikey` attr."""
545
- warnings.warn(
546
+ mp_deprecation(
546
547
  "`apikey` has been deprecated in favor of `api_key` for "
547
548
  "consistency with the Materials Project API client.",
548
- FutureWarning,
549
549
  stacklevel=2,
550
550
  )
551
551
  return self.api_key
@@ -994,11 +994,14 @@ class ContribsClient(SwaggerClient):
994
994
  else MPCDict(contrib)
995
995
  )
996
996
 
997
- def get_table(self, tid_or_md5: str) -> Table:
997
+ def get_table(self, tid_or_md5: str, timeout: int = -1) -> Table:
998
998
  """Retrieve full Pandas DataFrame for a table.
999
999
 
1000
+ Data pages beyond the first are fetched concurrently.
1001
+
1000
1002
  Args:
1001
1003
  tid_or_md5 (str): ObjectId or MD5 hash digest for table
1004
+ timeout (int): cancel remaining requests if timeout exceeded (in seconds)
1002
1005
  """
1003
1006
  str_len = len(tid_or_md5)
1004
1007
  if str_len not in {24, 32}:
@@ -1016,23 +1019,36 @@ class ContribsClient(SwaggerClient):
1016
1019
 
1017
1020
  op = self.swagger_spec.resources["tables"].queryTables
1018
1021
  per_page = op.params["data_per_page"].param_spec["maximum"]
1019
- table: dict[str, list[str]] = {"data": []}
1020
- page, pages = 1, None
1021
1022
 
1022
- while pages is None or page <= pages:
1023
- resp = self.tables.getTableById(
1024
- pk=tid, _fields=["_all"], data_page=page, data_per_page=per_page
1025
- ).result()
1026
- table["data"].extend(resp["data"])
1027
- if pages is None:
1028
- pages = resp["total_data_pages"]
1029
- table["index"] = resp["index"]
1030
- table["columns"] = resp["columns"]
1031
- table_attrs: dict[str, str] = (resp.get("attrs") or {}) | {
1032
- field: resp.get(field) for field in {"id", "name", "md5"}
1033
- }
1023
+ # fetch the first page to learn the total number of data pages + metadata
1024
+ resp = self.tables.getTableById(
1025
+ pk=tid, _fields=["_all"], data_page=1, data_per_page=per_page
1026
+ ).result()
1027
+ pages = resp["total_data_pages"]
1028
+ table: dict[str, Any] = {
1029
+ "data": resp["data"],
1030
+ "index": resp["index"],
1031
+ "columns": resp["columns"],
1032
+ }
1033
+ table_attrs: dict[str, str] = (resp.get("attrs") or {}) | {
1034
+ field: resp.get(field) for field in {"id", "name", "md5"}
1035
+ }
1034
1036
 
1035
- page += 1
1037
+ # fetch any remaining pages concurrently, then reassemble in page order
1038
+ if pages > 1:
1039
+ futures = [
1040
+ self._get_future(
1041
+ page,
1042
+ {"_fields": "_all", "data_page": page, "data_per_page": per_page},
1043
+ rel_url=f"tables/{tid}",
1044
+ )
1045
+ for page in range(2, pages + 1)
1046
+ ]
1047
+ responses = _run_futures(
1048
+ futures, timeout=timeout, disable=self.mute_progress_bars
1049
+ )
1050
+ for page in sorted(responses):
1051
+ table["data"].extend(responses[page]["result"]["data"])
1036
1052
 
1037
1053
  return Table.from_dict(table | {"attrs": table_attrs})
1038
1054
 
@@ -29,20 +29,62 @@ level, handler or propagation setting, here. Child loggers (`mp_api.client.*`,
29
29
  including contribs) never set their own level, so one setting controls all of
30
30
  them. Use `MPRESTER_LOG_LEVEL` or `enable_logging(level)`.
31
31
 
32
+ Messages are shown in the console's default style; only the level is
33
+ coloured.
34
+
35
+ The client's warnings
36
+ ---------------------
37
+ The client's own warnings (`MPRestWarning`, its `FutureWarning` deprecations,
38
+ the `MPDataset*Warning`s) are ordinary Python warnings, so filters
39
+ (`warnings.filterwarnings`, `pytest.warns`, `logging.captureWarnings`) work
40
+ as usual. In a terminal / notebook they are shown in the same layout as log
41
+ records, with the warning class as the label and the caller's code below:
42
+
43
+ ```
44
+ MPRestWarning <message>
45
+ my_script.py:12
46
+ ```
47
+
48
+ Elsewhere, or if the application installed its own warnings hook (e.g.
49
+ `logging.captureWarnings(True)`), Python's usual format / the application's
50
+ hook is used.
51
+
52
+ Other libraries
53
+ ---------------
54
+ Warnings from other libraries (pymatgen, emmet, ...) raised while a client
55
+ method runs, and their log records that nothing else would handle, are shown
56
+ as WARNING records on `mp_api.client.external.<library>`, naming the library
57
+ code they came from. In a terminal / notebook:
58
+
59
+ ```
60
+ WARNING mp_api.client.external.pymatgen <message>
61
+ pymatgen/analysis/compatibility/mixing_scheme.py:320
62
+ ```
63
+
64
+ and on one line otherwise. Library deprecation warnings are left alone.
65
+ Silence one library with e.g.
66
+ `logging.getLogger("mp_api.client.external.pymatgen").setLevel(logging.ERROR)`.
67
+ See `mp_api.client.core._external`.
68
+
32
69
  Quiet mode
33
70
  ----------
34
71
  `quiet()` (or `MPRESTER_QUIET=true`, or `MPRester(quiet=True)`) silences the
35
- client for the whole process: no log records, progress bars, status lines,
36
- notices or `MPRestWarning`s. Deprecation warnings (`FutureWarning`) and
37
- exceptions are still raised. It can be used as a context manager.
72
+ client for the whole process: no log records (including those from other
73
+ libraries, above), progress bars, status lines, notices, `MPRestWarning`s or
74
+ `MPDataset*Warning`s.
75
+ Exceptions are still raised, and the client's deprecation warnings
76
+ (`FutureWarning`) are still shown unless `quiet(deprecations=True)` or
77
+ `MPRESTER_QUIET_DEPRECATIONS=true`. It can be used as a context manager; see
78
+ `quiet()` for exactly what is and isn't silenced.
38
79
 
39
80
  Which channel to use
40
81
  --------------------
41
82
  - The operation failed: raise an exception.
42
- - A deprecated API was used: `warnings.warn(..., FutureWarning)`, which is
43
- shown by default, also when called from other libraries.
83
+ - A deprecated API was used: `mp_deprecation(...)` (a `FutureWarning`), which
84
+ is shown by default, also when called from other libraries.
44
85
  - Something the caller can change in their call (an ignored argument, misuse,
45
- a caveat about what the data means): `mp_warning(...)` (an `MPRestWarning`).
86
+ a caveat about what the data means): `mp_warning(...)` (an `MPRestWarning`),
87
+ or `mp_advice(..., category)` for a more specific warning class.
46
88
  - A runtime or data event the caller can't change in code (server
47
89
  unreachable, data missing, a local dataset reused): `logger.warning/info`.
48
90
  - Diagnostics: `logger.debug`.
@@ -81,11 +123,11 @@ import io
81
123
  import logging
82
124
  import threading
83
125
  import time
84
- import warnings
85
126
  from contextlib import contextmanager
86
127
  from typing import TYPE_CHECKING, Self
87
128
 
88
129
  from rich.console import Console
130
+ from rich.highlighter import NullHighlighter
89
131
  from rich.logging import RichHandler
90
132
  from rich.progress import (
91
133
  BarColumn,
@@ -137,6 +179,8 @@ MP_THEME = Theme(
137
179
  "mp.error": "#F14668", # Bulma danger
138
180
  "mp.muted": "#7A7A7A", # Bulma .has-text-grey
139
181
  "mp.title": "bold underline",
182
+ "mp.logger.external": "#939FC8", # mp.secondary: logger of a library record
183
+ "mp.warning.label": "#D69E00", # class name of a client warning
140
184
  "mp.field": "bold",
141
185
  "mp.table.header": "bold #008F83", # brand dark
142
186
  # database versions, relative to the one the API serves
@@ -200,6 +244,35 @@ def _can_draw_live(console: Console) -> bool:
200
244
  return console.is_terminal and not console.is_dumb_terminal
201
245
 
202
246
 
247
+ def print_warning(label: str, message: str, location: str) -> bool:
248
+ """Show a client warning on the console, if it's interactive.
249
+
250
+ Rendered like the log records: the label (the warning's class name) in a
251
+ column of its own, the message in the default style, and where the
252
+ warning points to (the caller's code) on a muted line below it.
253
+
254
+ Args:
255
+ label (str) : e.g. "MPRestWarning"
256
+ message (str) : warning text
257
+ location (str) : "file:line"
258
+
259
+ Returns:
260
+ bool : False if nothing was shown (not an interactive console)
261
+ """
262
+ console = get_console()
263
+ if not _is_interactive(console):
264
+ return False
265
+ grid = Table.grid(padding=(0, 1), expand=True)
266
+ grid.add_column(style="mp.warning.label", no_wrap=True)
267
+ grid.add_column(ratio=1, overflow="fold")
268
+ grid.add_row(
269
+ Text(label),
270
+ Text.assemble(message.strip(), "\n", (location, "mp.muted")),
271
+ )
272
+ console.print(grid)
273
+ return True
274
+
275
+
203
276
  def print_notice(message: str, style: str | None = None) -> None:
204
277
  """Print a one-line notice to the console (stderr).
205
278
 
@@ -675,6 +748,25 @@ def _app_level(logger: logging.Logger) -> int:
675
748
  return logging.WARNING # pragma: no cover - root always has a level
676
749
 
677
750
 
751
+ class _MPRichHandler(RichHandler):
752
+ """`RichHandler` showing messages in the console's default style.
753
+
754
+ Only the level (and, for records re-emitted from other libraries, the
755
+ logger name and origin) is styled.
756
+ """
757
+
758
+ def render_message(self, record: logging.LogRecord, message: str) -> Text:
759
+ if (origin := getattr(record, "mp_origin", None)) is None:
760
+ return Text(message)
761
+ return Text.assemble(
762
+ (record.name, "mp.logger.external"),
763
+ " ",
764
+ str(getattr(record, "mp_message", message)),
765
+ "\n",
766
+ (origin, "mp.muted"),
767
+ )
768
+
769
+
678
770
  class _MPDefaultHandler(logging.Handler):
679
771
  """Default handler for `mp_api.client`, see module docstring."""
680
772
 
@@ -686,12 +778,14 @@ class _MPDefaultHandler(logging.Handler):
686
778
 
687
779
  def _rich_handler(self, console: Console) -> RichHandler:
688
780
  if self._rich is None or self._rich.console is not console:
689
- self._rich = RichHandler(
781
+ self._rich = _MPRichHandler(
690
782
  console=console,
691
783
  show_time=False,
692
784
  show_path=False,
693
785
  markup=False,
694
786
  rich_tracebacks=False,
787
+ highlighter=NullHighlighter(),
788
+ keywords=[],
695
789
  )
696
790
  return self._rich
697
791
 
@@ -768,10 +862,11 @@ _SILENT = logging.CRITICAL + 1
768
862
 
769
863
 
770
864
  class _QuietState:
771
- """Process-wide quiet flag. `saved_level` is the log level to restore."""
865
+ """Process-wide quiet flags. `saved_level` is the log level to restore."""
772
866
 
773
867
  def __init__(self) -> None:
774
868
  self.enabled = False
869
+ self.deprecations = False
775
870
  self.saved_level: int = logging.NOTSET
776
871
 
777
872
 
@@ -781,36 +876,58 @@ _QUIET = _QuietState()
781
876
  class _QuietContext:
782
877
  """Returned by `quiet()`; restores the previous state when used with `with`."""
783
878
 
784
- def __init__(self, previous: bool) -> None:
879
+ def __init__(self, previous: tuple[bool, bool]) -> None:
785
880
  self._previous = previous
786
881
 
787
882
  def __enter__(self) -> Self:
788
883
  return self
789
884
 
790
885
  def __exit__(self, *exc_info: object) -> None:
791
- _set_quiet(self._previous)
886
+ _set_quiet(*self._previous)
792
887
 
793
888
 
794
- def _set_quiet(enabled: bool) -> bool:
795
- """Turn quiet mode on or off, returning the previous state."""
889
+ def _set_quiet(enabled: bool, deprecations: bool = False) -> tuple[bool, bool]:
890
+ """Set quiet mode, returning the previous `(enabled, deprecations)`."""
796
891
  logger = logging.getLogger(LOGGER_NAME)
797
892
  with _handler_lock:
798
- previous = _QUIET.enabled
799
- if enabled and not previous:
893
+ previous = (_QUIET.enabled, _QUIET.deprecations)
894
+ if enabled and not _QUIET.enabled:
800
895
  _QUIET.saved_level = logger.level
801
896
  logger.setLevel(_SILENT)
802
- elif previous and not enabled:
897
+ elif _QUIET.enabled and not enabled:
803
898
  logger.setLevel(_QUIET.saved_level)
804
899
  _QUIET.enabled = enabled
900
+ _QUIET.deprecations = enabled and deprecations
805
901
  return previous
806
902
 
807
903
 
808
- def quiet(enabled: bool = True) -> _QuietContext:
809
- """Silence all output from the client in this process.
904
+ def quiet(enabled: bool = True, *, deprecations: bool = False) -> _QuietContext:
905
+ """Silence output from the client in this process.
906
+
907
+ Silenced while quiet:
908
+
909
+ - the client's log records (they don't reach the application's handlers
910
+ either), progress bars, status lines and notices
911
+ - `MPRestWarning`s and the `MPDataset*Warning`s (slow indexing, slicing
912
+ or iteration of downloaded datasets)
913
+ - warnings and fallback log output from other libraries (e.g. pymatgen)
914
+ raised while a client method runs; these are otherwise shown as
915
+ WARNING records on `mp_api.client.external.<library>`
916
+ - with `deprecations=True`, also the client's deprecation warnings
917
+ (`FutureWarning`)
810
918
 
811
- While quiet, the client emits no log records (they don't reach the
812
- application's handlers either), progress bars, status lines, notices or
813
- `MPRestWarning`s. Deprecation warnings and exceptions are unaffected.
919
+ Not silenced:
920
+
921
+ - exceptions
922
+ - deprecation warnings from other libraries
923
+ - output libraries print directly (`print()`, their own progress bars)
924
+ or write from compiled code
925
+ - log records sent to handlers that a library or the application set up
926
+ - library warnings raised outside client methods, e.g. on import, or when
927
+ using returned objects later
928
+ - warnings, if the application replaced `warnings.showwarning` itself
929
+ (e.g. `logging.captureWarnings(True)` after importing the client, or
930
+ `warnings.catch_warnings(record=True)`): they go where it sent them
814
931
 
815
932
  Takes effect immediately; as a context manager, the previous state is
816
933
  restored on exit:
@@ -821,15 +938,19 @@ def quiet(enabled: bool = True) -> _QuietContext:
821
938
  quiet() # for the rest of the process
822
939
  with quiet(): # only within the block
823
940
  ...
941
+ with quiet(deprecations=True): # also hide the client's deprecation warnings
942
+ ...
824
943
  ```
825
944
 
826
945
  Args:
827
946
  enabled (bool) : True to silence the client, False to restore output.
947
+ deprecations (bool) : also silence the client's deprecation warnings.
948
+ Only applies when `enabled` is True.
828
949
 
829
950
  Returns:
830
951
  a context manager that restores the previous state on exit
831
952
  """
832
- return _QuietContext(_set_quiet(enabled))
953
+ return _QuietContext(_set_quiet(enabled, deprecations))
833
954
 
834
955
 
835
956
  def is_quiet() -> bool:
@@ -850,13 +971,50 @@ def mp_warning(message: str, stacklevel: int = 1) -> None:
850
971
  return
851
972
  from mp_api.client.core.exceptions import MPRestWarning
852
973
 
853
- warnings.warn(message, MPRestWarning, stacklevel=stacklevel + 1)
974
+ _warn(message, MPRestWarning, stacklevel + 1)
975
+
976
+
977
+ def mp_advice(message: str, category: type[Warning], stacklevel: int = 1) -> None:
978
+ """Like `mp_warning`, with another warning class, e.g. `MPDatasetIndexingWarning`.
979
+
980
+ Args:
981
+ message (str) : warning text
982
+ category (type[Warning]) : warning class
983
+ stacklevel (int) : as for `warnings.warn`, relative to the caller
984
+ """
985
+ if _QUIET.enabled:
986
+ return
987
+ _warn(message, category, stacklevel + 1)
988
+
989
+
990
+ def mp_deprecation(message: str, stacklevel: int = 1) -> None:
991
+ """Emit a deprecation warning (`FutureWarning`) for the client's own API.
992
+
993
+ Shown by default; silenced by `quiet(deprecations=True)` or
994
+ `MPRESTER_QUIET_DEPRECATIONS`.
995
+
996
+ Args:
997
+ message (str) : warning text
998
+ stacklevel (int) : as for `warnings.warn`, relative to the caller
999
+ """
1000
+ if _QUIET.deprecations:
1001
+ return
1002
+ _warn(message, FutureWarning, stacklevel + 1)
1003
+
1004
+
1005
+ def _warn(message: str, category: type[Warning], stacklevel: int) -> None:
1006
+ """Warn as if from `_warn`'s caller, not counting public method wrappers."""
1007
+ from mp_api.client.core._external import warn
1008
+
1009
+ warn(message, category, stacklevel + 1)
854
1010
 
855
1011
 
856
1012
  def _configure_from_settings() -> None:
857
1013
  """Configure client output from settings, on import of `mp_api.client`."""
1014
+ from mp_api.client.core._external import install
858
1015
  from mp_api.client.core.settings import MAPI_CLIENT_SETTINGS
859
1016
 
1017
+ install()
860
1018
  enable_logging()
861
1019
  if MAPI_CLIENT_SETTINGS.QUIET:
862
- quiet(True)
1020
+ quiet(True, deprecations=MAPI_CLIENT_SETTINGS.QUIET_DEPRECATIONS)
@@ -33,11 +33,27 @@ if TYPE_CHECKING:
33
33
  from collections.abc import Callable, Iterator, Sequence
34
34
  from typing import Any
35
35
 
36
+ from pymatgen.analysis.compatibility.mixing_scheme import (
37
+ MaterialsProjectDFTMixingScheme,
38
+ )
36
39
  from pymatgen.entries.compatibility import Compatibility
37
40
 
38
41
  from mp_api.client.core._display import ProgressHandle
39
42
 
40
43
 
44
+ def mixing_scheme_class() -> type[MaterialsProjectDFTMixingScheme]:
45
+ """The `MaterialsProjectDFTMixingScheme` class, wherever this pymatgen keeps it.
46
+
47
+ Newer pymatgen moved it to `pymatgen.analysis.compatibility.mixing_scheme`
48
+ and deprecated the old module.
49
+ """
50
+ try:
51
+ from pymatgen.analysis.compatibility import mixing_scheme
52
+ except ImportError: # pragma: no cover - older pymatgen
53
+ from pymatgen.entries import mixing_scheme # type: ignore[no-redef]
54
+ return mixing_scheme.MaterialsProjectDFTMixingScheme
55
+
56
+
41
57
  def _advance(handle: ProgressHandle, amount: float) -> None:
42
58
  """Advance `handle` by `amount`, without going past its total."""
43
59
  if handle.total is not None:
@@ -173,7 +189,8 @@ def apply_corrections(
173
189
  enabled (bool) : whether to show progress
174
190
  """
175
191
  from pymatgen.entries.compatibility import MaterialsProjectAqueousCompatibility
176
- from pymatgen.entries.mixing_scheme import MaterialsProjectDFTMixingScheme
192
+
193
+ mixing_scheme = mixing_scheme_class()
177
194
 
178
195
  n = len(entries)
179
196
  corrector = _private_copy(corrector)
@@ -183,7 +200,7 @@ def apply_corrections(
183
200
  ) as handle,
184
201
  ExitStack() as stack,
185
202
  ):
186
- if isinstance(corrector, MaterialsProjectDFTMixingScheme):
203
+ if isinstance(corrector, mixing_scheme):
187
204
  # passes: compat_1 corrections (run type 1 entries), structure
188
205
  # matching (all entries), then the mixing adjustments (all entries)
189
206
  n_1 = sum(