cmsdials 1.2.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {cmsdials-1.2.0 → cmsdials-1.4.0}/PKG-INFO +49 -1
  2. {cmsdials-1.2.0 → cmsdials-1.4.0}/README.md +47 -0
  3. cmsdials-1.4.0/cmsdials/clients/file_index/client.py +13 -0
  4. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/file_index/models.py +1 -1
  5. cmsdials-1.4.0/cmsdials/clients/h1d/client.py +13 -0
  6. cmsdials-1.4.0/cmsdials/clients/h2d/client.py +13 -0
  7. cmsdials-1.4.0/cmsdials/clients/lumisection/client.py +13 -0
  8. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/mes/client.py +4 -0
  9. cmsdials-1.4.0/cmsdials/clients/oms_proxy/client.py +46 -0
  10. cmsdials-1.4.0/cmsdials/clients/oms_proxy/models.py +14 -0
  11. cmsdials-1.4.0/cmsdials/clients/run/client.py +13 -0
  12. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/cmsdials.py +2 -0
  13. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/filters.py +3 -0
  14. cmsdials-1.4.0/cmsdials/utils/__init__.py +0 -0
  15. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/utils/api_client.py +82 -13
  16. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/utils/base_model.py +4 -0
  17. {cmsdials-1.2.0 → cmsdials-1.4.0}/pyproject.toml +2 -1
  18. cmsdials-1.2.0/cmsdials/clients/file_index/client.py +0 -27
  19. cmsdials-1.2.0/cmsdials/clients/h1d/client.py +0 -27
  20. cmsdials-1.2.0/cmsdials/clients/h2d/client.py +0 -27
  21. cmsdials-1.2.0/cmsdials/clients/lumisection/client.py +0 -27
  22. cmsdials-1.2.0/cmsdials/clients/run/client.py +0 -27
  23. {cmsdials-1.2.0 → cmsdials-1.4.0}/LICENSE +0 -0
  24. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/__init__.py +0 -0
  25. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/__init__.py +0 -0
  26. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/_base.py +0 -0
  27. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/bearer.py +0 -0
  28. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/client.py +0 -0
  29. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/exceptions.py +0 -0
  30. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/models.py +0 -0
  31. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/auth/secret_key.py +0 -0
  32. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/__init__.py +0 -0
  33. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/file_index/__init__.py +0 -0
  34. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/h1d/__init__.py +0 -0
  35. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/h1d/models.py +0 -0
  36. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/h2d/__init__.py +0 -0
  37. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/h2d/models.py +0 -0
  38. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/lumisection/__init__.py +0 -0
  39. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/lumisection/models.py +0 -0
  40. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/mes/__init__.py +0 -0
  41. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/mes/models.py +0 -0
  42. {cmsdials-1.2.0/cmsdials/clients/run → cmsdials-1.4.0/cmsdials/clients/oms_proxy}/__init__.py +0 -0
  43. {cmsdials-1.2.0/cmsdials/utils → cmsdials-1.4.0/cmsdials/clients/run}/__init__.py +0 -0
  44. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/clients/run/models.py +0 -0
  45. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/utils/_json.py +0 -0
  46. {cmsdials-1.2.0 → cmsdials-1.4.0}/cmsdials/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cmsdials
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: The Python api client interface to DIALS service
5
5
  Home-page: https://github.com/cms-DQM/dials-py
6
6
  Author: Gabriel Moreira
@@ -22,6 +22,7 @@ Requires-Dist: pydantic (>=1,<2)
22
22
  Requires-Dist: requests (>=2.31.0,<3.0.0)
23
23
  Requires-Dist: tqdm (>=4.66.1) ; extra == "tqdm"
24
24
  Requires-Dist: typing-extensions (>=3.6.6,<4.6.0)
25
+ Requires-Dist: urllib3 (>=2.2.3,<3.0.0)
25
26
  Project-URL: Repository, https://github.com/cms-DQM/dials-py
26
27
  Description-Content-Type: text/markdown
27
28
 
@@ -200,6 +201,53 @@ Progress: 100%|█████████████████████
200
201
 
201
202
  The total attribute of the bar is dynamically updated while fetching the pages.
202
203
 
