mpcontribs-client 5.10.4__py3-none-any.whl → 5.10.5rc1__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,65 +1,68 @@
1
- # -*- coding: utf-8 -*-
1
+ import functools
2
+ import gzip
3
+ import importlib.metadata
2
4
  import io
3
- import sys
5
+ import itertools
6
+ import logging
4
7
  import os
5
- import ujson
8
+ import sys
6
9
  import time
7
- import gzip
8
10
  import warnings
9
- import pandas as pd
10
- import numpy as np
11
- import plotly.io as pio
12
- import itertools
13
- import functools
14
- import requests
15
- import logging
16
- import datetime
17
-
11
+ from base64 import b64decode, b64encode, urlsafe_b64encode
12
+ from collections import defaultdict
13
+ from concurrent.futures import as_completed
14
+ from copy import deepcopy
15
+ from hashlib import md5
18
16
  from inspect import getfullargspec
19
17
  from math import isclose
20
- from semantic_version import Version
21
- from requests.exceptions import RequestException
22
- from bson.objectid import ObjectId
23
- from typing import Union, Type, Optional
24
- from tqdm.auto import tqdm
25
- from hashlib import md5
26
18
  from pathlib import Path
27
- from copy import deepcopy
28
- from filetype import guess
29
- from flatten_dict import flatten, unflatten
30
- from base64 import b64encode, b64decode, urlsafe_b64encode
19
+ from tempfile import gettempdir
20
+ from typing import Type
31
21
  from urllib.parse import urlparse
32
- from pyisemail import is_email
33
- from collections import defaultdict
34
- from pyisemail.diagnosis import BaseDiagnosis
35
- from swagger_spec_validator.common import SwaggerValidationError
36
- from jsonschema.exceptions import ValidationError
37
- from bravado_core.formatter import SwaggerFormat
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+ import plotly.io as pio
26
+ import requests
27
+ import ujson
28
+ from boltons.iterutils import remap
38
29
  from bravado.client import SwaggerClient
30
+ from bravado.config import bravado_config_from_config_dict
31
+ from bravado.exception import HTTPNotFound
39
32
  from bravado.requests_client import RequestsClient
40
33
  from bravado.swagger_model import Loader
41
- from bravado.config import bravado_config_from_config_dict
42
- from bravado_core.spec import Spec, build_api_serving_url, _identity
34
+ from bravado_core.formatter import SwaggerFormat
43
35
  from bravado_core.model import model_discovery
44
36
  from bravado_core.resource import build_resources
45
- from bravado.exception import HTTPNotFound
37
+ from bravado_core.spec import Spec, _identity, build_api_serving_url
46
38
  from bravado_core.validate import validate_object
47
- from json2html import Json2Html
48
- from IPython.display import display, HTML, Image, FileLink
49
- from boltons.iterutils import remap
50
- from pymatgen.core import Structure as PmgStructure
51
- from concurrent.futures import as_completed
52
- from requests_futures.sessions import FuturesSession
53
- from urllib3.util.retry import Retry
39
+ from bson.objectid import ObjectId
40
+ from cachetools import LRUCache, cached
41
+ from cachetools.keys import hashkey
42
+ from filetype import guess
54
43
  from filetype.types.archive import Gz
55
- from filetype.types.image import Jpeg, Png, Gif, Tiff
44
+ from filetype.types.image import Gif, Jpeg, Png, Tiff
45
+ from flatten_dict import flatten, unflatten
46
+ from IPython.display import HTML, FileLink, Image, display
47
+ from json2html import Json2Html
48
+ from jsonschema.exceptions import ValidationError
56
49
  from pint import UnitRegistry
57
50
  from pint.errors import DimensionalityError
58
- from tempfile import gettempdir
59
51
  from plotly.express._chart_types import line as line_chart
60
- from cachetools import cached, LRUCache
61
- from cachetools.keys import hashkey
62
- from pymatgen.core import SETTINGS
52
+ from pyisemail import is_email
53
+ from pyisemail.diagnosis import BaseDiagnosis
54
+ from pymatgen.core import Structure as PmgStructure
55
+ from requests.exceptions import RequestException
56
+ from requests_futures.sessions import FuturesSession
57
+ from swagger_spec_validator.common import SwaggerValidationError
58
+ from tqdm.auto import tqdm
59
+ from urllib3.util.retry import Retry
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
962
+ raise MPContribsClientError(str(ex))
958
963
 
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
964
-
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)
@@ -1124,10 +1128,10 @@ class Client(SwaggerClient):
1124
1128
  Returns:
1125
1129
  List of projects
