mpcontribs-client 5.10.4__py3-none-any.whl → 5.10.5rc0__py3-none-any.whl

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.
@@ -1,5 +1,5 @@
1
- # -*- coding: utf-8 -*-
2
1
  import io
2
+ import importlib.metadata
3
3
  import sys
4
4
  import os
5
5
  import ujson
@@ -13,14 +13,12 @@ import itertools
13
13
  import functools
14
14
  import requests
15
15
  import logging
16
- import datetime
17
16
 
18
17
  from inspect import getfullargspec
19
18
  from math import isclose
20
- from semantic_version import Version
21
19
  from requests.exceptions import RequestException
22
20
  from bson.objectid import ObjectId
23
- from typing import Union, Type, Optional
21
+ from typing import Type
24
22
  from tqdm.auto import tqdm
25
23
  from hashlib import md5
26
24
  from pathlib import Path
@@ -59,7 +57,12 @@ from tempfile import gettempdir
59
57
  from plotly.express._chart_types import line as line_chart
60
58
  from cachetools import cached, LRUCache
61
59
  from cachetools.keys import hashkey
62
- from pymatgen.core import SETTINGS
60
+
61
+ try:
62
+ __version__ = importlib.metadata.version("mpcontribs-client")
63
+ except Exception:
64
+ # package is not installed
65
+ pass
63
66
 
64
67
  RETRIES = 3
65
68
  MAX_WORKERS = 3