204
+ ## Retrying
205
+
206
+ In case of an unstable connection, DNS failures or service overload it is possible to configure any `get`, `list` and `list_all` to retry the underlying requests using native `urllib3` retry class, for example:
207
+
208
+ ```python
209
+ from urllib3.util import Retry
210
+
211
+ data = dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=1, retries=Retry(total=3, backoff_factor=0.1))
212
+ data = dials.h1d.list(retries=Retry(total=3, backoff_factor=0.1))
213
+ data = dials.h1d.list_all(LumisectionHistogram1DFilters(), max_pages=5, retries=Retry(total=5, backoff_factor=0.1))
214
+ ```
215
+
216
+ ## Resuming
217
+
218
+ When listing and endpoint for a long time, you may loose connection or the server can potentially return an error. By specifying `keep_failed` and using `resume_from` you can resume from an older response object, for example:
219
+
220
+ ```python
221
+ from cmsdials.auth.client import AuthClient
222
+ from cmsdials.auth.bearer import Credentials
223
+ from cmsdials import Dials
224
+ from cmsdials.filters import LumisectionHistogram2DFilters
225
+
226
+ auth = AuthClient()
227
+ creds = Credentials.from_creds_file()
228
+ dials = Dials(creds, workspace="tracker")
229
+
230
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), keep_failed=True) # Code may broke inside this routine
231
+ print(len(data.results)) # 100
232
+ print(data.exc_type)
233
+ print(data.exc_formatted)
234
+
235
+ # After failing, run this again
236
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), keep_failed=True, resume_from=data) # Resume from failed object
237
+ print(len(data.results)) # 200
238
+ ```
239
+
240
+ You may find it useful to resume from a partial response, i.e,
241
+
242
+ ```python
243
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=10, keep_failed=True) # Code may not break, however will fetch only 100 elements
244
+ print(len(data.results)) # 100
245
+
246
+ # After fetching first 100, fetch next 100
247
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=10, keep_failed=True, resume_from=data) # Resume from partial object
248
+ print(len(data.results)) # 200
249
+ ```
250
+
203
251
  ## Usage with local DIALS
204
252
 
205
253
  All classes that interface the DIALS service inherits the class `BaseAPIClient` which propagate the `base_url`, `route` and `version` attributes with production values. In order to use dials-py with a local version of DIALS it is possible to overwrite those attributes when instantiating the `AuthClient` and the `Dials` client, for example:
@@ -173,6 +173,53 @@ Progress: 100%|█████████████████████
173
173
 
174
174
  The total attribute of the bar is dynamically updated while fetching the pages.
175
175
 
176
+ ## Retrying
177
+
178
+ In case of an unstable connection, DNS failures or service overload it is possible to configure any `get`, `list` and `list_all` to retry the underlying requests using native `urllib3` retry class, for example:
179
+
180
+ ```python
181
+ from urllib3.util import Retry
182
+
183
+ data = dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=1, retries=Retry(total=3, backoff_factor=0.1))
184
+ data = dials.h1d.list(retries=Retry(total=3, backoff_factor=0.1))
185
+ data = dials.h1d.list_all(LumisectionHistogram1DFilters(), max_pages=5, retries=Retry(total=5, backoff_factor=0.1))
186
+ ```
187
+
188
+ ## Resuming
189
+
190
+ When listing and endpoint for a long time, you may loose connection or the server can potentially return an error. By specifying `keep_failed` and using `resume_from` you can resume from an older response object, for example:
191
+
192
+ ```python
193
+ from cmsdials.auth.client import AuthClient
194
+ from cmsdials.auth.bearer import Credentials
195
+ from cmsdials import Dials
196
+ from cmsdials.filters import LumisectionHistogram2DFilters
197
+
198
+ auth = AuthClient()
199
+ creds = Credentials.from_creds_file()
200
+ dials = Dials(creds, workspace="tracker")
201
+
202
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), keep_failed=True) # Code may broke inside this routine
203
+ print(len(data.results)) # 100
204
+ print(data.exc_type)
205
+ print(data.exc_formatted)
206
+
207
+ # After failing, run this again
208
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), keep_failed=True, resume_from=data) # Resume from failed object
209
+ print(len(data.results)) # 200
210
+ ```
211
+
212
+ You may find it useful to resume from a partial response, i.e,
213
+
214
+ ```python
215
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=10, keep_failed=True) # Code may not break, however will fetch only 100 elements
216
+ print(len(data.results)) # 100
217
+
218
+ # After fetching first 100, fetch next 100
219
+ data = dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=10, keep_failed=True, resume_from=data) # Resume from partial object
220
+ print(len(data.results)) # 200
221
+ ```
222
+
176
223
  ## Usage with local DIALS
