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.
- {cmsdials-1.0.1 → cmsdials-1.2.0}/PKG-INFO +58 -8
- {cmsdials-1.0.1 → cmsdials-1.2.0}/README.md +53 -7
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/__init__.py +1 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/_base.py +3 -3
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/bearer.py +4 -2
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/client.py +5 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/exceptions.py +1 -1
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/secret_key.py +1 -1
- cmsdials-1.2.0/cmsdials/clients/file_index/client.py +27 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/file_index/models.py +4 -4
- cmsdials-1.2.0/cmsdials/clients/h1d/client.py +27 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h1d/models.py +4 -5
- cmsdials-1.2.0/cmsdials/clients/h2d/client.py +27 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h2d/models.py +2 -3
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/client.py +1 -1
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/models.py +2 -2
- cmsdials-1.2.0/cmsdials/clients/mes/client.py +8 -0
- cmsdials-1.2.0/cmsdials/clients/mes/models.py +18 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/run/client.py +1 -1
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/run/models.py +2 -2
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/cmsdials.py +2 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/filters.py +3 -0
- cmsdials-1.2.0/cmsdials/utils/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/utils/api_client.py +42 -9
- cmsdials-1.2.0/cmsdials/utils/base_model.py +25 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/utils/logger.py +1 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/pyproject.toml +10 -2
- cmsdials-1.0.1/cmsdials/clients/file_index/client.py +0 -9
- cmsdials-1.0.1/cmsdials/clients/h1d/client.py +0 -9
- cmsdials-1.0.1/cmsdials/clients/h2d/client.py +0 -9
- cmsdials-1.0.1/cmsdials/utils/base_model.py +0 -6
- {cmsdials-1.0.1 → cmsdials-1.2.0}/LICENSE +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/auth/models.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/file_index/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h1d/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/h2d/__init__.py +0 -0
- {cmsdials-1.0.1 → cmsdials-1.2.0}/cmsdials/clients/lumisection/__init__.py +0 -0
- {cmsdials-1.0.1/cmsdials/clients/run → cmsdials-1.2.0/cmsdials/clients/mes}/__init__.py +0 -0
- {cmsdials-1.0.1/cmsdials/utils → cmsdials-1.2.0/cmsdials/clients/run}/__init__.py +0 -0
- {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
|
|
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(
|
|
101
|
-
dials.h1d.get(
|
|
102
|
-
dials.
|
|
103
|
-
dials.lumi.get(dataset_id=14677060, run_number=
|
|
104
|
-
dials.run.get(dataset_id=14677060, run_number=
|
|
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
|
|
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.
|
|
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(
|
|
78
|
-
dials.h1d.get(
|
|
79
|
-
dials.
|
|
80
|
-
dials.lumi.get(dataset_id=14677060, run_number=
|
|
81
|
-
dials.run.get(dataset_id=14677060, run_number=
|
|
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
|
|
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.
|
|
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,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
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
@@ -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
|
|
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(
|
|
21
|
+
class PaginatedFileIndexList(PaginatedBaseModel):
|
|
22
22
|
next: Optional[AnyUrl]
|
|
23
23
|
previous: Optional[AnyUrl]
|
|
24
|
-
results:
|
|
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
|
|
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(
|
|
23
|
+
class PaginatedLumisectionHistogram1DList(PaginatedBaseModel):
|
|
25
24
|
next: Optional[AnyUrl]
|
|
26
25
|
previous: Optional[AnyUrl]
|
|
27
|
-
results:
|
|
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(
|
|
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(
|
|
17
|
+
class PaginatedLumisectionList(PaginatedBaseModel):
|
|
18
18
|
next: Optional[AnyUrl]
|
|
19
19
|
previous: Optional[AnyUrl]
|
|
20
20
|
results: list[Lumisection]
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cmsdials"
|
|
3
|
-
version = "1.0
|
|
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 = "
|
|
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/"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|