@@ -88,6 +91,7 @@ VALID_URLS |= {f"http://localhost.{n}-api.materialsproject.org" for n in SUBDOMA
88
91
  SUPPORTED_FILETYPES = (Gz, Jpeg, Png, Gif, Tiff)
89
92
  SUPPORTED_MIMES = [t().mime for t in SUPPORTED_FILETYPES]
90
93
  DEFAULT_DOWNLOAD_DIR = Path.home() / "mpcontribs-downloads"
94
+ VALID_API_KEY_ALIASES = ["MPCONTRIBS_API_KEY", "MP_API_KEY", "PMG_MAPI_KEY"]
91
95
 
92
96
  j2h = Json2Html()
93
97
  pd.options.plotting.backend = "plotly"
@@ -126,7 +130,7 @@ _ipython = sys.modules["IPython"].get_ipython()
126
130
  class LogFilter(logging.Filter):
127
131
  def __init__(self, level, *args, **kwargs):
128
132
  self.level = level
129
- super(LogFilter, self).__init__(*args, **kwargs)
133
+ super().__init__(*args, **kwargs)
130
134
 
131
135
  def filter(self, record):
132
136
  return record.levelno < self.level
@@ -144,7 +148,7 @@ class TqdmToLogger(io.StringIO):
144
148
  buf = ""
145
149
 
146
150
  def __init__(self, logger, level=None):
147
- super(TqdmToLogger, self).__init__()
151
+ super().__init__()
148
152
  self.logger = logger
149
153
  self.level = level or logging.INFO
150
154
 
@@ -177,7 +181,7 @@ class MPContribsClientError(ValueError):
177
181
  """custom error for mpcontribs-client"""
178
182
 
179
183
 
180
- def get_md5(d):
184
+ def get_md5(d) -> str:
181
185
  s = ujson.dumps(d, sort_keys=True).encode("utf-8")
182
186
  return md5(s).hexdigest()
183
187
 
@@ -478,7 +482,7 @@ class Attachment(dict):
478
482
 
479
483
  return unpacked
480
484
 
481
- def write(self, outdir: Optional[Union[str, Path]] = None) -> Path:
485
+ def write(self, outdir: str | Path | None = None) -> Path:
482
486
  """Write attachment to file using its name
483
487
 
484
488
  Args:
@@ -490,7 +494,7 @@ class Attachment(dict):
490
494
  path.write_bytes(content)
491
495
  return path
492
496
 
493
- def display(self, outdir: Optional[Union[str, Path]] = None):
497
+ def display(self, outdir: str | Path | None = None):
494
498
  """Display Image/FileLink for attachment if in IPython/Jupyter
495
499
 
496
500
  Args:
@@ -519,7 +523,7 @@ class Attachment(dict):
519
523
  return self["name"]
520
524
 
521
525
  @classmethod
522
- def from_data(cls, data: Union[list, dict], name: str = "attachment"):
526
+ def from_data(cls, data: list | dict, name: str = "attachment"):
523
527
  """Construct attachment from data dict or list
524
528
 
525
529
  Args:
@@ -539,7 +543,7 @@ class Attachment(dict):
539
543
  )
540
544
 
541
545
  @classmethod
542
- def from_file(cls, path: Union[Path, str]):
546
+ def from_file(cls, path: str | Path):
543
547
  """Construct attachment from file
544
548
 
545
549
  Args:
@@ -616,7 +620,7 @@ class Attachments(list):
616
620
  return attachments
617
621
 
618
622
  @classmethod
619
- def from_data(cls, data: Union[list, dict], prefix: str = "attachment"):
623
+ def from_data(cls, data: list | dict, prefix: str = "attachment"):
620
624
  """Construct list of attachments from data dict or list
621
625
 
622
626
  Args:
@@ -830,32 +834,24 @@ def _expand_params(protocol, host, version, projects_json, apikey=None):
830
834
  def _version(url):
831
835
  retries, max_retries = 0, 3
832
836
  protocol = urlparse(url).scheme
833
- is_mock_test = "pytest" in sys.modules and protocol == "http"
834
-
835
- if is_mock_test:
836
- now = datetime.datetime.now()
837
- return Version(
838
- major=now.year,
839
- minor=now.month,
840
- patch=now.day,
841
- prerelease=(str(now.hour), str(now.minute)),
842
- )
843
- else:
844
- while retries < max_retries:
845
- try:
846
- r = requests.get(f"{url}/healthcheck", timeout=5)
847
- if r.status_code in {200, 403}:
848
- return r.json().get("version")
849
- else:
850
- retries += 1
851
- logger.warning(
852
- f"Healthcheck for {url} failed ({r.status_code})! Wait 30s."
853
- )
854
- time.sleep(30)
855
- except RequestException as ex:
837
+ if "pytest" in sys.modules and protocol == "http":
838
+ return __version__
839
+
840
+ while retries < max_retries:
841
+ try:
842
+ r = requests.get(f"{url}/healthcheck", timeout=5)
843
+ if r.status_code in {200, 403}:
844
+ return r.json().get("version")
845
+ else:
856
846
  retries += 1
857
- logger.warning(f"Could not connect to {url} ({ex})! Wait 30s.")
847
+ logger.warning(
848
+ f"Healthcheck for {url} failed ({r.status_code})! Wait 30s."
849
+ )
858
850
  time.sleep(30)
851
+ except RequestException as ex:
852
+ retries += 1
853
+ logger.warning(f"Could not connect to {url} ({ex})! Wait 30s.")
854
+ time.sleep(30)
859
855
 
860
856
 
861
857
  class Client(SwaggerClient):
@@ -870,11 +866,11 @@ class Client(SwaggerClient):
870
866
 
871
867
  def __init__(
872
868
  self,
873
- apikey: Optional[str] = None,
874
- headers: Optional[dict] = None,
875
- host: Optional[str] = None,
876
- project: Optional[str] = None,
877
- session: Optional[requests.Session] = None,
869
+ apikey: str | None = None,
870
+ headers: dict | None = None,
871
+ host: str | None = None,
872
+ project: str | None = None,
873
+ session: requests.Session | None = None,
878
874
  ):
879
875
  """Initialize the client - only reloads API spec from server as needed
880
876
 
@@ -892,7 +888,17 @@ class Client(SwaggerClient):
892
888
  host = os.environ.get("MPCONTRIBS_API_HOST", DEFAULT_HOST)
893
889
 
894
890
  if not apikey:
895
- apikey = os.environ.get("MPCONTRIBS_API_KEY", SETTINGS.get("PMG_MAPI_KEY"))
891
+ try:
892
+ apikey = next(
893
+ os.environ.get(kalias)
894
+ for kalias in VALID_API_KEY_ALIASES
895
+ if kalias is not None
896
+ )
897
+ except StopIteration:
898
+ from pymatgen.core import SETTINGS
899
+
900
+ apikey = SETTINGS.get("PMG_MAPI_KEY")
901
+
896
902
  if apikey and len(apikey) != 32:
897
903
  raise MPContribsClientError(f"Invalid API key: {apikey}")
898
904
 
@@ -925,7 +931,7 @@ class Client(SwaggerClient):
925
931
  def __enter__(self):
926
932
  return self
927
933
 
928
- def __exit__(self, exc_type, exc_val, exc_tb):
934
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
929
935
  return None
930
936
 
931
937
  @property
@@ -934,17 +940,18 @@ class Client(SwaggerClient):
934
940
  self.protocol, self.host, self.headers_json, self.project, self.version
935
941
  )
936
942
 
937
- def __dir__(self):
943
+ def __dir__(self) -> set[str]:
938
944
  members = set(self.swagger_spec.resources.keys())
939
- members |= set(k for k in self.__dict__.keys() if not k.startswith("_"))
940
- members |= set(k for k in dir(self.__class__) if not k.startswith("_"))
945
+ members |= {k for k in self.__dict__.keys() if not k.startswith("_")}
946
+ members |= {k for k in dir(self.__class__) if not k.startswith("_")}
941
947
  return members
942
948
 
943
949
  def _reinit(self):
944
950
  _load.cache_clear()
945
951
  super().__init__(self.cached_swagger_spec)
946
952
 
947
- def _is_valid_payload(self, model: str, data: dict):
953
+ def _is_valid_payload(self, model: str, data: dict) -> None:
954
+ """Raise an error if a payload is invalid."""
948
955
  model_spec = deepcopy(self.get_model(f"{model}sSchema")._model_spec)
949
956
  model_spec.pop("required")
950
957
  model_spec["additionalProperties"] = False
@@ -952,17 +959,20 @@ class Client(SwaggerClient):
952
959
  try:
953
960
  validate_object(self.swagger_spec, model_spec, data)
954
961
  except ValidationError as ex:
955
- return False, str(ex)
956
-
957
- return True, None
958
-
959
- def _is_serializable_dict(self, dct):
960
- for k, v in flatten(dct, reducer="dot").items():
961
- if v is not None and not isinstance(v, (str, int, float)):
962
- error = f"Value {v} of {type(v)} for key {k} not supported."
963
- return False, error
962
+ raise MPContribsClientError(str(ex))
964
963
 
965
- return True, None
964
+ def _is_serializable_dict(self, dct: dict) -> None:
965
+ """Raise an error if an input dict is not JSON serializable."""
966
+ try:
967
+ raise MPContribsClientError(
968
+ next(
969
+ f"Value {v} of {type(v)} for key {k} not supported."
970
+ for k, v in flatten(dct, reducer="dot").items()
971
+ if v is not None and not isinstance(v, (str, int, float))
972
+ )
973
+ )
974
+ except StopIteration:
975
+ pass
966
976
 
967
977
  def _get_per_page_default_max(
968
978
  self, op: str = "query", resource: str = "contributions"
@@ -1030,14 +1040,10 @@ class Client(SwaggerClient):
1030
1040
 
1031
1041
  for q in queries:
1032
1042
  # copy over missing parameters
1033
- for k, v in query.items():
1034
- if k not in q:
1035
- q[k] = v
1043
+ q.update({k: v for k, v in query.items() if k not in q})
1036
1044
 
1037
1045
  # comma-separated lists
1038
- for k, v in q.items():
1039
- if isinstance(v, list):
1040
- q[k] = ",".join(v)
1046
+ q.update({k: ",".join(v) for k, v in q.items() if isinstance(v, list)})
1041
1047
 
1042
1048
  return queries
1043
1049
 
@@ -1047,7 +1053,7 @@ class Client(SwaggerClient):
1047
1053
  params: dict,
1048
1054
  rel_url: str = "contributions",
1049
1055
  op: str = "query",
1050
- data: Optional[dict] = None,
1056
+ data: dict | None = None,
1051
1057
  ):
1052
1058
  rname = rel_url.split("/", 1)[0]
1053
1059
  resource = self.swagger_spec.resources[rname]
@@ -1066,7 +1072,7 @@ class Client(SwaggerClient):
1066
1072
 
1067
1073
  def available_query_params(
1068
1074
  self,
1069
- startswith: Optional[tuple] = None,
1075
+ startswith: tuple | None = None,
1070
1076
  resource: str = "contributions",
1071
1077
  ) -> list:
1072
1078
  resources = self.swagger_spec.resources
@@ -1083,9 +1089,7 @@ class Client(SwaggerClient):
1083
1089
 
1084
1090
  return [param for param in params if param.startswith(startswith)]
1085
1091
 
1086
- def get_project(
1087
- self, name: Optional[str] = None, fields: Optional[list] = None
1088
- ) -> Dict:
1092
+ def get_project(self, name: str | None = None, fields: list | None = None) -> Dict:
1089
1093
  """Retrieve a project entry
1090
1094
 
1091
1095
  Args:
@@ -1103,10 +1107,10 @@ class Client(SwaggerClient):
1103
1107
 
1104
1108
  def query_projects(
1105
1109
  self,
1106
- query: Optional[dict] = None,
1107
- term: Optional[str] = None,
1108
- fields: Optional[list] = None,
1109
- sort: Optional[str] = None,
1110
+ query: dict | None = None,
1111
+ term: str | None = None,
1112
+ fields: list | None = None,
1113
+ sort: str | None = None,
1110
1114
  timeout: int = -1,
1111
1115
  ) -> list[dict]:
1112
1116
  """Query projects by query and/or term (Atlas Search)
@@ -1157,9 +1161,13 @@ class Client(SwaggerClient):
1157
1161
  if total_pages < 2:
1158
1162
  return ret["data"]
1159
1163
 
1160
- for field in ["name__in", "_fields"]:
1161
- if field in query:
1162
- query[field] = ",".join(query[field])
1164
+ query.update(
1165
+ {
1166
+ field: ",".join(query[field])
1167
+ for field in ["name__in", "_fields"]
1168
+ if field in query
1169
+ }
1170
+ )
1163
1171
 
1164
1172
  queries = []
1165
1173
 
@@ -1172,8 +1180,7 @@ class Client(SwaggerClient):
1172
1180
  ]
1173
1181
  responses = _run_futures(futures, total=total_count, timeout=timeout)
1174
1182
 
1175
- for resp in responses.values():
1176
- ret["data"] += resp["result"]["data"]
1183
+ ret["data"].extend([resp["result"]["data"] for resp in responses.values()])
1177
1184
 
1178
1185
  return ret["data"]
1179
1186
 
@@ -1210,7 +1217,7 @@ class Client(SwaggerClient):
1210
1217
  else:
1211
1218
  raise MPContribsClientError(resp)
1212
1219
 