177
224
 
178
225
  All classes that interface the DIALS service inherits the class `BaseAPIClient` which propagate the `base_url`, `route` and `version` attributes with production values. In order to use dials-py with a local version of DIALS it is possible to overwrite those attributes when instantiating the `AuthClient` and the `Dials` client, for example:
@@ -0,0 +1,13 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import FileIndex, FileIndexFilters, PaginatedFileIndexList
3
+
4
+
5
+ class FileIndexClient(BaseAuthorizedAPIClient):
6
+ data_model = FileIndex
7
+ pagination_model = PaginatedFileIndexList
8
+ filter_class = FileIndexFilters
9
+ lookup_url = "file-index/"
10
+
11
+ def get(self, dataset_id: int, file_id: int, **kwargs):
12
+ edp = f"{dataset_id}/{file_id}/"
13
+ return super().get(edp, **kwargs)
@@ -14,7 +14,7 @@ class FileIndex(BaseModel):
14
14
  creation_date: datetime
15
15
  last_modification_date: datetime
16
16
  logical_file_name: str
17
- status: str = Field(..., max_length=15)
17
+ status: str = Field(..., max_length=255)
18
18
  err_trace: Optional[str] = Field(..., max_length=5000)
19
19
 
20
20
 
@@ -0,0 +1,13 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import LumisectionHistogram1D, LumisectionHistogram1DFilters, PaginatedLumisectionHistogram1DList
3
+
4
+
5
+ class LumisectionHistogram1DClient(BaseAuthorizedAPIClient):
6
+ data_model = LumisectionHistogram1D
7
+ pagination_model = PaginatedLumisectionHistogram1DList
8
+ filter_class = LumisectionHistogram1DFilters
9
+ lookup_url = "th1/"
10
+
11
+ def get(self, dataset_id: int, run_number: int, ls_number: int, me_id: int, **kwargs):
12
+ edp = f"{dataset_id}/{run_number}/{ls_number}/{me_id}/"
13
+ return super().get(edp, **kwargs)
@@ -0,0 +1,13 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import LumisectionHistogram2D, LumisectionHistogram2DFilters, PaginatedLumisectionHistogram2DList
3
+
4
+
5
+ class LumisectionHistogram2DClient(BaseAuthorizedAPIClient):
6
+ data_model = LumisectionHistogram2D
7
+ pagination_model = PaginatedLumisectionHistogram2DList
8
+ filter_class = LumisectionHistogram2DFilters
9
+ lookup_url = "th2/"
10
+
11
+ def get(self, dataset_id: int, run_number: int, ls_number: int, me_id: int, **kwargs):
12
+ edp = f"{dataset_id}/{run_number}/{ls_number}/{me_id}/"
13
+ return super().get(edp, **kwargs)
@@ -0,0 +1,13 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import Lumisection, LumisectionFilters, PaginatedLumisectionList
3
+
4
+
5
+ class LumisectionClient(BaseAuthorizedAPIClient):
6
+ data_model = Lumisection
7
+ pagination_model = PaginatedLumisectionList
8
+ filter_class = LumisectionFilters
9
+ lookup_url = "lumisection/"
10
+
11
+ def get(self, dataset_id: int, run_number: int, ls_number: int, **kwargs):
12
+ edp = f"{dataset_id}/{run_number}/{ls_number}/"
13
+ return super().get(edp, **kwargs)
@@ -6,3 +6,7 @@ class MonitoringElementClient(BaseAuthorizedAPIClient):
6
6
  data_model = MonitoringElement
7
7
  filter_class = MEFilters
8
8
  lookup_url = "mes/"