1126
1130
  """
1127
- query = query or {}
1131
+ q = deepcopy(query) or {}
1128
1132
 
1129
- if self.project or "name" in query:
1130
- return [self.get_project(name=query.get("name"), fields=fields)]
1133
+ if self.project or "name" in q:
1134
+ return [self.get_project(name=q.get("name"), fields=fields)]
1131
1135
 
1132
1136
  if term:
1133
1137
 
@@ -1144,38 +1148,78 @@ class Client(SwaggerClient):
1144
1148
  responses = _run_futures(
1145
1149
  [search_future(term)], timeout=timeout, disable=True
1146
1150
  )
1147
- query["name__in"] = responses["search"].get("result", [])
1151
+ q["name__in"] = responses["search"].get("result", [])
1148
1152
 
1149
1153
  if fields:
1150
- query["_fields"] = fields
1154
+ q["_fields"] = fields
1151
1155
  if sort:
1152
- query["_sort"] = sort
1156
+ q["_sort"] = sort
1153
1157
 
1154
- ret = self.projects.queryProjects(**query).result() # first page
1158
+ ret = self.projects.queryProjects(**q).result() # first page
1159
+ """
1160
+ 'ret' type:
1161
+ {
1162
+ "data": [
1163
+ ...
1164
+ ],
1165
+ "has_more": <bool>,
1166
+ "total_count": <int>,
1167
+ "total_pages": <int>
1168
+ }
1169
+ """
1155
1170
  total_count, total_pages = ret["total_count"], ret["total_pages"]
1156
1171
 
1157
1172
  if total_pages < 2:
1158
1173
  return ret["data"]
1159
1174
 
1160
- for field in ["name__in", "_fields"]:
1161
- if field in query:
1162
- query[field] = ",".join(query[field])
1175
+ q.update(
1176
+ {
1177
+ field: ",".join(q[field])
1178
+ for field in ["name__in", "_fields"]
1179
+ if field in q
1180
+ }
1181
+ )
1163
1182
 
1164
1183
  queries = []
1165
1184
 
1166
1185
  for page in range(2, total_pages + 1):
1167
- queries.append(deepcopy(query))
1186
+ queries.append(deepcopy(q))
1168
1187
  queries[-1]["page"] = page
1169
1188
 
1170
1189
  futures = [
1171
- self._get_future(i, q, rel_url="projects") for i, q in enumerate(queries)
1190
+ self._get_future(i, _q, rel_url="projects") for i, _q in enumerate(queries)
1172
1191
  ]
1173
1192
  responses = _run_futures(futures, total=total_count, timeout=timeout)
1193
+ """
1194
+ 'responses' type:
1195
+ {
1196
+ "0": {
1197
+ "result": {
1198
+ "data": [
1199
+ ...
1200
+ ],
1201
+ "has_more": <bool>,
1202
+ "total_count": <int>,
1203
+ "total_pages": <int>
1204
+ },
1205
+ "count": <int>
1206
+ },
1207
+ "1": ...
1208
+ }
1209
+ """
1174
1210
 
1175
- for resp in responses.values():
1176
- ret["data"] += resp["result"]["data"]
1177
-
1178
- return ret["data"]
1211
+ return list(
1212
+ itertools.chain.from_iterable(
1213
+ [
1214
+ ret["data"],
1215
+ itertools.chain.from_iterable(
1216
+ # did not hit early return, guaranteed
1217
+ # to have additional pages w/ data
1218
+ map(lambda x: x["result"]["data"], iter(responses.values()))
1219
+ ),
1220
+ ]
1221
+ )
1222
+ )
1179
1223
 
1180
1224
  def create_project(
1181
1225
  self, name: str, title: str, authors: str, description: str, url: str
@@ -1210,7 +1254,7 @@ class Client(SwaggerClient):
1210
1254
  else:
1211
1255
  raise MPContribsClientError(resp)
1212
1256
 
1213
- def update_project(self, update: dict, name: Optional[str] = None):
1257
+ def update_project(self, update: dict, name: str | None = None):
1214
1258
  """Update project info
1215
1259
 
1216
1260
  Args:
@@ -1268,15 +1312,12 @@ class Client(SwaggerClient):
1268
1312
  logger.warning("nothing to update")
1269
1313
  return
1270
1314
 
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)
1315
+ self._is_valid_payload("Project", payload)
1316
+ resp = self.projects.updateProjectByName(pk=name, project=payload).result()
1317
+ if not resp.get("count", 0):
1318
+ raise MPContribsClientError(resp)
1278
1319
 