1213
- def update_project(self, update: dict, name: Optional[str] = None):
1220
+ def update_project(self, update: dict, name: str | None = None):
1214
1221
  """Update project info
1215
1222
 
1216
1223
  Args:
@@ -1268,15 +1275,12 @@ class Client(SwaggerClient):
1268
1275
  logger.warning("nothing to update")
1269
1276
  return
1270
1277
 
1271
- valid, error = self._is_valid_payload("Project", payload)
1272
- if valid:
1273
- resp = self.projects.updateProjectByName(pk=name, project=payload).result()
1274
- if not resp.get("count", 0):
1275
- raise MPContribsClientError(resp)
1276
- else:
1277
- raise MPContribsClientError(error)
1278
+ self._is_valid_payload("Project", payload)
1279
+ resp = self.projects.updateProjectByName(pk=name, project=payload).result()
1280
+ if not resp.get("count", 0):
1281
+ raise MPContribsClientError(resp)
1278
1282
 
1279
- def delete_project(self, name: Optional[str] = None):
1283
+ def delete_project(self, name: str | None = None):
1280
1284
  """Delete a project
1281
1285
 
1282
1286
  Args:
@@ -1295,7 +1299,7 @@ class Client(SwaggerClient):
1295
1299
  if resp and "error" in resp:
1296
1300
  raise MPContribsClientError(resp["error"])
1297
1301
 
1298
- def get_contribution(self, cid: str, fields: Optional[list] = None) -> Dict:
1302
+ def get_contribution(self, cid: str, fields: list | None = None) -> Dict:
1299
1303
  """Retrieve a contribution
1300
1304
 
1301
1305
  Args:
@@ -1408,7 +1412,7 @@ class Client(SwaggerClient):
1408
1412
  )
1409
1413
 
1410
1414
  def init_columns(
1411
- self, columns: Optional[dict] = None, name: Optional[str] = None
1415
+ self, columns: dict | None = None, name: str | None = None
1412
1416
  ) -> dict:
1413
1417
  """initialize columns for a project to set their order and desired units
1414
1418
 
@@ -1532,20 +1536,23 @@ class Client(SwaggerClient):
1532
1536
  new_unit = new_column.get("unit", "NaN")
1533
1537
  existing_unit = existing_column.get("unit")
1534
1538
  if existing_unit != new_unit:
1535
- conv_args = []
1536
- for u in [existing_unit, new_unit]:
1539
+ if existing_unit == "NaN" and new_unit == "":
1540
+ factor = 1
1541
+ else:
1542
+ conv_args = []
1543
+ for u in [existing_unit, new_unit]:
1544
+ try:
1545
+ conv_args.append(ureg.Unit(u))
1546
+ except ValueError:
1547
+ raise MPContribsClientError(
1548
+ f"Can't convert {existing_unit} to {new_unit} for {path}"
1549
+ )
1537
1550
  try:
1538
- conv_args.append(ureg.Unit(u))
1539
- except ValueError:
1551
+ factor = ureg.convert(1, *conv_args)
1552
+ except DimensionalityError:
1540
1553
  raise MPContribsClientError(
1541
1554
  f"Can't convert {existing_unit} to {new_unit} for {path}"
1542
1555
  )
1543
- try:
1544
- factor = ureg.convert(1, *conv_args)
1545
- except DimensionalityError:
1546
- raise MPContribsClientError(
1547
- f"Can't convert {existing_unit} to {new_unit} for {path}"
1548
- )
1549
1556
 
1550
1557
  if not isclose(factor, 1):
1551
1558
  logger.info(
@@ -1560,13 +1567,11 @@ class Client(SwaggerClient):
1560
1567
  new_columns.append(new_column)
1561
1568
 
1562
1569
  payload = {"columns": new_columns}
1563
- valid, error = self._is_valid_payload("Project", payload)
1564
- if not valid:
1565
- raise MPContribsClientError(error)
1570
+ self._is_valid_payload("Project", payload)
1566
1571
 
1567
1572
  return self.projects.updateProjectByName(pk=name, project=payload).result()
1568
1573
 
1569
- def delete_contributions(self, query: Optional[dict] = None, timeout: int = -1):
1574
+ def delete_contributions(self, query: dict | None = None, timeout: int = -1):
1570
1575
  """Remove all contributions for a query
1571
1576
 
1572
1577
  Args:
@@ -1612,7 +1617,7 @@ class Client(SwaggerClient):
1612
1617
 
1613
1618
  def get_totals(
1614
1619
  self,
1615
- query: Optional[dict] = None,
1620
+ query: dict | None = None,
1616
1621
  timeout: int = -1,
1617
1622
  resource: str = "contributions",
1618
1623
  op: str = "query",
@@ -1641,23 +1646,23 @@ class Client(SwaggerClient):
1641
1646
  query = {k: v for k, v in query.items() if k not in skip_keys}
1642
1647
  query["_fields"] = [] # only need totals -> explicitly request no fields
1643
1648
  queries = self._split_query(query, resource=resource, op=op) # don't paginate
1644
- result = {"total_count": 0, "total_pages": 0}
1645
1649
  futures = [
1646
1650
  self._get_future(i, q, rel_url=resource) for i, q in enumerate(queries)
1647
1651
  ]
1648
1652
  responses = _run_futures(futures, timeout=timeout, desc="Totals")
1649
1653
 
1650
- for resp in responses.values():
1651
- for k in result:
1652
- result[k] += resp.get("result", {}).get(k, 0)
1654
+ result = {
1655
+ k: sum(resp.get("result", {}).get(k, 0) for resp in responses.values())
1656
+ for k in ("total_count", "total_pages")
1657
+ }
1653
1658
 
1654
1659
  return result["total_count"], result["total_pages"]
1655
1660
 
1656
- def count(self, query: Optional[dict] = None) -> int:
1661
+ def count(self, query: dict | None = None) -> int:
1657
1662
  """shortcut for get_totals()"""
1658
1663
  return self.get_totals(query=query)[0]
1659
1664
 
1660
- def get_unique_identifiers_flags(self, query: Optional[dict] = None) -> dict:
1665
+ def get_unique_identifiers_flags(self, query: dict | None = None) -> dict:
1661
1666
  """Retrieve values for `unique_identifiers` flags.
1662
1667
 
1663
1668
  See `client.available_query_params(resource="projects")` for available query parameters.
@@ -1677,10 +1682,10 @@ class Client(SwaggerClient):
1677
1682
 
1678
1683
  def get_all_ids(
1679
1684
  self,
1680
- query: Optional[dict] = None,
1681
- include: Optional[list[str]] = None,
1685
+ query: dict | None = None,
1686
+ include: list[str] | None = None,
1682
1687
  timeout: int = -1,
1683
- data_id_fields: Optional[dict] = None,
1688
+ data_id_fields: dict | None = None,
1684
1689
  fmt: str = "sets",
1685
1690
  op: str = "query",
1686
1691
  ) -> dict:
@@ -1731,7 +1736,7 @@ class Client(SwaggerClient):
1731
1736
  }, ...}