9
+
10
+ def get(self, id: int, **kwargs): # noqa: A002
11
+ edp = f"{id}/"
12
+ return super().get(edp, **kwargs)
@@ -0,0 +1,46 @@
1
+ from typing import Optional
2
+
3
+ import requests
4
+ from requests.exceptions import HTTPError
5
+
6
+ from ...auth._base import BaseCredentials
7
+ from ...utils.api_client import BaseAPIClient
8
+ from ...utils.logger import logger
9
+ from .models import OMSFilter, OMSPage
10
+
11
+
12
+ class OMSProxyClient(BaseAPIClient):
13
+ default_timeout = 30
14
+ lookup_url = "oms-proxy/"
15
+
16
+ def __init__(
17
+ self,
18
+ creds: BaseCredentials,
19
+ *args: str,
20
+ **kwargs: str,
21
+ ) -> None:
22
+ super().__init__(*args, **kwargs)
23
+ self.creds = creds
24
+
25
+ def _build_headers(self) -> dict:
26
+ base = {"accept": "application/json"}
27
+ self.creds.before_request(base)
28
+ return base
29
+
30
+ def query(self, endpoint: str, filters: list[OMSFilter], pages: Optional[list[OMSPage]] = None):
31
+ headers = self._build_headers()
32
+ endpoint_url = self.api_url + self.lookup_url
33
+
34
+ # Format filters
35
+ filters = {f"filter[{_filter.attribute_name}][{_filter.operator}]": _filter.value for _filter in filters}
36
+ pages = {f"page[{page.attribute_name}]": page.value for page in pages} if pages and len(pages) > 0 else {}
37
+ params = {"endpoint": endpoint, **filters, **pages}
38
+
39
+ response = requests.get(endpoint_url, headers=headers, params=params, timeout=self.default_timeout)
40
+ try:
41
+ response.raise_for_status()
42
+ except HTTPError as err:
43
+ logger.info(f"Api raw response: {response.text}")
44
+ raise err
45
+
46
+ return response.json()
@@ -0,0 +1,14 @@
1
+ from typing import Literal, Union
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class OMSFilter(BaseModel):
7
+ attribute_name: str
8
+ value: Union[str, int, float]
9
+ operator: Literal["EQ", "NEQ", "LT", "GT", "LE", "GE", "LIKE"]
10
+
11
+
12
+ class OMSPage(BaseModel):
13
+ attribute_name: str
14
+ value: int
@@ -0,0 +1,13 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import PaginatedRunList, Run, RunFilters
3
+
4
+
5
+ class RunClient(BaseAuthorizedAPIClient):
6
+ data_model = Run
7
+ pagination_model = PaginatedRunList
8
+ filter_class = RunFilters
9
+ lookup_url = "run/"
10
+
11
+ def get(self, dataset_id: int, run_number: int, **kwargs):
12
+ edp = f"{dataset_id}/{run_number}/"
13
+ return super().get(edp, **kwargs)
@@ -6,6 +6,7 @@ from .clients.h1d.client import LumisectionHistogram1DClient
6
6
  from .clients.h2d.client import LumisectionHistogram2DClient
7
7
  from .clients.lumisection.client import LumisectionClient
8
8
  from .clients.mes.client import MonitoringElementClient
9
+ from .clients.oms_proxy.client import OMSProxyClient
9
10
  from .clients.run.client import RunClient
10
11
 
11
12
 
@@ -17,3 +18,4 @@ class Dials:
17
18
  self.lumi = LumisectionClient(creds, workspace, *args, **kwargs)
18
19
  self.run = RunClient(creds, workspace, *args, **kwargs)
19
20
  self.mes = MonitoringElementClient(creds, workspace, *args, **kwargs)
21
+ self.oms = OMSProxyClient(creds, *args, **kwargs)
@@ -3,6 +3,7 @@ from .clients.h1d.models import LumisectionHistogram1DFilters
3
3
  from .clients.h2d.models import LumisectionHistogram2DFilters
4
4
  from .clients.lumisection.models import LumisectionFilters
5
5
  from .clients.mes.models import MEFilters
6
+ from .clients.oms_proxy.models import OMSFilter, OMSPage
6
7
  from .clients.run.models import RunFilters
7
8
 
8
9
 