1279
- def delete_project(self, name: Optional[str] = None):
1320
+ def delete_project(self, name: str | None = None):
1280
1321
  """Delete a project
1281
1322
 
1282
1323
  Args:
@@ -1295,7 +1336,7 @@ class Client(SwaggerClient):
1295
1336
  if resp and "error" in resp:
1296
1337
  raise MPContribsClientError(resp["error"])
1297
1338
 
1298
- def get_contribution(self, cid: str, fields: Optional[list] = None) -> Dict:
1339
+ def get_contribution(self, cid: str, fields: list | None = None) -> Dict:
1299
1340
  """Retrieve a contribution
1300
1341
 
1301
1342
  Args:
@@ -1408,7 +1449,7 @@ class Client(SwaggerClient):
1408
1449
  )
1409
1450
 
1410
1451
  def init_columns(
1411
- self, columns: Optional[dict] = None, name: Optional[str] = None
1452
+ self, columns: dict | None = None, name: str | None = None
1412
1453
  ) -> dict:
1413
1454
  """initialize columns for a project to set their order and desired units
1414
1455
 
@@ -1532,20 +1573,23 @@ class Client(SwaggerClient):
1532
1573
  new_unit = new_column.get("unit", "NaN")
1533
1574
  existing_unit = existing_column.get("unit")
1534
1575
  if existing_unit != new_unit:
1535
- conv_args = []
1536
- for u in [existing_unit, new_unit]:
1576
+ if existing_unit == "NaN" and new_unit == "":
1577
+ factor = 1
1578
+ else:
1579
+ conv_args = []
1580
+ for u in [existing_unit, new_unit]:
1581
+ try:
1582
+ conv_args.append(ureg.Unit(u))
1583
+ except ValueError:
1584
+ raise MPContribsClientError(
1585
+ f"Can't convert {existing_unit} to {new_unit} for {path}"
1586
+ )
1537
1587
  try:
1538
- conv_args.append(ureg.Unit(u))
1539
- except ValueError:
1588
+ factor = ureg.convert(1, *conv_args)
1589
+ except DimensionalityError:
1540
1590
  raise MPContribsClientError(
1541
1591
  f"Can't convert {existing_unit} to {new_unit} for {path}"
1542
1592
  )
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
1593
 
1550
1594
  if not isclose(factor, 1):
1551
1595
  logger.info(
@@ -1560,13 +1604,11 @@ class Client(SwaggerClient):
1560
1604
  new_columns.append(new_column)
1561
1605
 
1562
1606
  payload = {"columns": new_columns}
1563
- valid, error = self._is_valid_payload("Project", payload)
1564
- if not valid:
1565
- raise MPContribsClientError(error)
1607
+ self._is_valid_payload("Project", payload)
1566
1608
 
1567
1609
  return self.projects.updateProjectByName(pk=name, project=payload).result()
1568
1610
 
1569
- def delete_contributions(self, query: Optional[dict] = None, timeout: int = -1):
1611
+ def delete_contributions(self, query: dict | None = None, timeout: int = -1):
1570
1612
  """Remove all contributions for a query
1571
1613
 
1572
1614
  Args:
@@ -1579,25 +1621,25 @@ class Client(SwaggerClient):
1579
1621
  )
1580
1622
 
1581
1623
  tic = time.perf_counter()
1582
- query = query or {}
1624
+ q = deepcopy(query) or {}
1583
1625
 
1584
1626
  if self.project:
1585
- query["project"] = self.project
1627
+ q["project"] = self.project
1586
1628
 
1587
- name = query["project"]
1588
- cids = list(self.get_all_ids(query).get(name, {}).get("ids", set()))
1629
+ name = q["project"]
1630
+ cids = list(self.get_all_ids(q).get(name, {}).get("ids", set()))
1589
1631
 
1590
1632
  if not cids:
1591
1633
  logger.info(f"There aren't any contributions to delete for {name}")
1592
1634
  return
1593
1635
 
1594
1636
  total = len(cids)
1595
- query = {"id__in": cids}
1596
- _, total_pages = self.get_totals(query=query)
1597
- queries = self._split_query(query, op="delete", pages=total_pages)
1598
- futures = [self._get_future(i, q, op="delete") for i, q in enumerate(queries)]
1637
+ id_query = {"id__in": cids}
1638
+ _, total_pages = self.get_totals(query=id_query)
1639
+ queries = self._split_query(id_query, op="delete", pages=total_pages)
1640
+ futures = [self._get_future(i, _q, op="delete") for i, _q in enumerate(queries)]
1599
1641
  _run_futures(futures, total=total, timeout=timeout)
1600
- left, _ = self.get_totals(query=query)
1642
+ left, _ = self.get_totals(query=id_query)
1601
1643
  deleted = total - left
1602
1644
  self.init_columns(name=name)
1603
1645
  self._reinit()