1732
1737
  """
1733
1738
  include = include or []
1734
- components = set(x for x in include if x in COMPONENTS)
1739
+ components = {x for x in include if x in COMPONENTS}
1735
1740
  if include and not components:
1736
1741
  raise MPContribsClientError(f"`include` must be subset of {COMPONENTS}!")
1737
1742
 
@@ -1745,9 +1750,13 @@ class Client(SwaggerClient):
1745
1750
 
1746
1751
  unique_identifiers = self.get_unique_identifiers_flags()
1747
1752
  data_id_fields = data_id_fields or {}
1748
- for k, v in data_id_fields.items():
1749
- if k in unique_identifiers and isinstance(v, str):
1750
- data_id_fields[k] = v
1753
+ data_id_fields.update(
1754
+ {
1755
+ k: v
1756
+ for k, v in data_id_fields.items()
1757
+ if k in unique_identifiers and isinstance(v, str)
1758
+ }
1759
+ )
1751
1760
 
1752
1761
  ret = {}
1753
1762
  query = query or {}
@@ -1810,34 +1819,40 @@ class Client(SwaggerClient):
1810
1819
  if data_id_field and data_id_field_val:
1811
1820
  ret[project][identifier][data_id_field] = data_id_field_val
1812
1821
 
1813
- for component in components:
1814
- if component in contrib:
1815
- ret[project][identifier][component] = {
1822
+ ret[project][identifier].update(
1823
+ {
1824
+ component: {
1816
1825
  d["name"]: {"id": d["id"], "md5": d["md5"]}
1817
1826
  for d in contrib[component]
1818
1827
  }
1828
+ for component in components
1829
+ if component in contrib
1830
+ }
1831
+ )
1819
1832
 
1820
1833
  elif data_id_field and data_id_field_val:
1821
1834
  ret[project][identifier] = {
1822
1835
  data_id_field_val: {"id": contrib["id"]}
1823
1836
  }
1824
1837
 
1825
- for component in components:
1826
- if component in contrib:
1827
- ret[project][identifier][data_id_field_val][
1828
- component
1829
- ] = {
1838
+ ret[project][identifier][data_id_field_val].update(
1839
+ {
1840
+ component: {
1830
1841
  d["name"]: {"id": d["id"], "md5": d["md5"]}
1831
1842
  for d in contrib[component]
1832
1843
  }
1844
+ for component in components
1845
+ if component in contrib
1846
+ }
1847
+ )
1833
1848
 
1834
1849
  return ret
1835
1850
 
1836
1851
  def query_contributions(
1837
1852
  self,
1838
- query: Optional[dict] = None,
1839
- fields: Optional[list] = None,
1840
- sort: Optional[str] = None,
1853
+ query: dict | None = None,
1854
+ fields: list | None = None,
1855
+ sort: str | None = None,
1841
1856
  paginate: bool = False,
1842
1857
  timeout: int = -1,
1843
1858
  ) -> dict:
@@ -1861,12 +1876,11 @@ class Client(SwaggerClient):
1861
1876
  query["project"] = self.project
1862
1877
 
1863
1878
  if paginate:
1864
- cids = []
1865
-
1866
- for v in self.get_all_ids(query).values():
1867
- cids_project = v.get("ids")
1868
- if cids_project:
1869
- cids.extend(cids_project)
1879
+ cids = [
1880
+ idx
1881
+ for v in self.get_all_ids(query).values()
1882
+ for idx in (v.get("ids") or [])
1883
+ ]
1870
1884
 
1871
1885
  if not cids:
1872
1886
  raise MPContribsClientError("No contributions match the query.")
@@ -1891,7 +1905,7 @@ class Client(SwaggerClient):
1891
1905
  return ret
1892
1906
 
1893
1907
  def update_contributions(
1894
- self, data: dict, query: Optional[dict] = None, timeout: int = -1
1908
+ self, data: dict, query: dict | None = None, timeout: int = -1
1895
1909
  ) -> dict:
1896
1910
  """Apply the same update to all contributions in a project (matching query)