@@ -13,4 +14,6 @@ __all__ = [
13
14
  "LumisectionFilters",
14
15
  "RunFilters",
15
16
  "MEFilters",
17
+ "OMSFilter",
18
+ "OMSPage",
16
19
  ]
File without changes
@@ -1,8 +1,11 @@
1
1
  from importlib import util as importlib_util
2
+ from traceback import format_exc
2
3
  from typing import Optional
3
4
  from urllib.parse import parse_qs, urlparse
5
+ from warnings import warn
4
6
 
5
- import requests
7
+ from requests import Response, Session
8
+ from requests.adapters import DEFAULT_RETRIES, HTTPAdapter
6
9
  from requests.exceptions import HTTPError
7
10
 
8
11
  from ..auth._base import BaseCredentials
@@ -35,6 +38,19 @@ class BaseAPIClient:
35
38
  return value + "/"
36
39
  return value
37
40
 
41
+ @classmethod
42
+ def _requests_get_retriable(cls, *args, retries=DEFAULT_RETRIES, **kwargs) -> Response:
43
+ """
44
+ requests.get() with an additional `retries` parameter.
45
+
46
+ Specify retries=<number of attempts - 1> for simple use cases.
47
+ For advanced usage, see https://docs.python-requests.org/en/latest/user/advanced/
48
+ """
49
+ with Session() as s:
50
+ s.mount(cls.PRODUCTION_BASE_URL, HTTPAdapter(max_retries=retries))
51
+ ret = s.get(*args, **kwargs)
52
+ return ret
53
+
38
54
  @property
39
55
  def api_url(self):
40
56
  return self.base_url + self.route + self.version
@@ -65,10 +81,15 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
65
81
  self.creds.before_request(base)
66
82
  return base
67
83
 
68
- def get(self, id: int): # noqa: A002
69
- endpoint_url = self.api_url + self.lookup_url + str(id) + "/"
84
+ def get(self, edp: str, retries=DEFAULT_RETRIES): # noqa: A002
85
+ endpoint_url = self.api_url + self.lookup_url + edp
70
86
  headers = self._build_headers()
71
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
87
+ response = self._requests_get_retriable(
88
+ endpoint_url,
89
+ headers=headers,
90
+ timeout=self.default_timeout,
91
+ retries=retries,
92
+ )
72
93
 
73
94
  try:
74
95
  response.raise_for_status()
@@ -79,11 +100,17 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
79
100
  response = response.json()
80
101
  return self.data_model(**response)
81
102
 
82
- def list(self, filters=None):
103
+ def list(self, filters=None, retries=DEFAULT_RETRIES):
83
104
  filters = filters or self.filter_class()
84
105
  endpoint_url = self.api_url + self.lookup_url
85
106
  headers = self._build_headers()
86
- response = requests.get(endpoint_url, headers=headers, params=filters.cleandict(), timeout=self.default_timeout)
107
+ response = self._requests_get_retriable(
108
+ endpoint_url,
109
+ headers=headers,
110
+ params=filters.cleandict(),
111
+ timeout=self.default_timeout,
112
+ retries=retries,
113
+ )
87
114
 
88
115
  try:
89
116
  response.raise_for_status()
@@ -99,10 +126,30 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
99
126
 
100
127
  raise ValueError("pagination model is None and response is not a list.")
101
128
 
102
- def __list_sync(self, filters, max_pages: Optional[int] = None, enable_progress: bool = False):
103
- next_token = None
129
+ def __list_sync(
130
+ self,
131
+ filters,
132
+ max_pages: Optional[int] = None,
133
+ enable_progress: bool = False,
134
+ retries=DEFAULT_RETRIES,
135
+ keep_failed: bool = False,
136
+ resume_from=None,
137
+ ):
138
+ next_string: Optional[str] = None
104
139
  results = []
105
140
  is_last_page = False
141
+
142
+ if resume_from is not None:
143
+ results = resume_from.results
144
+ next_string = resume_from.next
145
+ if next_string is None and len(results):
146
+ warn(
147
+ "resume_from.next is None while resume_from.result is not empty, doing nothing.",
148
+ RuntimeWarning,
149
+ stacklevel=2,
150
+ )
151
+ is_last_page = True
152
+
106
153
  total_pages = 0
