cmsdials 1.0.1__tar.gz → 1.2.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 (42) hide show
  1. {cmsdials-1.0.1 → cmsdials-1.2.0}/PKG-INFO +58 -8
  2. {cmsdials-1.0.1 → cmsdials-1.2.0}/README.md +53 -7
  3. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/__init__.py +1 -0
  4. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/_base.py +3 -3
  5. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/bearer.py +4 -2
  6. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/client.py +5 -0
  7. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/exceptions.py +1 -1
  8. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/secret_key.py +1 -1
  9. cmsdials-1.2.0/cmsdials/clients/file_index/client.py +27 -0
  10. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/file_index/models.py +4 -4
  11. cmsdials-1.2.0/cmsdials/clients/h1d/client.py +27 -0
  12. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h1d/models.py +4 -5
  13. cmsdials-1.2.0/cmsdials/clients/h2d/client.py +27 -0
  14. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h2d/models.py +2 -3
  15. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/client.py +1 -1
  16. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/models.py +2 -2
  17. cmsdials-1.2.0/cmsdials/clients/mes/client.py +8 -0
  18. cmsdials-1.2.0/cmsdials/clients/mes/models.py +18 -0
  19. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/run/client.py +1 -1
  20. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/run/models.py +2 -2
  21. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/cmsdials.py +2 -0
  22. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/filters.py +3 -0
  23. cmsdials-1.2.0/cmsdials/utils/__init__.py +0 -0
  24. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/utils/api_client.py +42 -9
  25. cmsdials-1.2.0/cmsdials/utils/base_model.py +25 -0
  26. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/utils/logger.py +1 -0
  27. {cmsdials-1.0.1 → cmsdials-1.2.0}/pyproject.toml +10 -2
  28. cmsdials-1.0.1/cmsdials/clients/file_index/client.py +0 -9
  29. cmsdials-1.0.1/cmsdials/clients/h1d/client.py +0 -9
  30. cmsdials-1.0.1/cmsdials/clients/h2d/client.py +0 -9
  31. cmsdials-1.0.1/cmsdials/utils/base_model.py +0 -6
  32. {cmsdials-1.0.1 → cmsdials-1.2.0}/LICENSE +0 -0
  33. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/__init__.py +0 -0
  34. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/models.py +0 -0
  35. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/__init__.py +0 -0
  36. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/file_index/__init__.py +0 -0
  37. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h1d/__init__.py +0 -0
  38. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h2d/__init__.py +0 -0
  39. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/__init__.py +0 -0
  40. {cmsdials-1.0.1/cmsdials/clients/run → cmsdials-1.2.0/cmsdials/clients/mes}/__init__.py +0 -0
  41. {cmsdials-1.0.1/cmsdials/utils → cmsdials-1.2.0/cmsdials/clients/run}/__init__.py +0 -0
  42. {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/utils/_json.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cmsdials
3
- Version: 1.0.1
3
+ Version: 1.2.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
@@ -15,8 +15,12 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Provides-Extra: pandas
19
+ Provides-Extra: tqdm
20
+ Requires-Dist: pandas (>=1.5.3) ; extra == "pandas"
18
21
  Requires-Dist: pydantic (>=1,<2)
19
22
  Requires-Dist: requests (>=2.31.0,<3.0.0)
23
+ Requires-Dist: tqdm (>=4.66.1) ; extra == "tqdm"
20
24
  Requires-Dist: typing-extensions (>=3.6.6,<4.6.0)
21
25
  Project-URL: Repository, https://github.com/cms-DQM/dials-py
22
26
  Description-Content-Type: text/markdown
@@ -33,6 +37,12 @@ To install dials-py, simply
33
37
  $ pip install cmsdials
34
38
  ```
35
39
 
40
+ It is also possible to specify the following extras, to enable optional features:
41
+
42
+ ```
43
+ pandas, tqdm
44
+ ```
45
+
36
46
  ## Usage
37
47
 
38
48
  Before interfacing with any route you need to generate valid credentials, it is possible to authenticate trough the `device authorization flow` or using the `client secret key` of any application registered in DIALS. Note that, the device flow is an interactively authentication procedure that is possible to distinguish users in DIALS backend and the client secret flow is not interactive and is not possible to distinguish users so it should only be used for automation scripts.
@@ -97,11 +107,11 @@ This package interacts with DIALS api endpoints using underlying classes in `Dia
97
107
  ### Retrieving a specific object using `get`
98
108
 
99
109
  ```python
100
- dials.file_index.get(id=3386119397)
101
- dials.h1d.get(id=1)
102
- dials.h2d.get(id=1)
103
- dials.lumi.get(dataset_id=14677060, run_number=367094, ls_number=1)
104
- dials.run.get(dataset_id=14677060, run_number=367094)
110
+ dials.file_index.get(dataset_id=14677060, file_id=3393809397)
111
+ dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=1)
112
+ dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=96)
113
+ dials.lumi.get(dataset_id=14677060, run_number=367112, ls_number=10)
114
+ dials.run.get(dataset_id=14677060, run_number=367112)
105
115
  ```
106
116
 
107
117
  ### Retrieving a list of objects per page using `list`
@@ -160,12 +170,36 @@ dials.run.list_all(RunFilters(run_number__gte=360392, run_number__lte=365000), m
160
170
 
161
171
  ### Dials MEs
162
172
 
163
- It is possible to inspect the list of selected MEs considered in DIALS during ETL requesting the endpoint `configured-mes` trough the method:
173
+ It is possible to inspect the list of ingested MEs in DIALS listing the endpoint `mes` trough the method:
174
+
175
+ ```python
176
+ dials.mes.list()
177
+ ```
178
+
179
+ ### Automatically convert paginated results to pandas DataFrame
180
+
181
+ You can enable this optional feature by installing the package with the `pandas` extra.
182
+
183
+ All `Paginated` metaclasses contain the method `to_pandas` that will automatically transform the `results` attribute of the metaclass into a pandas dataframe, for example:
184
+
185
+ ```python
186
+ data = dials.h1d.list_all(LumisectionHistogram1DFilters(me="PixelPhase1/Tracks/PXBarrel/charge_PXLayer_2"), max_pages=5)
187
+ data.to_pandas()
188
+ ```
189
+
190
+ ### Indefinite progress bar when fetch multi-page result
191
+
192
+ You can enable this optional feature by installing the package with the `tqdm` extra.
193
+
194
+ Whenever you call a `list_all` method that fetches multiple pages a dynamic progress will be rendered to indicate duration and number of pages, for example:
164
195
 
165
196
  ```python
166
- dials.lumi.configured_mes()
197
+ >>> dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=5)
198
+ Progress: 100%|█████████████████████████████████████| 5/5 [00:02<00:00, 1.70it/s]
167
199
  ```
168
200
 
201
+ The total attribute of the bar is dynamically updated while fetching the pages.
202
+
169
203
  ## Usage with local DIALS
170
204
 
171
205
  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:
@@ -186,3 +220,19 @@ dials = Dials(creds, base_url=DEV_URL)
186
220
  dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy EE +", entries__gte=100, run_number__gte=360392, run_number__lte=365000), max_pages=5)
187
221
  ```
188
222
 
223
+ ## Running tests
224
+
225
+ The repository has some tests written to make sure DIALS responses are compatible with pydantic metaclasses, you can use pytest to run all tests but you need to specify a secret key to authenticate non-interactively against DIALS api:
226
+
227
+ ```bash
228
+ SECRET_KEY=... pytest tests
229
+ ```
230
+
231
+ The secret key is an api client enabled secret key and can be obtained from the applications portal, any api client secret key whitelisted in DIALS can be used. The interactive authentication flow should be tested manually, an example for this can be found in [this line](/tests/integration/test_auth_client.py#L33).
232
+
233
+ If testing against a local version of DIALS you need to specify the BASE_URL:
234
+
235
+ ```bash
236
+ SECRET_KEY=... BASE_URl=http://localhost:8000 pytest tests
237
+ ```
238
+
@@ -10,6 +10,12 @@ To install dials-py, simply
10
10
  $ pip install cmsdials
11
11
  ```
12
12
 
13
+ It is also possible to specify the following extras, to enable optional features:
14
+
15
+ ```
16
+ pandas, tqdm
17
+ ```
18
+
13
19
  ## Usage
14
20
 
15
21
  Before interfacing with any route you need to generate valid credentials, it is possible to authenticate trough the `device authorization flow` or using the `client secret key` of any application registered in DIALS. Note that, the device flow is an interactively authentication procedure that is possible to distinguish users in DIALS backend and the client secret flow is not interactive and is not possible to distinguish users so it should only be used for automation scripts.
@@ -74,11 +80,11 @@ This package interacts with DIALS api endpoints using underlying classes in `Dia
74
80
  ### Retrieving a specific object using `get`
75
81
 
76
82
  ```python
77
- dials.file_index.get(id=3386119397)
78
- dials.h1d.get(id=1)
79
- dials.h2d.get(id=1)
80
- dials.lumi.get(dataset_id=14677060, run_number=367094, ls_number=1)
81
- dials.run.get(dataset_id=14677060, run_number=367094)
83
+ dials.file_index.get(dataset_id=14677060, file_id=3393809397)
84
+ dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=1)
85
+ dials.h1d.get(dataset_id=14677060, run_number=367112, ls_number=10, me_id=96)
86
+ dials.lumi.get(dataset_id=14677060, run_number=367112, ls_number=10)
87
+ dials.run.get(dataset_id=14677060, run_number=367112)
82
88
  ```
83
89
 
84
90
  ### Retrieving a list of objects per page using `list`
@@ -137,12 +143,36 @@ dials.run.list_all(RunFilters(run_number__gte=360392, run_number__lte=365000), m
137
143
 
138
144
  ### Dials MEs
139
145
 
140
- It is possible to inspect the list of selected MEs considered in DIALS during ETL requesting the endpoint `configured-mes` trough the method:
146
+ It is possible to inspect the list of ingested MEs in DIALS listing the endpoint `mes` trough the method:
147
+
148
+ ```python
149
+ dials.mes.list()
150
+ ```
151
+
152
+ ### Automatically convert paginated results to pandas DataFrame
153
+
154
+ You can enable this optional feature by installing the package with the `pandas` extra.
155
+
156
+ All `Paginated` metaclasses contain the method `to_pandas` that will automatically transform the `results` attribute of the metaclass into a pandas dataframe, for example:
157
+
158
+ ```python
159
+ data = dials.h1d.list_all(LumisectionHistogram1DFilters(me="PixelPhase1/Tracks/PXBarrel/charge_PXLayer_2"), max_pages=5)
160
+ data.to_pandas()
161
+ ```
162
+
163
+ ### Indefinite progress bar when fetch multi-page result
164
+
165
+ You can enable this optional feature by installing the package with the `tqdm` extra.
166
+
167
+ Whenever you call a `list_all` method that fetches multiple pages a dynamic progress will be rendered to indicate duration and number of pages, for example:
141
168
 
142
169
  ```python
143
- dials.lumi.configured_mes()
170
+ >>> dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="PXBarrel", ls_number=78, entries__gte=100), max_pages=5)
171
+ Progress: 100%|█████████████████████████████████████| 5/5 [00:02<00:00, 1.70it/s]
144
172
  ```
145
173
 
174
+ The total attribute of the bar is dynamically updated while fetching the pages.
175
+
146
176
  ## Usage with local DIALS
147
177
 
148
178
  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:
@@ -162,3 +192,19 @@ dials = Dials(creds, base_url=DEV_URL)
162
192
 
163
193
  dials.h2d.list_all(LumisectionHistogram2DFilters(me__regex="EEOT digi occupancy EE +", entries__gte=100, run_number__gte=360392, run_number__lte=365000), max_pages=5)
164
194
  ```