@@ -1612,7 +1654,7 @@ class Client(SwaggerClient):
1612
1654
 
1613
1655
  def get_totals(
1614
1656
  self,
1615
- query: Optional[dict] = None,
1657
+ query: dict | None = None,
1616
1658
  timeout: int = -1,
1617
1659
  resource: str = "contributions",
1618
1660
  op: str = "query",
@@ -1633,31 +1675,31 @@ class Client(SwaggerClient):
1633
1675
  if op not in ops:
1634
1676
  raise MPContribsClientError(f"`op` has to be one of {ops}")
1635
1677
 
1636
- query = query or {}
1637
- if self.project and "project" not in query:
1638
- query["project"] = self.project
1678
+ q = deepcopy(query) or {}
1679
+ if self.project and "project" not in q:
1680
+ q["project"] = self.project
1639
1681
 
1640
1682
  skip_keys = {"per_page", "_fields", "format", "_sort"}
1641
- query = {k: v for k, v in query.items() if k not in skip_keys}
1642
- query["_fields"] = [] # only need totals -> explicitly request no fields
1643
- queries = self._split_query(query, resource=resource, op=op) # don't paginate
1644
- result = {"total_count": 0, "total_pages": 0}
1683
+ q = {k: v for k, v in q.items() if k not in skip_keys}
1684
+ q["_fields"] = [] # only need totals -> explicitly request no fields
1685
+ queries = self._split_query(q, resource=resource, op=op) # don't paginate
1645
1686
  futures = [
1646
- self._get_future(i, q, rel_url=resource) for i, q in enumerate(queries)
1687
+ self._get_future(i, _q, rel_url=resource) for i, _q in enumerate(queries)
1647
1688
  ]
1648
1689
  responses = _run_futures(futures, timeout=timeout, desc="Totals")
1649
1690
 
1650
- for resp in responses.values():
1651
- for k in result:
1652
- result[k] += resp.get("result", {}).get(k, 0)
1691
+ result = {
1692
+ k: sum(resp.get("result", {}).get(k, 0) for resp in responses.values())
1693
+ for k in ("total_count", "total_pages")
1694
+ }
1653
1695
 
1654
1696
  return result["total_count"], result["total_pages"]
1655
1697
 
1656
- def count(self, query: Optional[dict] = None) -> int:
1698
+ def count(self, query: dict | None = None) -> int:
1657
1699
  """shortcut for get_totals()"""
1658
1700
  return self.get_totals(query=query)[0]
1659
1701
 
1660
- def get_unique_identifiers_flags(self, query: Optional[dict] = None) -> dict:
1702
+ def get_unique_identifiers_flags(self, query: dict | None = None) -> dict:
1661
1703
  """Retrieve values for `unique_identifiers` flags.
1662
1704
 
1663
1705
  See `client.available_query_params(resource="projects")` for available query parameters.
@@ -1677,10 +1719,10 @@ class Client(SwaggerClient):
1677
1719
 
1678
1720
  def get_all_ids(
1679
1721
  self,
1680
- query: Optional[dict] = None,
1681
- include: Optional[list[str]] = None,
1722
+ query: dict | None = None,
1723
+ include: list[str] | None = None,
1682
1724
  timeout: int = -1,
1683
- data_id_fields: Optional[dict] = None,
1725
+ data_id_fields: dict | None = None,
1684
1726
  fmt: str = "sets",
1685
1727
  op: str = "query",
1686
1728
  ) -> dict:
@@ -1731,7 +1773,7 @@ class Client(SwaggerClient):
1731
1773
  }, ...}
1732
1774
  """
1733
1775
  include = include or []
1734
- components = set(x for x in include if x in COMPONENTS)
1776
+ components = {x for x in include if x in COMPONENTS}
1735
1777
  if include and not components:
1736
1778
  raise MPContribsClientError(f"`include` must be subset of {COMPONENTS}!")
1737
1779
 
@@ -1745,16 +1787,20 @@ class Client(SwaggerClient):
1745
1787
 
1746
1788
  unique_identifiers = self.get_unique_identifiers_flags()
1747
1789
  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
1790
+ data_id_fields.update(
1791
+ {
1792
+ k: v
1793
+ for k, v in data_id_fields.items()
1794
+ if k in unique_identifiers and isinstance(v, str)
1795
+ }
1796
+ )
1751
1797
 
1752
1798
  ret = {}
1753
- query = query or {}
1754
- if self.project and "project" not in query:
1755
- query["project"] = self.project
1799
+ q = deepcopy(query) or {}
1800
+ if self.project and "project" not in q:
1801
+ q["project"] = self.project
1756
1802
 
1757
- [query.pop(k, None) for k in ["page", "per_page", "_fields"]]
1803
+ [q.pop(k, None) for k in ["page", "per_page", "_fields"]]
1758
1804
  id_fields = {"project", "id", "identifier"}
1759
1805
 
1760
1806
  if data_id_fields:
@@ -1762,10 +1808,10 @@ class Client(SwaggerClient):
1762
1808
  f"data.{data_id_field}" for data_id_field in data_id_fields.values()
1763
1809
  )