107
154
  use_tqdm = TQDM_INSTALLED and enable_progress
108
155
 
@@ -110,12 +157,34 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
110
157
  progress = tqdm(desc="Progress", total=1)
111
158
 
112
159
  while is_last_page is False:
160
+ next_token = parse_qs(urlparse(next_string).query).get("next_token") if next_string else None
113
161
  curr_filters = self.filter_class(**filters.dict())
114
162
  curr_filters.next_token = next_token
115
- response = self.list(curr_filters)
163
+ try:
164
+ response = self.list(curr_filters, retries=retries)
165
+ except Exception as e: # noqa: BLE001
166
+ if use_tqdm:
167
+ progress.close()
168
+
169
+ if not keep_failed:
170
+ raise e
171
+
172
+ exc_formatted = format_exc()
173
+ warn(
174
+ "HTTP request failed, returning partial results. Exception: " + exc_formatted,
175
+ RuntimeWarning,
176
+ stacklevel=2,
177
+ )
178
+ return self.pagination_model(
179
+ next=next_string,
180
+ previous=None,
181
+ results=results,
182
+ exc_type=e.__class__.__name__,
183
+ exc_formatted=exc_formatted,
184
+ )
116
185
  results.extend(response.results)
117
- is_last_page = response.next is None
118
- next_token = parse_qs(urlparse(response.next).query).get("next_token") if response.next else None
186
+ next_string = response.next
187
+ is_last_page = next_string is None
119
188
  total_pages += 1
120
189
  max_pages_reached = max_pages and total_pages >= max_pages
121
190
  if use_tqdm:
@@ -131,12 +200,12 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
131
200
  progress.close()
132
201
 
133
202
  return self.pagination_model(
134
- next=None,
203
+ next=next_string,
135
204
  previous=None,
136
205
  results=results, # No problem re-using last response count
137
206
  )
138
207
 
139
208
  def list_all(self, filters, max_pages: Optional[int] = None, **kwargs):
140
209
  if self.pagination_model is None:
141
- return self.list(filters)
210
+ return self.list(filters, retries=kwargs.get("retries", DEFAULT_RETRIES))
142
211
  return self.__list_sync(filters, max_pages, **kwargs)
@@ -1,4 +1,5 @@
1
1
  from importlib import util as importlib_util
2
+ from typing import Optional
2
3
 
3
4
  from pydantic import BaseModel
4
5
 
@@ -17,6 +18,9 @@ class OBaseModel(BaseModel):
17
18
 
18
19
 
19
20
  class PaginatedBaseModel(BaseModel):
21
+ exc_type: Optional[str] = None
22
+ exc_formatted: Optional[str] = None
23
+
20
24
  def to_pandas(self):
21
25
  if PANDAS_NOT_INSTALLED:
22
26
  raise RuntimeError(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cmsdials"
3
- version = "1.2.0"
3
+ version = "1.4.0"
4
4
  description = "The Python api client interface to DIALS service"
5
5
  authors = ["Gabriel Moreira <gabrielmscampos@gmail.com>"]
6
6
  readme = "README.md"
@@ -23,6 +23,7 @@ pydantic = "<2, >=1"
23
23
  typing-extensions = "<4.6.0, >=3.6.6"
24
24
  pandas = {version = ">=1.5.3", optional = true}
25
25
  tqdm = {version = ">=4.66.1", optional = true}
26
+ urllib3 = "^2.2.3"
26
27
 
27
28
  [tool.poetry.group.dev.dependencies]
28
29
  pre-commit = "^3.6.2"
@@ -1,27 +0,0 @@
1
- import requests
2
- from requests.exceptions import HTTPError
3
-
4
- from ...utils.api_client import BaseAuthorizedAPIClient
5
- from ...utils.logger import logger
6
- from .models import FileIndex, FileIndexFilters, PaginatedFileIndexList
7
-
8
-
9
- class FileIndexClient(BaseAuthorizedAPIClient):
10
- data_model = FileIndex
11
- pagination_model = PaginatedFileIndexList
12
- filter_class = FileIndexFilters
13
- lookup_url = "file-index/"
14
-
15
- def get(self, dataset_id: int, file_id: int):
16
- endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{file_id}/"
17
- headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
-
20
- try:
21
- response.raise_for_status()
22
- except HTTPError as err:
23
- logger.info(f"Api raw response: {response.text}")
24
- raise err
25
-
26
- response = response.json()
27
- return self.data_model(**response)
@@ -1,27 +0,0 @@
1
- import requests
2
- from requests.exceptions import HTTPError
3
-
4
- from ...utils.api_client import BaseAuthorizedAPIClient
5
- from ...utils.logger import logger
6
- from .models import LumisectionHistogram1D, LumisectionHistogram1DFilters, PaginatedLumisectionHistogram1DList
7
-
8
-
9
- class LumisectionHistogram1DClient(BaseAuthorizedAPIClient):
10
- data_model = LumisectionHistogram1D
11
- pagination_model = PaginatedLumisectionHistogram1DList
12
- filter_class = LumisectionHistogram1DFilters
13
- lookup_url = "th1/"
14
-
15
- def get(self, dataset_id: int, run_number: int, ls_number: int, me_id: int):
16
- endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/{ls_number}/{me_id}/"
17
- headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
-
20
- try:
21
- response.raise_for_status()
22
- except HTTPError as err:
23
- logger.info(f"Api raw response: {response.text}")
24
- raise err
25
-
26
- response = response.json()
27
- return self.data_model(**response)
@@ -1,27 +0,0 @@
1
- import requests
2
- from requests.exceptions import HTTPError
3
-
4
- from ...utils.api_client import BaseAuthorizedAPIClient
5
- from ...utils.logger import logger
6
- from .models import LumisectionHistogram2D, LumisectionHistogram2DFilters, PaginatedLumisectionHistogram2DList
7
-
8
-
9
- class LumisectionHistogram2DClient(BaseAuthorizedAPIClient):
10
- data_model = LumisectionHistogram2D
11
- pagination_model = PaginatedLumisectionHistogram2DList
12
- filter_class = LumisectionHistogram2DFilters
13
- lookup_url = "th2/"
14
-
15
- def get(self, dataset_id: int, run_number: int, ls_number: int, me_id: int):
16
- endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/{ls_number}/{me_id}/"
17
- headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
-
20
- try:
21
- response.raise_for_status()
22
- except HTTPError as err:
23
- logger.info(f"Api raw response: {response.text}")
24
- raise err
25
-
26
- response = response.json()
27
- return self.data_model(**response)
@@ -1,27 +0,0 @@
1
- import requests
2
- from requests.exceptions import HTTPError
3
-
4
- from ...utils.api_client import BaseAuthorizedAPIClient
5
- from ...utils.logger import logger
6
- from .models import Lumisection, LumisectionFilters, PaginatedLumisectionList
7
-
8
-
9
- class LumisectionClient(BaseAuthorizedAPIClient):
10
- data_model = Lumisection
11
- pagination_model = PaginatedLumisectionList
12
- filter_class = LumisectionFilters
13
- lookup_url = "lumisection/"
14
-
15
- def get(self, dataset_id: int, run_number: int, ls_number: int):
16
- endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/{ls_number}/"
17
- headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
-
20
- try:
21
- response.raise_for_status()
22
- except HTTPError as err:
23
- logger.info(f"Api raw response: {response.text}")
24
- raise err
25
-
26
- response = response.json()
27
- return self.data_model(**response)
@@ -1,27 +0,0 @@
1
- import requests
2
- from requests.exceptions import HTTPError
3
-
4
- from ...utils.api_client import BaseAuthorizedAPIClient
5
- from ...utils.logger import logger
6
- from .models import PaginatedRunList, Run, RunFilters
7
-
8
-
9
- class RunClient(BaseAuthorizedAPIClient):
10
- data_model = Run
11
- pagination_model = PaginatedRunList
12
- filter_class = RunFilters
13
- lookup_url = "run/"
14
-
15
- def get(self, dataset_id: int, run_number: int):
16
- endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/"
17
- headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
-
20
- try:
21
- response.raise_for_status()
22
- except HTTPError as err:
23
- logger.info(f"Api raw response: {response.text}")
24
- raise err
25
-
26
- response = response.json()
27
- return self.data_model(**response)
File without changes
File without changes