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.
- mpcontribs/client/__init__.py +326 -274
- mpcontribs_client-5.10.5rc1.dist-info/METADATA +39 -0
- mpcontribs_client-5.10.5rc1.dist-info/RECORD +6 -0
- {mpcontribs_client-5.10.4.dist-info → mpcontribs_client-5.10.5rc1.dist-info}/WHEEL +1 -1
- mpcontribs_client-5.10.4.dist-info/METADATA +0 -88
- mpcontribs_client-5.10.4.dist-info/RECORD +0 -6
- {mpcontribs_client-5.10.4.dist-info → mpcontribs_client-5.10.5rc1.dist-info}/licenses/LICENSE +0 -0
- {mpcontribs_client-5.10.4.dist-info → mpcontribs_client-5.10.5rc1.dist-info}/top_level.txt +0 -0
mpcontribs/client/__init__.py
CHANGED
|
@@ -1,65 +1,68 @@
|
|
|
1
|
-
|
|
1
|
+
import functools
|
|
2
|
+
import gzip
|
|
3
|
+
import importlib.metadata
|
|
2
4
|
import io
|
|
3
|
-
import
|
|
5
|
+
import itertools
|
|
6
|
+
import logging
|
|
4
7
|
import os
|
|
5
|
-
import
|
|
8
|
+
import sys
|
|
6
9
|
import time
|
|
7
|
-
import gzip
|
|
8
10
|
import warnings
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
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
|
|
28
|
-
from
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
37
|
+
from bravado_core.spec import Spec, _identity, build_api_serving_url
|
|
46
38
|
from bravado_core.validate import validate_object
|
|
47
|
-
from
|
|
48
|
-
from
|
|
49
|
-
from
|
|
50
|
-
from
|
|
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,
|
|
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
|
|
61
|
-
from
|
|
62
|
-
from pymatgen.core import
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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(
|
|
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:
|
|
874
|
-
headers:
|
|
875
|
-
host:
|
|
876
|
-
project:
|
|
877
|
-
session:
|
|
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
|
-
|
|
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 |=
|
|
940
|
-
members |=
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
return True, None
|
|
962
|
+
raise MPContribsClientError(str(ex))
|
|
958
963
|
|
|
959
|
-
def _is_serializable_dict(self, dct):
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
1107
|
-
term:
|
|
1108
|
-
fields:
|
|
1109
|
-
sort:
|
|
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
|
-
|
|
1131
|
+
q = deepcopy(query) or {}
|
|
1128
1132
|
|
|
1129
|
-
if self.project or "name" in
|
|
1130
|
-
return [self.get_project(name=
|
|
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
|
-
|
|
1151
|
+
q["name__in"] = responses["search"].get("result", [])
|
|
1148
1152
|
|
|
1149
1153
|
if fields:
|
|
1150
|
-
|
|
1154
|
+
q["_fields"] = fields
|
|
1151
1155
|
if sort:
|
|
1152
|
-
|
|
1156
|
+
q["_sort"] = sort
|
|
1153
1157
|
|
|
1154
|
-
ret = self.projects.queryProjects(**
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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(
|
|
1186
|
+
queries.append(deepcopy(q))
|
|
1168
1187
|
queries[-1]["page"] = page
|
|
1169
1188
|
|
|
1170
1189
|
futures = [
|
|
1171
|
-
self._get_future(i,
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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:
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
1539
|
-
except
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1624
|
+
q = deepcopy(query) or {}
|
|
1583
1625
|
|
|
1584
1626
|
if self.project:
|
|
1585
|
-
|
|
1627
|
+
q["project"] = self.project
|
|
1586
1628
|
|
|
1587
|
-
name =
|
|
1588
|
-
cids = list(self.get_all_ids(
|
|
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
|
-
|
|
1596
|
-
_, total_pages = self.get_totals(query=
|
|
1597
|
-
queries = self._split_query(
|
|
1598
|
-
futures = [self._get_future(i,
|
|
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=
|
|
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:
|
|
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
|
-
|
|
1637
|
-
if self.project and "project" not in
|
|
1638
|
-
|
|
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
|
-
|
|
1642
|
-
|
|
1643
|
-
queries = self._split_query(
|
|
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,
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
1681
|
-
include:
|
|
1722
|
+
query: dict | None = None,
|
|
1723
|
+
include: list[str] | None = None,
|
|
1682
1724
|
timeout: int = -1,
|
|
1683
|
-
data_id_fields:
|
|
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 =
|
|
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
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
1754
|
-
if self.project and "project" not in
|
|
1755
|
-
|
|
1799
|
+
q = deepcopy(query) or {}
|
|
1800
|
+
if self.project and "project" not in q:
|
|
1801
|
+
q["project"] = self.project
|
|
1756
1802
|
|
|
1757
|
-
[
|
|
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
|
-
|
|
1766
|
-
_, total_pages = self.get_totals(query=
|
|
1767
|
-
queries = self._split_query(
|
|
1768
|
-
futures = [self._get_future(i,
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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:
|
|
1839
|
-
fields:
|
|
1840
|
-
sort:
|
|
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
|
-
|
|
1910
|
+
q: dict = deepcopy(query) or {}
|
|
1859
1911
|
|
|
1860
|
-
if self.project and "project" not in
|
|
1861
|
-
|
|
1912
|
+
if self.project and "project" not in q:
|
|
1913
|
+
q["project"] = self.project
|
|
1862
1914
|
|
|
1863
1915
|
if paginate:
|
|
1864
|
-
cids = [
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
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,
|
|
1879
|
-
responses =
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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, **
|
|
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:
|
|
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
|
-
|
|
1910
|
-
if not valid:
|
|
1911
|
-
raise MPContribsClientError(error)
|
|
1969
|
+
self._is_valid_payload("Contribution", data)
|
|
1912
1970
|
|
|
1913
1971
|
if "data" in data:
|
|
1914
|
-
|
|
1915
|
-
if not serializable:
|
|
1916
|
-
raise MPContribsClientError(error)
|
|
1972
|
+
self._is_serializable_dict(data["data"])
|
|
1917
1973
|
|
|
1918
|
-
|
|
1974
|
+
q = deepcopy(query) or {}
|
|
1919
1975
|
|
|
1920
1976
|
if self.project:
|
|
1921
|
-
if "project" in
|
|
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
|
-
|
|
1981
|
+
q["project"] = self.project
|
|
1926
1982
|
else:
|
|
1927
|
-
if not
|
|
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 =
|
|
1933
|
-
cids = list(self.get_all_ids(
|
|
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 =
|
|
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,
|
|
1950
|
-
for i,
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
2068
|
+
q = deepcopy(query) or {}
|
|
2013
2069
|
|
|
2014
2070
|
if self.project:
|
|
2015
|
-
|
|
2071
|
+
q["project"] = self.project
|
|
2016
2072
|
|
|
2017
2073
|
try:
|
|
2018
2074
|
resp = self.projects.getProjectByName(
|
|
2019
|
-
pk=
|
|
2075
|
+
pk=q["project"], _fields=["is_public", "is_approved"]
|
|
2020
2076
|
).result()
|
|
2021
2077
|
except HTTPNotFound:
|
|
2022
2078
|
raise MPContribsClientError(
|
|
2023
|
-
f"project `{
|
|
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 `{
|
|
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 `{
|
|
2092
|
+
f"project `{q['project']}` is not approved yet!"
|
|
2037
2093
|
)
|
|
2038
2094
|
|
|
2039
2095
|
resp = self.projects.updateProjectByName(
|
|
2040
|
-
pk=
|
|
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
|
-
|
|
2046
|
-
|
|
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=
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
2429
|
-
outdir:
|
|
2480
|
+
query: dict | None = None,
|
|
2481
|
+
outdir: str | Path = DEFAULT_DOWNLOAD_DIR,
|
|
2430
2482
|
overwrite: bool = False,
|
|
2431
|
-
include:
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
2456
|
-
fmt =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,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
|
-

|
|
51
|
-

|
|
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,,
|
{mpcontribs_client-5.10.4.dist-info → mpcontribs_client-5.10.5rc1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|