195
+
196
+ ## Running tests
197
+
198
+ The repository has some tests written to make sure DIALS responses are compatible with pydantic metaclasses, you can use pytest to run all tests but you need to specify a secret key to authenticate non-interactively against DIALS api:
199
+
200
+ ```bash
201
+ SECRET_KEY=... pytest tests
202
+ ```
203
+
204
+ The secret key is an api client enabled secret key and can be obtained from the applications portal, any api client secret key whitelisted in DIALS can be used. The interactive authentication flow should be tested manually, an example for this can be found in [this line](/tests/integration/test_auth_client.py#L33).
205
+
206
+ If testing against a local version of DIALS you need to specify the BASE_URL:
207
+
208
+ ```bash
209
+ SECRET_KEY=... BASE_URl=http://localhost:8000 pytest tests
210
+ ```
@@ -1,3 +1,4 @@
1
1
  from .cmsdials import Dials
2
2
 
3
+
3
4
  __all__ = ["Dials"]
@@ -1,10 +1,10 @@
1
1
  import threading
2
2
  from abc import ABC, abstractmethod
3
+ from collections.abc import Mapping
3
4
  from datetime import datetime, timedelta
4
5
  from enum import Enum
5
- from typing import Mapping
6
6
 
7
- from .exceptions import ImpossibleToRefreshToken
7
+ from .exceptions import ImpossibleToRefreshTokenError
8
8
 
9
9
 
10
10
  class TokenState(Enum):
@@ -116,7 +116,7 @@ class BaseCredentials(ABC):
116
116
  # If token is invalid (both token and refresh token expired): Throw error
117
117
  # User need to interactively authenticate again
118
118
  if self.token_state == TokenState.INVALID:
119
- raise ImpossibleToRefreshToken
119
+ raise ImpossibleToRefreshTokenError
120
120
 
121
121
  # If token is stale (token is close to expire)
122
122
  # Re-use the token but launch a new thread to refresh the token
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  import os
3
3
  import os.path
4
+ from collections.abc import Mapping
4
5
  from datetime import datetime
5
- from typing import Mapping, Optional
6
+ from typing import Optional
6
7
 
7
8
  from ..utils._json import TokenDecoder, TokenEncoder
8
9
  from ..utils.logger import logger
@@ -10,6 +11,7 @@ from ._base import BaseCredentials, TokenState
10
11
  from .client import AuthClient
11
12
  from .models import Token
12
13
 
14
+
13
15
  DEFAULT_CACHE_DIR = ".cache"
14
16
  DEFAULT_CACHE_FILENAME = "creds"
15
17
 
@@ -103,7 +105,7 @@ class Credentials(BaseCredentials):
103
105
  client = client or AuthClient()
104
106
  fpath = Credentials._handle_creds_filepath(cache_dir)
105
107
  try:
106
- with open(fpath, "r") as filebuffer:
108
+ with open(fpath) as filebuffer:
107
109
  fcontents = json.load(filebuffer, cls=TokenDecoder)
108
110
  token = Token(**fcontents)
109
111
  except FileNotFoundError:
@@ -14,6 +14,8 @@ class AuthClient(BaseAPIClient):
14
14
  Client for DIALS auth endpoint
15
15
  """
16
16
 
17
+ default_timeout = 30 # seconds
18
+
17
19
  def __init__(self, *args, **kwargs) -> None:
18
20
  super().__init__(*args, **kwargs)
19
21
 
@@ -24,6 +26,7 @@ class AuthClient(BaseAPIClient):
24
26
  headers={
25
27
  "accept": "application/json",
26
28
  },
29
+ timeout=self.default_timeout,
27
30
  )
28
31
  response.raise_for_status()
29
32
  response = response.json()
@@ -38,6 +41,7 @@ class AuthClient(BaseAPIClient):
38
41
  "Content-Type": "application/json",
39
42
  },
40
43
  json={"device_code": device_code},
44
+ timeout=self.default_timeout,
41
45
  )
42
46
 
43
47
  if response.status_code == 400 and "authorization_pending" in response.text:
@@ -63,6 +67,7 @@ class AuthClient(BaseAPIClient):
63
67
  "Authorization": f"{token_type} {access_token}",
64
68
  },
65
69
  json={"refresh_token": refresh_token},
70
+ timeout=self.default_timeout,
66
71
  )
67
72
  response.raise_for_status()
68
73
  response = response.json()
@@ -2,5 +2,5 @@ class PendingAuthorizationError(Exception):
2
2
  pass
3
3
 
4
4
 
5
- class ImpossibleToRefreshToken(Exception):
5
+ class ImpossibleToRefreshTokenError(Exception):
6
6
  pass
@@ -1,4 +1,4 @@
1
- from typing import Mapping
1
+ from collections.abc import Mapping
2
2
 
3
3
  from ._base import BaseCredentials
4
4
 
@@ -0,0 +1,27 @@
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,9 +1,9 @@
1
1
  from datetime import datetime
2
- from typing import List, Optional
2
+ from typing import Optional
3
3
 
4
4
  from pydantic import AnyUrl, BaseModel, Field
5
5
 
6
- from ...utils.base_model import OBaseModel
6
+ from ...utils.base_model import OBaseModel, PaginatedBaseModel
7
7
 
8
8
 
9
9
  class FileIndex(BaseModel):
@@ -18,10 +18,10 @@ class FileIndex(BaseModel):
18
18
  err_trace: Optional[str] = Field(..., max_length=5000)
19
19
 
20
20
 
21
- class PaginatedFileIndexList(BaseModel):
21
+ class PaginatedFileIndexList(PaginatedBaseModel):
22
22
  next: Optional[AnyUrl]
23
23
  previous: Optional[AnyUrl]
24
- results: List[FileIndex]
24
+ results: list[FileIndex]
25
25
 
26
26
 
27
27
  class FileIndexFilters(OBaseModel):
@@ -0,0 +1,27 @@
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,12 +1,11 @@
1
- from typing import List, Optional
1
+ from typing import Optional
2
2
 
3
3
  from pydantic import AnyUrl, BaseModel, Field
4
4
 
5
- from ...utils.base_model import OBaseModel
5
+ from ...utils.base_model import OBaseModel, PaginatedBaseModel
6
6
 
7
7
 
8
8
  class LumisectionHistogram1D(BaseModel):
9
- hist_id: int
10
9
  dataset: str = Field(..., max_length=255)
11
10
  me: str = Field(..., max_length=255)
12
11
  dataset_id: int
@@ -21,10 +20,10 @@ class LumisectionHistogram1D(BaseModel):
21
20
  data: list[float]
22
21
 
23
22
 
24
- class PaginatedLumisectionHistogram1DList(BaseModel):
23
+ class PaginatedLumisectionHistogram1DList(PaginatedBaseModel):
25
24
  next: Optional[AnyUrl]
26
25
  previous: Optional[AnyUrl]
27
- results: List[LumisectionHistogram1D]
26
+ results: list[LumisectionHistogram1D]
28
27
 
29
28
 
30
29
  class LumisectionHistogram1DFilters(OBaseModel):
@@ -0,0 +1,27 @@
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)
@@ -2,11 +2,10 @@ from typing import Optional
2
2
 
3
3
  from pydantic import AnyUrl, BaseModel, Field
4
4
 
5
- from ...utils.base_model import OBaseModel
5
+ from ...utils.base_model import OBaseModel, PaginatedBaseModel
6
6
 
7
7
 
8
8
  class LumisectionHistogram2D(BaseModel):
9
- hist_id: int
10
9
  dataset: str = Field(..., max_length=255)
11
10
  me: str = Field(..., max_length=255)
12
11
  dataset_id: int
@@ -24,7 +23,7 @@ class LumisectionHistogram2D(BaseModel):
24
23
  data: list[list[float]]
25
24
 
26
25
 
27
- class PaginatedLumisectionHistogram2DList(BaseModel):
26
+ class PaginatedLumisectionHistogram2DList(PaginatedBaseModel):
28
27
  next: Optional[AnyUrl]
29
28
  previous: Optional[AnyUrl]
30
29
  results: list[LumisectionHistogram2D]
@@ -15,7 +15,7 @@ class LumisectionClient(BaseAuthorizedAPIClient):
15
15
  def get(self, dataset_id: int, run_number: int, ls_number: int):
16
16
  endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/{ls_number}/"
17
17
  headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers)
18
+ response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
19
 
20
20
  try:
21
21
  response.raise_for_status()
@@ -2,7 +2,7 @@ from typing import Optional
2
2
 
3
3
  from pydantic import AnyUrl, BaseModel, Field
4
4
 
5
- from ...utils.base_model import OBaseModel
5
+ from ...utils.base_model import OBaseModel, PaginatedBaseModel
6
6
 
7
7
 
8
8
  class Lumisection(BaseModel):
@@ -14,7 +14,7 @@ class Lumisection(BaseModel):
14
14
  th2_count: int
15
15
 
16
16
 
17
- class PaginatedLumisectionList(BaseModel):
17
+ class PaginatedLumisectionList(PaginatedBaseModel):
18
18
  next: Optional[AnyUrl]
19
19
  previous: Optional[AnyUrl]
20
20
  results: list[Lumisection]
@@ -0,0 +1,8 @@
1
+ from ...utils.api_client import BaseAuthorizedAPIClient
2
+ from .models import MEFilters, MonitoringElement
3
+
4
+
5
+ class MonitoringElementClient(BaseAuthorizedAPIClient):
6
+ data_model = MonitoringElement
7
+ filter_class = MEFilters
8
+ lookup_url = "mes/"
@@ -0,0 +1,18 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...utils.base_model import OBaseModel
6
+
7
+
8
+ class MonitoringElement(BaseModel):
9
+ me_id: int
10
+ me: str = Field(..., max_length=255)
11
+ count: int
12
+ dim: int
13
+
14
+
15
+ class MEFilters(OBaseModel):
16
+ me: Optional[str] = None
17
+ me__regex: Optional[str] = None
18
+ dim: Optional[int] = None
@@ -15,7 +15,7 @@ class RunClient(BaseAuthorizedAPIClient):
15
15
  def get(self, dataset_id: int, run_number: int):
16
16
  endpoint_url = f"{self.api_url}{self.lookup_url}{dataset_id}/{run_number}/"
17
17
  headers = self._build_headers()
18
- response = requests.get(endpoint_url, headers=headers)
18
+ response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
19
19
 
20
20
  try:
21
21
  response.raise_for_status()
@@ -2,7 +2,7 @@ from typing import Optional
2
2
 
3
3
  from pydantic import AnyUrl, BaseModel, Field
4
4
 
5
- from ...utils.base_model import OBaseModel
5
+ from ...utils.base_model import OBaseModel, PaginatedBaseModel
6
6
 
7
7
 
8
8
  class Run(BaseModel):
@@ -12,7 +12,7 @@ class Run(BaseModel):
12
12
  ls_count: int
13
13
 
14
14
 
15
- class PaginatedRunList(BaseModel):
15
+ class PaginatedRunList(PaginatedBaseModel):
16
16
  next: Optional[AnyUrl]
17
17
  previous: Optional[AnyUrl]
18
18
  results: list[Run]
@@ -5,6 +5,7 @@ from .clients.file_index.client import FileIndexClient
5
5
  from .clients.h1d.client import LumisectionHistogram1DClient
6
6
  from .clients.h2d.client import LumisectionHistogram2DClient
7
7
  from .clients.lumisection.client import LumisectionClient
8
+ from .clients.mes.client import MonitoringElementClient
8
9
  from .clients.run.client import RunClient
9
10
 
10
11
 
@@ -15,3 +16,4 @@ class Dials:
15
16
  self.h2d = LumisectionHistogram2DClient(creds, workspace, *args, **kwargs)
16
17
  self.lumi = LumisectionClient(creds, workspace, *args, **kwargs)
17
18
  self.run = RunClient(creds, workspace, *args, **kwargs)
19
+ self.mes = MonitoringElementClient(creds, workspace, *args, **kwargs)
@@ -2,12 +2,15 @@ from .clients.file_index.models import FileIndexFilters
2
2
  from .clients.h1d.models import LumisectionHistogram1DFilters
3
3
  from .clients.h2d.models import LumisectionHistogram2DFilters
4
4
  from .clients.lumisection.models import LumisectionFilters
5
+ from .clients.mes.models import MEFilters
5
6
  from .clients.run.models import RunFilters
6
7
 
8
+
7
9
  __all__ = [
8
10
  "FileIndexFilters",
9
11
  "LumisectionHistogram1DFilters",
10
12
  "LumisectionHistogram2DFilters",
11
13
  "LumisectionFilters",
12
14
  "RunFilters",
15
+ "MEFilters",
13
16
  ]
File without changes
@@ -1,3 +1,4 @@
1
+ from importlib import util as importlib_util
1
2
  from typing import Optional
2
3
  from urllib.parse import parse_qs, urlparse
3
4
 
@@ -8,6 +9,14 @@ from ..auth._base import BaseCredentials
8
9
  from ..utils.logger import logger
9
10
 
10
11
 
12
+ if importlib_util.find_spec("tqdm"):
13
+ from tqdm.auto import tqdm
14
+
15
+ TQDM_INSTALLED = True
16
+ else:
17
+ TQDM_INSTALLED = False
18
+
19
+
11
20
  class BaseAPIClient:
12
21
  PRODUCTION_BASE_URL = "https://cmsdials-api.web.cern.ch/"
13
22
  PRODUCTION_API_ROUTE = "api/"
@@ -36,6 +45,7 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
36
45
  pagination_model = None
37
46
  filter_class = None
38
47
  lookup_url = None
48
+ default_timeout = 30 # seconds
39
49
 
40
50
  def __init__(
41
51
  self,
@@ -55,10 +65,10 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
55
65
  self.creds.before_request(base)
56
66
  return base
57
67
 
58
- def get(self, id: int):
68
+ def get(self, id: int): # noqa: A002
59
69
  endpoint_url = self.api_url + self.lookup_url + str(id) + "/"
60
70
  headers = self._build_headers()
61
- response = requests.get(endpoint_url, headers=headers)
71
+ response = requests.get(endpoint_url, headers=headers, timeout=self.default_timeout)
62
72
 
63
73
  try:
64
74
  response.raise_for_status()
@@ -73,7 +83,7 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
73
83
  filters = filters or self.filter_class()
74
84
  endpoint_url = self.api_url + self.lookup_url
75
85
  headers = self._build_headers()
76
- response = requests.get(endpoint_url, headers=headers, params=filters.cleandict())
86
+ response = requests.get(endpoint_url, headers=headers, params=filters.cleandict(), timeout=self.default_timeout)
77
87
 
78
88
  try:
79
89
  response.raise_for_status()
@@ -82,13 +92,22 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
82
92
  raise err
83
93
 
84
94
  response = response.json()
85
- return self.pagination_model(**response)
95
+ if self.pagination_model:
96
+ return self.pagination_model(**response)
97
+ elif isinstance(response, list):
98
+ return [self.data_model(**res) for res in response]
86
99
 
87
- def __list_sync(self, filters, max_pages: Optional[int] = None):
100
+ raise ValueError("pagination model is None and response is not a list.")
101
+
102
+ def __list_sync(self, filters, max_pages: Optional[int] = None, enable_progress: bool = False):
88
103
  next_token = None
89
104
  results = []
90
105
  is_last_page = False
91
106
  total_pages = 0
107
+ use_tqdm = TQDM_INSTALLED and enable_progress
108
+
109
+ if use_tqdm:
110
+ progress = tqdm(desc="Progress", total=1)
92
111
 
93
112
  while is_last_page is False:
94
113
  curr_filters = self.filter_class(**filters.dict())
@@ -98,12 +117,26 @@ class BaseAuthorizedAPIClient(BaseAPIClient):
98
117
  is_last_page = response.next is None
99
118
  next_token = parse_qs(urlparse(response.next).query).get("next_token") if response.next else None
100
119
  total_pages += 1
101
- if max_pages and total_pages > max_pages:
120
+ max_pages_reached = max_pages and total_pages >= max_pages
121
+ if use_tqdm:
122
+ if is_last_page or max_pages_reached:
123
+ progress.update()
124
+ else:
125
+ progress.total = total_pages + 1
126
+ progress.update(1)
127
+ if max_pages_reached:
102
128
  break
103
129
 
130
+ if use_tqdm:
131
+ progress.close()
132
+
104
133
  return self.pagination_model(
105
- next=None, previous=None, results=results # No problem re-using last response count
134
+ next=None,
135
+ previous=None,
136
+ results=results, # No problem re-using last response count
106
137
  )
107
138
 
108
- def list_all(self, filters, max_pages: Optional[int] = None):
109
- return self.__list_sync(filters, max_pages)
139
+ def list_all(self, filters, max_pages: Optional[int] = None, **kwargs):
140
+ if self.pagination_model is None:
141
+ return self.list(filters)
142
+ return self.__list_sync(filters, max_pages, **kwargs)
@@ -0,0 +1,25 @@
1
+ from importlib import util as importlib_util
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ if importlib_util.find_spec("pandas"):
7
+ import pandas as pd
8
+
9
+ PANDAS_NOT_INSTALLED = False
10
+ else:
11
+ PANDAS_NOT_INSTALLED = True
12
+
13
+
14
+ class OBaseModel(BaseModel):
15
+ def cleandict(self):
16
+ return {key: value for key, value in self.dict().items() if value is not None}
17
+
18
+
19
+ class PaginatedBaseModel(BaseModel):
20
+ def to_pandas(self):
21
+ if PANDAS_NOT_INSTALLED:
22
+ raise RuntimeError(
23
+ "The 'pandas' package is not installed, you can re-install cmsdials specifying the pandas extra: pip install cmsdials[pandas]"
24
+ )
25
+ return pd.DataFrame([res.__dict__ for res in self.results])
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
 
3
+
3
4
  logging.basicConfig(format="[%(asctime)s] %(levelname)s: %(message)s", level=logging.INFO)
4
5
 
5
6
  logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cmsdials"
3
- version = "1.0.1"
3
+ version = "1.2.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"
@@ -21,10 +21,18 @@ python = "^3.9"
21
21
  requests = "^2.31.0"
22
22
  pydantic = "<2, >=1"
23
23
  typing-extensions = "<4.6.0, >=3.6.6"
24
+ pandas = {version = ">=1.5.3", optional = true}
25
+ tqdm = {version = ">=4.66.1", optional = true}
24
26
 
25
27
  [tool.poetry.group.dev.dependencies]
26
28
  pre-commit = "^3.6.2"
27
29
  ruff = "^0.4.2"
30
+ notebook = "^7.1.3"
31
+ pytest = "^8.2.0"
32
+
33
+ [tool.poetry.extras]
34
+ pandas = ["pandas"]
35
+ tqdm = ["tqdm"]
28
36
 
29
37
  [tool.ruff]
30
38
  exclude = [
@@ -52,7 +60,7 @@ exclude = [
52
60
  ]
53
61
  line-length = 120
54
62
  indent-width = 4
55
- target-version = "py310"
63
+ target-version = "py39"
56
64
 
57
65
  [tool.ruff.lint]
58
66
  select = [
@@ -1,9 +0,0 @@
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/"
@@ -1,9 +0,0 @@
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/"
@@ -1,9 +0,0 @@
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/"
@@ -1,6 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
-
4
- class OBaseModel(BaseModel):
5
- def cleandict(self):
6
- return {key: value for key, value in self.dict().items() if value is not None}
File without changes