1897
1911
 
@@ -1906,14 +1920,10 @@ class Client(SwaggerClient):
1906
1920
  raise MPContribsClientError("Nothing to update.")
1907
1921
 
1908
1922
  tic = time.perf_counter()
1909
- valid, error = self._is_valid_payload("Contribution", data)
1910
- if not valid:
1911
- raise MPContribsClientError(error)
1923
+ self._is_valid_payload("Contribution", data)
1912
1924
 
1913
1925
  if "data" in data:
1914
- serializable, error = self._is_serializable_dict(data["data"])
1915
- if not serializable:
1916
- raise MPContribsClientError(error)
1926
+ self._is_serializable_dict(data["data"])
1917
1927
 
1918
1928
  query = query or {}
1919
1929
 
@@ -1939,7 +1949,7 @@ class Client(SwaggerClient):
1939
1949
 
1940
1950
  # get current list of data columns to decide if swagger reload is needed
1941
1951
  resp = self.projects.getProjectByName(pk=name, _fields=["columns"]).result()
1942
- old_paths = set(c["path"] for c in resp["columns"])
1952
+ old_paths = {c["path"] for c in resp["columns"]}
1943
1953
 
1944
1954
  total = len(cids)
1945
1955
  cids_query = {"id__in": cids}
@@ -1954,7 +1964,7 @@ class Client(SwaggerClient):
1954
1964
 
1955
1965
  if updated:
1956
1966
  resp = self.projects.getProjectByName(pk=name, _fields=["columns"]).result()
1957
- new_paths = set(c["path"] for c in resp["columns"])
1967
+ new_paths = {c["path"] for c in resp["columns"]}
1958
1968
 
1959
1969
  if new_paths != old_paths:
1960
1970
  self.init_columns(name=name)
@@ -1964,7 +1974,7 @@ class Client(SwaggerClient):
1964
1974
  return {"updated": updated, "total": total, "seconds_elapsed": toc - tic}
1965
1975
 
1966
1976
  def make_public(
1967
- self, query: Optional[dict] = None, recursive: bool = False, timeout: int = -1
1977
+ self, query: dict | None = None, recursive: bool = False, timeout: int = -1
1968
1978
  ) -> dict:
1969
1979
  """Publish a project and optionally its contributions
1970
1980
 
@@ -1977,7 +1987,7 @@ class Client(SwaggerClient):
1977
1987
  )
1978
1988
 
1979
1989
  def make_private(
1980
- self, query: Optional[dict] = None, recursive: bool = False, timeout: int = -1
1990
+ self, query: dict | None = None, recursive: bool = False, timeout: int = -1
1981
1991
  ) -> dict:
1982
1992
  """Make a project and optionally its contributions private
1983
1993
 
@@ -1992,7 +2002,7 @@ class Client(SwaggerClient):
1992
2002
  def _set_is_public(
1993
2003
  self,
1994
2004
  is_public: bool,
1995
- query: Optional[dict] = None,
2005
+ query: dict | None = None,
1996
2006
  recursive: bool = False,
1997
2007
  timeout: int = -1,
1998
2008
  ) -> dict:
@@ -2124,9 +2134,13 @@ class Client(SwaggerClient):
2124
2134
  resp = self.get_all_ids(dict(id__in=collect_ids), timeout=timeout)
2125
2135
  project_names |= set(resp.keys())
2126
2136
 
2127
- for project_name, values in resp.items():
2128
- for cid in values["ids"]:
2129
- id2project[cid] = project_name
2137
+ id2project.update(
2138
+ {
2139
+ cid: project_name
2140
+ for project_name, values in resp.items()
2141
+ for cid in values["ids"]
2142
+ }
2143
+ )
2130
2144
 
2131
2145
  existing = defaultdict(dict)
2132
2146
  unique_identifiers = defaultdict(dict)
@@ -2160,9 +2174,7 @@ class Client(SwaggerClient):
2160
2174
  for contrib in tqdm(contributions, desc="Prepare"):
2161
2175
  if "data" in contrib:
2162
2176
  contrib["data"] = unflatten(contrib["data"], splitter="dot")
2163
- serializable, error = self._is_serializable_dict(contrib["data"])
2164
- if not serializable:
2165
- raise MPContribsClientError(error)
2177
+ self._is_serializable_dict(contrib["data"])
2166
2178
 
2167
2179
  update = "id" in contrib
2168
2180
  project_name = id2project[contrib["id"]] if update else contrib["project"]