1764
1810
 
1765
- query["_fields"] = list(id_fields | components)
1766
- _, total_pages = self.get_totals(query=query, timeout=timeout)
1767
- queries = self._split_query(query, op=op, pages=total_pages)
1768
- futures = [self._get_future(i, q) for i, q in enumerate(queries)]
1811
+ q["_fields"] = list(id_fields | components)
1812
+ _, total_pages = self.get_totals(query=q, timeout=timeout)
1813
+ queries = self._split_query(q, op=op, pages=total_pages)
1814
+ futures = [self._get_future(i, _q) for i, _q in enumerate(queries)]
1769
1815
  responses = _run_futures(futures, timeout=timeout, desc="Identifiers")
1770
1816
 
1771
1817
  for resp in responses.values():
@@ -1810,34 +1856,40 @@ class Client(SwaggerClient):
1810
1856
  if data_id_field and data_id_field_val:
1811
1857
  ret[project][identifier][data_id_field] = data_id_field_val
1812
1858
 
1813
- for component in components:
1814
- if component in contrib:
1815
- ret[project][identifier][component] = {
1859
+ ret[project][identifier].update(
1860
+ {
1861
+ component: {
1816
1862
  d["name"]: {"id": d["id"], "md5": d["md5"]}
1817
1863
  for d in contrib[component]
1818
1864
  }
1865
+ for component in components
1866
+ if component in contrib
1867
+ }
1868
+ )
1819
1869
 
1820
1870
  elif data_id_field and data_id_field_val:
1821
1871
  ret[project][identifier] = {
1822
1872
  data_id_field_val: {"id": contrib["id"]}
1823
1873
  }
1824
1874
 
1825
- for component in components:
1826
- if component in contrib:
1827
- ret[project][identifier][data_id_field_val][
1828
- component
1829
- ] = {
1875
+ ret[project][identifier][data_id_field_val].update(
1876
+ {
1877
+ component: {
1830
1878
  d["name"]: {"id": d["id"], "md5": d["md5"]}
1831
1879
  for d in contrib[component]
1832
1880
  }
1881
+ for component in components
1882
+ if component in contrib
1883
+ }
1884
+ )
1833
1885
 
1834
1886
  return ret
1835
1887
 
1836
1888
  def query_contributions(
1837
1889
  self,
1838
- query: Optional[dict] = None,
1839
- fields: Optional[list] = None,
1840
- sort: Optional[str] = None,
1890
+ query: dict | None = None,
1891
+ fields: list | None = None,
1892
+ sort: str | None = None,
1841
1893
  paginate: bool = False,
1842
1894
  timeout: int = -1,
1843
1895
  ) -> dict:
@@ -1855,18 +1907,17 @@ class Client(SwaggerClient):
1855
1907
  Returns:
1856
1908
  List of contributions
1857
1909
  """
1858
- query = query or {}
1910
+ q: dict = deepcopy(query) or {}
1859
1911
 
1860
- if self.project and "project" not in query:
1861
- query["project"] = self.project
1912
+ if self.project and "project" not in q:
1913
+ q["project"] = self.project
1862
1914
 
1863
1915
  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)
1916
+ cids = [
1917
+ idx
1918
+ for v in self.get_all_ids(q).values()
1919
+ for idx in (v.get("ids") or [])
1920
+ ]
1870
1921
 
1871
1922
  if not cids:
1872
1923
  raise MPContribsClientError("No contributions match the query.")
@@ -1875,23 +1926,32 @@ class Client(SwaggerClient):
1875
1926
  cids_query = {"id__in": cids, "_fields": fields, "_sort": sort}
1876
1927
  _, total_pages = self.get_totals(query=cids_query)
1877
1928
  queries = self._split_query(cids_query, pages=total_pages)
1878
- futures = [self._get_future(i, q) for i, q in enumerate(queries)]
1879
- responses = _run_futures(futures, total=total, timeout=timeout)
1880
- ret = {"total_count": 0, "data": []}
1881
-
1882
- for resp in responses.values():
1883
- result = resp["result"]
1884
- ret["data"].extend(result["data"])
1885
- ret["total_count"] += result["total_count"]
1929
+ futures = [self._get_future(i, _q) for i, _q in enumerate(queries)]
1930
+ responses = [
1931
+ resp
1932
+ for resp in _run_futures(futures, total=total, timeout=timeout).values()
1933
+ if resp.get("result")
1934
+ ]
1935
+ ret = {
1936
+ "total_count": sum(
1937
+ resp["result"].get("total_count", 0) for resp in responses
1938
+ ),
1939
+ "data": list(
1940
+ itertools.chain.from_iterable(
1941
+ [resp["result"].get("data", []) for resp in responses]
1942
+ )
1943
+ ),
1944
+ }
1945
+
1886
1946
  else:
1887
1947
  ret = self.contributions.queryContributions(
1888
- _fields=fields, _sort=sort, **query
1948
+ _fields=fields, _sort=sort, **q
1889
1949
  ).result()
1890
1950
 
1891
1951
  return ret
1892
1952
 
1893
1953
  def update_contributions(
1894
- self, data: dict, query: Optional[dict] = None, timeout: int = -1
1954
+ self, data: dict, query: dict | None = None, timeout: int = -1
1895
1955
  ) -> dict:
1896
1956
  """Apply the same update to all contributions in a project (matching query)
1897
1957
 
@@ -1906,31 +1966,27 @@ class Client(SwaggerClient):
1906
1966
  raise MPContribsClientError("Nothing to update.")
1907
1967
 
1908
1968
  tic = time.perf_counter()
1909
- valid, error = self._is_valid_payload("Contribution", data)
1910
- if not valid:
1911
- raise MPContribsClientError(error)
1969
+ self._is_valid_payload("Contribution", data)
1912
1970
 
1913
1971
  if "data" in data:
1914
- serializable, error = self._is_serializable_dict(data["data"])
1915
- if not serializable:
1916
- raise MPContribsClientError(error)
1972
+ self._is_serializable_dict(data["data"])
1917
1973
 
1918
- query = query or {}
1974
+ q = deepcopy(query) or {}
1919
1975
 
1920
1976
  if self.project:
1921
- if "project" in query and self.project != query["project"]:
1977
+ if "project" in q and self.project != q["project"]:
1922
1978
  raise MPContribsClientError(
1923
1979
  f"client initialized with different project {self.project}!"
1924
1980
  )
1925
- query["project"] = self.project
1981
+ q["project"] = self.project
1926
1982
  else:
1927
- if not query or "project" not in query:
1983
+ if not q or "project" not in q:
1928
1984
  raise MPContribsClientError(
1929
1985
  "initialize client with project, or include project in query!"
1930
1986
  )
1931
1987
 
1932
- name = query["project"]
1933
- cids = list(self.get_all_ids(query).get(name, {}).get("ids", set()))
1988
+ name = q["project"]
1989
+ cids = list(self.get_all_ids(q).get(name, {}).get("ids", set()))
1934
1990
 
1935
1991
  if not cids:
1936
1992
  raise MPContribsClientError(
@@ -1939,22 +1995,22 @@ class Client(SwaggerClient):
1939
1995
 
1940
1996
  # get current list of data columns to decide if swagger reload is needed
1941
1997
  resp = self.projects.getProjectByName(pk=name, _fields=["columns"]).result()
1942
- old_paths = set(c["path"] for c in resp["columns"])
1998
+ old_paths = {c["path"] for c in resp["columns"]}
1943
1999
 
1944
2000
  total = len(cids)
1945
2001
  cids_query = {"id__in": cids}
1946
2002
  _, total_pages = self.get_totals(query=cids_query)
1947
2003
  queries = self._split_query(cids_query, op="update", pages=total_pages)
1948
2004
  futures = [
1949
- self._get_future(i, q, op="update", data=data)
1950
- for i, q in enumerate(queries)
2005
+ self._get_future(i, _q, op="update", data=data)
2006
+ for i, _q in enumerate(queries)
1951
2007
  ]
1952
2008
  responses = _run_futures(futures, total=total, timeout=timeout)
1953
2009
  updated = sum(resp["count"] for _, resp in responses.items())
1954
2010
 
1955
2011
  if updated:
1956
2012
  resp = self.projects.getProjectByName(pk=name, _fields=["columns"]).result()
1957
- new_paths = set(c["path"] for c in resp["columns"])
2013
+ new_paths = {c["path"] for c in resp["columns"]}
1958
2014
 
1959
2015
  if new_paths != old_paths:
1960
2016
  self.init_columns(name=name)
@@ -1964,7 +2020,7 @@ class Client(SwaggerClient):
1964
2020
  return {"updated": updated, "total": total, "seconds_elapsed": toc - tic}
1965
2021
 
1966
2022
  def make_public(
1967
- self, query: Optional[dict] = None, recursive: bool = False, timeout: int = -1
2023
+ self, query: dict | None = None, recursive: bool = False, timeout: int = -1
1968
2024
  ) -> dict:
1969
2025
  """Publish a project and optionally its contributions
1970
2026
 
@@ -1977,7 +2033,7 @@ class Client(SwaggerClient):
1977
2033
  )
1978
2034
 
1979
2035
  def make_private(
1980
- self, query: Optional[dict] = None, recursive: bool = False, timeout: int = -1
2036
+ self, query: dict | None = None, recursive: bool = False, timeout: int = -1
1981
2037
  ) -> dict:
1982
2038
  """Make a project and optionally its contributions private
1983
2039
 
@@ -1992,7 +2048,7 @@ class Client(SwaggerClient):
1992
2048
  def _set_is_public(
1993
2049
  self,
1994
2050
  is_public: bool,
1995
- query: Optional[dict] = None,
2051
+ query: dict | None = None,
1996
2052
  recursive: bool = False,
1997
2053
  timeout: int = -1,
1998
2054
  ) -> dict:
@@ -2009,23 +2065,23 @@ class Client(SwaggerClient):
2009
2065
  "initialize client with project, or include project in query!"
2010
2066
  )
2011
2067
 
2012
- query = query or {}
2068
+ q = deepcopy(query) or {}
2013
2069
 
2014
2070
  if self.project:
2015
- query["project"] = self.project
2071
+ q["project"] = self.project
2016
2072
 
2017
2073
  try:
2018
2074
  resp = self.projects.getProjectByName(
2019
- pk=query["project"], _fields=["is_public", "is_approved"]
2075
+ pk=q["project"], _fields=["is_public", "is_approved"]
2020
2076
  ).result()
2021
2077
  except HTTPNotFound:
2022
2078
  raise MPContribsClientError(
2023
- f"project `{query['project']}` not found or access denied!"
2079
+ f"project `{q['project']}` not found or access denied!"
2024
2080
  )
2025
2081
 
2026
2082
  if not recursive and resp["is_public"] == is_public:
2027
2083
  return {
2028
- "warning": f"`is_public` already set to {is_public} for `{query['project']}`."
2084
+ "warning": f"`is_public` already set to {is_public} for `{q['project']}`."
2029
2085
  }
2030
2086
 
2031
2087
  ret = {}
@@ -2033,19 +2089,19 @@ class Client(SwaggerClient):
2033
2089
  if resp["is_public"] != is_public:
2034
2090
  if is_public and not resp["is_approved"]:
2035
2091
  raise MPContribsClientError(
2036
- f"project `{query['project']}` is not approved yet!"
2092
+ f"project `{q['project']}` is not approved yet!"
2037
2093
  )
2038
2094
 
2039
2095
  resp = self.projects.updateProjectByName(
2040
- pk=query["project"], project={"is_public": is_public}
2096
+ pk=q["project"], project={"is_public": is_public}
2041
2097
  ).result()
2042
2098
  ret["published"] = resp["count"] == 1
2043
2099
 
2044
2100
  if recursive:
2045
- query = query or {}
2046
- query["is_public"] = not is_public
2101
+ q = deepcopy(query) or {}
2102
+ q["is_public"] = not is_public
2047
2103
  ret["contributions"] = self.update_contributions(
2048
- {"is_public": is_public}, query=query, timeout=timeout
2104
+ {"is_public": is_public}, query=q, timeout=timeout
2049
2105
  )
2050
2106
 
2051
2107
  return ret
@@ -2124,9 +2180,13 @@ class Client(SwaggerClient):
2124
2180
  resp = self.get_all_ids(dict(id__in=collect_ids), timeout=timeout)
2125
2181
  project_names |= set(resp.keys())
2126
2182
 
2127
- for project_name, values in resp.items():
2128
- for cid in values["ids"]:
2129
- id2project[cid] = project_name
2183
+ id2project.update(
2184
+ {
2185
+ cid: project_name
2186
+ for project_name, values in resp.items()
2187
+ for cid in values["ids"]
2188
+ }
2189
+ )
2130
2190
 
2131
2191
  existing = defaultdict(dict)
2132
2192
  unique_identifiers = defaultdict(dict)
@@ -2160,9 +2220,7 @@ class Client(SwaggerClient):
2160
2220
  for contrib in tqdm(contributions, desc="Prepare"):
2161
2221
  if "data" in contrib:
2162
2222
  contrib["data"] = unflatten(contrib["data"], splitter="dot")
2163
- serializable, error = self._is_serializable_dict(contrib["data"])
2164
- if not serializable:
2165
- raise MPContribsClientError(error)
2223
+ self._is_serializable_dict(contrib["data"])
2166
2224
 
2167
2225
  update = "id" in contrib
2168
2226
  project_name = id2project[contrib["id"]] if update else contrib["project"]
@@ -2282,13 +2340,7 @@ class Client(SwaggerClient):
2282
2340
  digests[project_name][component].add(digest)
2283
2341
  contribs[project_name][-1][component].append(dct)
2284
2342
 
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
- )
2343
+ self._is_valid_payload("Contribution", contribs[project_name][-1])
2292
2344
 
2293
2345
  # submit contributions
2294
2346
  if contribs:
@@ -2425,10 +2477,10 @@ class Client(SwaggerClient):
2425
2477
 
2426
2478
  def download_contributions(
2427
2479
  self,
2428
- query: Optional[dict] = None,
2429
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2480
+ query: dict | None = None,
2481
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2430
2482
  overwrite: bool = False,
2431
- include: Optional[list[str]] = None,
2483
+ include: list[str] | None = None,
2432
2484
  timeout: int = -1,
2433
2485
  ) -> list:
2434
2486
  """Download a list of contributions as .json.gz file(s)
@@ -2444,16 +2496,16 @@ class Client(SwaggerClient):
2444
2496
  Number of new downloads written to disk.
2445
2497
  """
2446
2498
  start = time.perf_counter()
2447
- query = query or {}
2499
+ q = deepcopy(query) or {}
2448
2500
  include = include or []
2449
2501
  outdir = Path(outdir) or Path(".")
2450
2502
  outdir.mkdir(parents=True, exist_ok=True)
2451
- components = set(x for x in include if x in COMPONENTS)
2503
+ components = {x for x in include if x in COMPONENTS}
2452
2504
  if include and not components:
2453
2505
  raise MPContribsClientError(f"`include` must be subset of {COMPONENTS}!")
2454
2506
 
2455
- all_ids = self.get_all_ids(query, include=list(components), timeout=timeout)
2456
- fmt = query.get("format", "json")
2507
+ all_ids = self.get_all_ids(q, include=list(components), timeout=timeout)
2508
+ fmt = q.get("format", "json")
2457
2509
  contributions, components_loaded = [], defaultdict(dict)
2458
2510
 
2459
2511
  for name, values in all_ids.items():
@@ -2527,7 +2579,7 @@ class Client(SwaggerClient):
2527
2579
  def download_structures(
2528
2580
  self,
2529
2581
  ids: list[str],
2530
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2582
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2531
2583
  overwrite: bool = False,
2532
2584
  timeout: int = -1,
2533
2585
  fmt: str = "json",
@@ -2556,7 +2608,7 @@ class Client(SwaggerClient):
2556
2608
  def download_tables(
2557
2609
  self,
2558
2610
  ids: list[str],
2559
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2611
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2560
2612
  overwrite: bool = False,
2561
2613
  timeout: int = -1,
2562
2614
  fmt: str = "json",
@@ -2585,7 +2637,7 @@ class Client(SwaggerClient):
2585
2637
  def download_attachments(
2586
2638
  self,
2587
2639
  ids: list[str],
2588
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2640
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2589
2641
  overwrite: bool = False,
2590
2642
  timeout: int = -1,
2591
2643
  fmt: str = "json",
@@ -2615,7 +2667,7 @@ class Client(SwaggerClient):
2615
2667
  self,
2616
2668
  resource: str,
2617
2669
  ids: list[str],
2618
- outdir: Union[str, Path] = DEFAULT_DOWNLOAD_DIR,
2670
+ outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
2619
2671
  overwrite: bool = False,
2620
2672
  timeout: int = -1,
2621
2673
  fmt: str = "json",
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpcontribs-client
3
+ Version: 5.10.5rc1
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=AwKIDzTg30ZMW97QxQZ1OCwJAFrXT3XMdRktgL356Jc,98456
2
+ mpcontribs_client-5.10.5rc1.dist-info/licenses/LICENSE,sha256=5tG0Niaqw2hnuyZZYkRXLSnfVrZA47COwduU_6caPLM,1074
3
+ mpcontribs_client-5.10.5rc1.dist-info/METADATA,sha256=APCeqCMlQLDEAEBwUYk6CFqHdawo-JDmCgz3VA9R8bg,1289
4
+ mpcontribs_client-5.10.5rc1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
5
+ mpcontribs_client-5.10.5rc1.dist-info/top_level.txt,sha256=t8R5L_Dg9oDQMh2gyRFdZGnrzZsr7OjCBTrhTcmimC8,11
6
+ mpcontribs_client-5.10.5rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
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,,