@@ -2282,13 +2294,7 @@ class Client(SwaggerClient):
2282
2294
  digests[project_name][component].add(digest)
2283
2295
  contribs[project_name][-1][component].append(dct)
2284
2296
 
2285
- valid, error = self._is_valid_payload(
2286
- "Contribution", contribs[project_name][-1]
2287
- )
2288
- if not valid:
2289
- raise MPContribsClientError(
2290
- f"{contrib['identifier']} invalid: {error}!"
2291
- )
2297
+ self._is_valid_payload("Contribution", contribs[project_name][-1])
2292
2298
 
2293
2299
  # submit contributions
2294
2300
  if contribs:
@@ -2425,10 +2431,10 @@ class Client(SwaggerClient):
2425
2431
 
2426
2432
  def download_contributions(
2427
2433
  self,
2428
- query: Optional[dict] = None,
2429
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2434
+ query: dict | None = None,
2435
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2430
2436
  overwrite: bool = False,
2431
- include: Optional[list[str]] = None,
2437
+ include: list[str] | None = None,
2432
2438
  timeout: int = -1,
2433
2439
  ) -> list:
2434
2440
  """Download a list of contributions as .json.gz file(s)
@@ -2448,7 +2454,7 @@ class Client(SwaggerClient):
2448
2454
  include = include or []
2449
2455
  outdir = Path(outdir) or Path(".")
2450
2456
  outdir.mkdir(parents=True, exist_ok=True)
2451
- components = set(x for x in include if x in COMPONENTS)
2457
+ components = {x for x in include if x in COMPONENTS}
2452
2458
  if include and not components:
2453
2459
  raise MPContribsClientError(f"`include` must be subset of {COMPONENTS}!")
2454
2460
 
@@ -2527,7 +2533,7 @@ class Client(SwaggerClient):
2527
2533
  def download_structures(
2528
2534
  self,
2529
2535
  ids: list[str],
2530
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2536
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2531
2537
  overwrite: bool = False,
2532
2538
  timeout: int = -1,
2533
2539
  fmt: str = "json",
@@ -2556,7 +2562,7 @@ class Client(SwaggerClient):
2556
2562
  def download_tables(
2557
2563
  self,
2558
2564
  ids: list[str],
2559
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2565
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2560
2566
  overwrite: bool = False,
2561
2567
  timeout: int = -1,
2562
2568
  fmt: str = "json",
@@ -2585,7 +2591,7 @@ class Client(SwaggerClient):
2585
2591
  def download_attachments(
2586
2592
  self,
2587
2593
  ids: list[str],
2588
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2594
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2589
2595
  overwrite: bool = False,
2590
2596
  timeout: int = -1,
2591
2597
  fmt: str = "json",
@@ -2615,7 +2621,7 @@ class Client(SwaggerClient):
2615
2621
  self,
2616
2622
  resource: str,
2617
2623
  ids: list[str],
2618
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2624
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2619
2625
  overwrite: bool = False,
2620
2626
  timeout: int = -1,
2621
2627
  fmt: str = "json",
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpcontribs-client
3
+ Version: 5.10.5rc0
4
+ Summary: Client library for MPContribs API
5
+ Author-email: Patrick Huck <phuck@lbl.gov>, The Materials Project <feedback@materialsproject.org>
6
+ License-Expression: BSD-3-Clause-LBNL
7
+ Project-URL: Homepage, https://github.com/materialsproject/MPContribs
8
+ Project-URL: Documentation, https://docs.materialsproject.org/services/mpcontribs
9
+ Requires-Python: >=3.11
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: boltons
13
+ Requires-Dist: bravado
14
+ Requires-Dist: filetype
15
+ Requires-Dist: flatten-dict
16
+ Requires-Dist: ipython
17
+ Requires-Dist: json2html
18
+ Requires-Dist: pandas
19
+ Requires-Dist: pint
20
+ Requires-Dist: plotly
21
+ Requires-Dist: pyIsEmail
22
+ Requires-Dist: pymatgen
23
+ Requires-Dist: pymongo
24
+ Requires-Dist: requests-futures
25
+ Requires-Dist: swagger-spec-validator
26
+ Requires-Dist: tqdm
27
+ Requires-Dist: ujson
28
+ Requires-Dist: cachetools
29
+ Provides-Extra: dev
30
+ Requires-Dist: flake8; extra == "dev"
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-flake8; extra == "dev"
33
+ Requires-Dist: pytest-pycodestyle; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: pytest-xdist; extra == "dev"
36
+ Requires-Dist: py; extra == "dev"
37
+ Provides-Extra: all
38
+ Requires-Dist: mpcontribs-client[dev]; extra == "all"
39
+ Dynamic: license-file
@@ -0,0 +1,6 @@
1
+ mpcontribs/client/__init__.py,sha256=YOv2KfnQK4T7rHp45XCudqzmv1C4EjeDcgH_VyAog1o,97366
2
+ mpcontribs_client-5.10.5rc0.dist-info/licenses/LICENSE,sha256=5tG0Niaqw2hnuyZZYkRXLSnfVrZA47COwduU_6caPLM,1074
3
+ mpcontribs_client-5.10.5rc0.dist-info/METADATA,sha256=1Mry_eGOJVD61aLEhjeElyA6geL9HRPIwb4aDydD8Ps,1289
4
+ mpcontribs_client-5.10.5rc0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
5
+ mpcontribs_client-5.10.5rc0.dist-info/top_level.txt,sha256=t8R5L_Dg9oDQMh2gyRFdZGnrzZsr7OjCBTrhTcmimC8,11
6
+ mpcontribs_client-5.10.5rc0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mpcontribs-client
3
- Version: 5.10.4
4
- Summary: client library for MPContribs API
5
- Home-page: https://github.com/materialsproject/MPContribs/tree/master/mpcontribs-client
6
- Author: Patrick Huck
7
- Author-email: phuck@lbl.gov
8
- License: MIT
9
- Requires-Python: >=3.8
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: numpy
13
- Requires-Dist: boltons
14
- Requires-Dist: bravado
15
- Requires-Dist: filetype
16
- Requires-Dist: flatten-dict
17
- Requires-Dist: ipython
18
- Requires-Dist: json2html
19
- Requires-Dist: pandas
20
- Requires-Dist: pint
21
- Requires-Dist: plotly
22
- Requires-Dist: pyIsEmail
23
- Requires-Dist: pymatgen
24
- Requires-Dist: pymongo
25
- Requires-Dist: requests-futures
26
- Requires-Dist: swagger-spec-validator
27
- Requires-Dist: tqdm
28
- Requires-Dist: ujson
29
- Requires-Dist: semantic-version
30
- Requires-Dist: cachetools
31
- Provides-Extra: dev
32
- Requires-Dist: flake8; extra == "dev"
33
- Requires-Dist: pytest; extra == "dev"
34
- Requires-Dist: pytest-flake8; extra == "dev"
35
- Requires-Dist: pytest-pycodestyle; extra == "dev"
36
- Requires-Dist: pytest-cov; extra == "dev"
37
- Requires-Dist: py; extra == "dev"
38
- Dynamic: author
39
- Dynamic: author-email
40
- Dynamic: description
41
- Dynamic: description-content-type
42
- Dynamic: home-page
43
- Dynamic: license
44
- Dynamic: license-file
45
- Dynamic: provides-extra
46
- Dynamic: requires-dist
47
- Dynamic: requires-python
48
- Dynamic: summary
49
-
50
- ![PyPI](https://img.shields.io/pypi/v/mpcontribs-client?style=flat-square)
51
- ![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/pypi/mpcontribs-client?style=flat-square)
52
-
53
- Small, dynamic python client library to connect to [MPContribs](https://docs.mpcontribs.org)
54
- APIs based on Yelp's [bravado](https://bravado.readthedocs.io).
55
-
56
- ```python
57
- from mpcontribs.client import Client
58
- client = Client()
59
- dir(client) # show available resources
60
- ```
61
-
62
- By default, the client connects to https://contribs-api.materialsproject.org and uses the environment variable
63
- `MPCONTRIBS_API_KEY` to set the API key. The key can alternatively be set explicitly via the
64
- `apikey` argument to the constructor. The `host` argument or the `MPCONTRIBS_API_HOST`
65
- environment variable can be set to connect to other MPContribs-style APIs:
66
-
67
- ```python
68
- client = Client(host='ml-api.materialsproject.org')
69
- ```
70
-
71
- **Troubleshooting**
72
-
73
- ```
74
- twisted.web._newclient.ResponseNeverReceived:
75
- [<twisted.python.failure.Failure OpenSSL.SSL.Error:
76
- [('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')]>]
77
- ```
78
-
79
- Set the environment variable `SSL_CERT_FILE` to `$(python -m certifi)`.
80
-
81
- ```
82
- OverflowError: timeout value is too large
83
- ```
84
-
85
- Install the bravado fork ([PR](https://github.com/Yelp/bravado/pull/472)) manually via
86
- ```
87
- pip install "bravado[fido] @ git+https://github.com/tschaume/bravado@9ce06f2df7118e16af4a3d3fdc21ccfeedc5cd50#egg=bravado-11.0.3"
88
- ```
@@ -1,6 +0,0 @@
1
- mpcontribs/client/__init__.py,sha256=9BAtCiXfx38ueipZB5sxVw7208-n6sxDrQ6MGNG8Cuw,97518
2
- mpcontribs_client-5.10.4.dist-info/licenses/LICENSE,sha256=5tG0Niaqw2hnuyZZYkRXLSnfVrZA47COwduU_6caPLM,1074
3
- mpcontribs_client-5.10.4.dist-info/METADATA,sha256=wXAUbmYkDzPOcQqIAGVyTliExfTPjbtS7KpP4NuUpM8,2798
4
- mpcontribs_client-5.10.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- mpcontribs_client-5.10.4.dist-info/top_level.txt,sha256=t8R5L_Dg9oDQMh2gyRFdZGnrzZsr7OjCBTrhTcmimC8,11
6
- mpcontribs_client-5.10.4.dist-info/RECORD,,