cmsdials 2.0.0rc4__tar.gz → 2.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-2.0.0rc4 → cmsdials-2.2.0}/.gitignore +0 -3
- cmsdials-2.2.0/.gitlab-ci.yml +125 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.pre-commit-config.yaml +1 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/PKG-INFO +49 -14
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/README.md +48 -13
- cmsdials-2.2.0/cmsdials/_version.py +24 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/_base.py +9 -2
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/bearer.py +39 -5
- cmsdials-2.2.0/cmsdials/auth/client.py +198 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/models.py +12 -0
- cmsdials-2.2.0/cmsdials/auth/secret_key.py +65 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/client.py +5 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/client.py +13 -2
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/models.py +3 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/client.py +13 -2
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/models.py +3 -1
- cmsdials-2.2.0/cmsdials/clients/ml_bad_lumisection/client.py +74 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/client.py +5 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/client.py +18 -4
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/workspace/client.py +4 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/cmsdials.py +22 -6
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/_json.py +3 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/api_client.py +107 -13
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/base_model.py +9 -3
- cmsdials-2.2.0/cmsdials/utils/logger.py +8 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/PKG-INFO +49 -14
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/SOURCES.txt +4 -0
- cmsdials-2.2.0/cmsdials.egg-info/scm_file_list.json +82 -0
- cmsdials-2.2.0/cmsdials.egg-info/scm_version.json +8 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/mypy.ini +1 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/pyproject.toml +1 -1
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/ruff.toml +1 -1
- cmsdials-2.2.0/tests/integration/env.py +25 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_auth_client.py +33 -9
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_file_index_client.py +15 -4
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_h1d_client.py +15 -3
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_h2d_client.py +15 -3
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_lumisection_client.py +14 -3
- cmsdials-2.2.0/tests/integration/test_oms_proxy_client.py +60 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_resume.py +12 -4
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_run_client.py +9 -3
- cmsdials-2.2.0/tests/integration/utils.py +23 -0
- cmsdials-2.2.0/tests/unit/test_auth.py +265 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tox.ini +1 -1
- cmsdials-2.2.0/uv.lock +2571 -0
- cmsdials-2.0.0rc4/.gitlab-ci.yml +0 -74
- cmsdials-2.0.0rc4/cmsdials/_version.py +0 -34
- cmsdials-2.0.0rc4/cmsdials/auth/client.py +0 -104
- cmsdials-2.0.0rc4/cmsdials/auth/secret_key.py +0 -42
- cmsdials-2.0.0rc4/cmsdials/clients/ml_bad_lumisection/client.py +0 -41
- cmsdials-2.0.0rc4/cmsdials/utils/logger.py +0 -6
- cmsdials-2.0.0rc4/tests/integration/env.py +0 -15
- cmsdials-2.0.0rc4/tests/integration/test_oms_proxy_client.py +0 -38
- cmsdials-2.0.0rc4/tests/integration/utils.py +0 -9
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.coveragerc +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.editorconfig +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.mailmap +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.tool-versions +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/LICENSE +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/exceptions.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/client.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/client.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/client.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_bad_lumisection/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_bad_lumisection/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/client.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/models.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/workspace/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/filters.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/dependency_links.txt +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/requires.txt +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/top_level.txt +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/pytest.ini +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/setup.cfg +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_mes_client.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/unit/__init__.py +0 -0
- {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/unit/test_base_api_client.py +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
include:
|
|
2
|
+
- template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml
|
|
3
|
+
- template: Security/Secret-Detection.gitlab-ci.yml
|
|
4
|
+
- template: Security/SAST.gitlab-ci.yml
|
|
5
|
+
- project: "cms-ppd/technical-support/ci-templates"
|
|
6
|
+
ref: release/v2
|
|
7
|
+
file:
|
|
8
|
+
- "templates/python/uv_build.yml"
|
|
9
|
+
- "templates/python/uv_publish.yml"
|
|
10
|
+
- "templates/gitlab/create_release.yml"
|
|
11
|
+
- "templates/python/uv_ruff.yml"
|
|
12
|
+
- "templates/python/uv_mypy.yml"
|
|
13
|
+
- "templates/gitlab/mr_quality_comment.yml"
|
|
14
|
+
|
|
15
|
+
stages:
|
|
16
|
+
- quality
|
|
17
|
+
- test
|
|
18
|
+
- build
|
|
19
|
+
- publish
|
|
20
|
+
- create_release
|
|
21
|
+
|
|
22
|
+
#################################################
|
|
23
|
+
# Run lint tools and export patch in case of failures
|
|
24
|
+
#################################################
|
|
25
|
+
|
|
26
|
+
secret_detection:
|
|
27
|
+
rules:
|
|
28
|
+
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
|
29
|
+
|
|
30
|
+
semgrep-sast:
|
|
31
|
+
rules:
|
|
32
|
+
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
|
33
|
+
|
|
34
|
+
lint:
|
|
35
|
+
extends: .uv_ruff_lint
|
|
36
|
+
variables:
|
|
37
|
+
PYTHON_VERSION: '3.9'
|
|
38
|
+
rules:
|
|
39
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
40
|
+
|
|
41
|
+
static_check:
|
|
42
|
+
extends: .uv_mypy_check
|
|
43
|
+
variables:
|
|
44
|
+
PYTHON_VERSION: '3.9'
|
|
45
|
+
rules:
|
|
46
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
47
|
+
|
|
48
|
+
quality_comment:
|
|
49
|
+
extends: .mr_quality_comment
|
|
50
|
+
rules:
|
|
51
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
52
|
+
when: always
|
|
53
|
+
needs:
|
|
54
|
+
- job: lint
|
|
55
|
+
artifacts: true
|
|
56
|
+
optional: true
|
|
57
|
+
- job: static_check
|
|
58
|
+
artifacts: true
|
|
59
|
+
optional: true
|
|
60
|
+
|
|
61
|
+
###########
|
|
62
|
+
# Run tests
|
|
63
|
+
###########
|
|
64
|
+
|
|
65
|
+
test_matrix:
|
|
66
|
+
stage: test
|
|
67
|
+
parallel:
|
|
68
|
+
matrix:
|
|
69
|
+
- PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12"]
|
|
70
|
+
image: registry.cern.ch/docker.io/library/python:$PYTHON_VERSION
|
|
71
|
+
before_script:
|
|
72
|
+
- python -m pip install --upgrade pip
|
|
73
|
+
- pip install uv
|
|
74
|
+
- uv sync --all-groups --extra pandas --extra tqdm
|
|
75
|
+
script:
|
|
76
|
+
- uv run pytest tests
|
|
77
|
+
coverage: "/TOTAL.+ ([0-9]{1,3}%)/"
|
|
78
|
+
artifacts:
|
|
79
|
+
reports:
|
|
80
|
+
coverage_report:
|
|
81
|
+
coverage_format: cobertura
|
|
82
|
+
path: coverage.xml
|
|
83
|
+
variables:
|
|
84
|
+
SECRET_KEY: "$SECRET_KEY"
|
|
85
|
+
# Application owning SECRET_KEY, enables the api-access token tests
|
|
86
|
+
CLIENT_ID: "cms-dials-dev-api-client-test"
|
|
87
|
+
rules:
|
|
88
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
89
|
+
|
|
90
|
+
###############
|
|
91
|
+
# Build package
|
|
92
|
+
###############
|
|
93
|
+
|
|
94
|
+
build:
|
|
95
|
+
extends: .uv_build
|
|
96
|
+
variables:
|
|
97
|
+
UV_SYNC_FLAGS: "--all-groups --extra pandas --extra tqdm"
|
|
98
|
+
rules:
|
|
99
|
+
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
100
|
+
- if: '$CI_COMMIT_TAG'
|
|
101
|
+
needs:
|
|
102
|
+
- job: test_matrix
|
|
103
|
+
optional: true
|
|
104
|
+
|
|
105
|
+
#################
|
|
106
|
+
# Publish to PyPI
|
|
107
|
+
#################
|
|
108
|
+
|
|
109
|
+
publish:
|
|
110
|
+
extends: .uv_publish
|
|
111
|
+
rules:
|
|
112
|
+
- if: $CI_COMMIT_TAG
|
|
113
|
+
needs:
|
|
114
|
+
- build
|
|
115
|
+
|
|
116
|
+
##########################
|
|
117
|
+
# Create release in GitLab
|
|
118
|
+
##########################
|
|
119
|
+
|
|
120
|
+
create_release:
|
|
121
|
+
extends: .create_gitlab_release
|
|
122
|
+
rules:
|
|
123
|
+
- if: $CI_COMMIT_TAG
|
|
124
|
+
needs:
|
|
125
|
+
- publish
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmsdials
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The Python api client interface to DIALS service
|
|
5
5
|
Author-email: Gabriel Moreira <gabriel.moreira@cern.ch>
|
|
6
6
|
Project-URL: Repository, https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py
|
|
@@ -21,9 +21,9 @@ Provides-Extra: tqdm
|
|
|
21
21
|
Requires-Dist: tqdm>=4.66.1; extra == "tqdm"
|
|
22
22
|
Dynamic: license-file
|
|
23
23
|
|
|
24
|
-

|
|
25
|
+

|
|
26
|
+

|
|
27
27
|
[](https://badge.fury.io/py/cmsdials)
|
|
28
28
|
|
|
29
29
|
# dials-py
|
|
@@ -46,16 +46,29 @@ pandas, tqdm
|
|
|
46
46
|
|
|
47
47
|
## Usage
|
|
48
48
|
|
|
49
|
-
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
|
|
49
|
+
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 id and secret` of an application granted API access 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.
|
|
50
|
+
|
|
51
|
+
Credentials are issued by CERN's Keycloak directly, dials-py never sends your password or client secret to DIALS.
|
|
50
52
|
|
|
51
53
|
### Generating credentials with client secret
|
|
52
54
|
|
|
53
55
|
```python
|
|
54
56
|
from cmsdials.auth.secret_key import Credentials
|
|
55
57
|
|
|
56
|
-
creds = Credentials(token=".....")
|
|
58
|
+
creds = Credentials(client_id="my-application", token=".....")
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
The token is renewed automatically before it expires.
|
|
62
|
+
|
|
63
|
+
To get API access:
|
|
64
|
+
|
|
65
|
+
1. Register an application in the [CERN Application Portal](https://application-portal.web.cern.ch) and enable "My application will need to get tokens using its own client ID and secret".
|
|
66
|
+
2. Ask the DIALS team to grant API access to the application (it is added to the DIALS API access group).
|
|
67
|
+
3. Use the application's client id and secret as shown above.
|
|
68
|
+
|
|
69
|
+
> [!WARNING]
|
|
70
|
+
> `Credentials(token=".....")` without `client_id` authenticates with the `X-CLIENT-SECRET` header, which is deprecated and emits a `DeprecationWarning`. DIALS will stop accepting it, so pass the `client_id` of your application.
|
|
71
|
+
|
|
59
72
|
### Generating credentials with device
|
|
60
73
|
|
|
61
74
|
#### Loading from AuthClient
|
|
@@ -298,9 +311,11 @@ You may need to query the `ml_models_index` client to fetch the models ids you a
|
|
|
298
311
|
|
|
299
312
|
Attention: Those endpoints doesn't return a Pydantic model, instead they are returning a plain json response. Consequently, the method `to_pandas` doesn't work on them.
|
|
300
313
|
|
|
301
|
-
## Usage with
|
|
314
|
+
## Usage with other DIALS instances
|
|
315
|
+
|
|
316
|
+
All classes that interface the DIALS service inherit the class `BaseAPIClient`, which propagates the `base_url`, `route` and `version` attributes with production values. In order to use dials-py with another DIALS instance it is possible to overwrite those attributes when instantiating the `Dials` client.
|
|
302
317
|
|
|
303
|
-
|
|
318
|
+
The `AuthClient` issues tokens for the production DIALS applications by default. For another instance, overwrite the Keycloak public application (`client_id`) and the DIALS application the tokens are issued for (`audience`). The audience must be the `DJANGO_KEYCLOAK_CLIENT_ID` configured in that DIALS backend, and that application must allow token exchange from the public application. For example, with a local instance:
|
|
304
319
|
|
|
305
320
|
```python
|
|
306
321
|
from cmsdials.auth.client import AuthClient
|
|
@@ -308,16 +323,36 @@ from cmsdials.auth.bearer import Credentials
|
|
|
308
323
|
from cmsdials import Dials
|
|
309
324
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
310
325
|
|
|
311
|
-
|
|
312
|
-
|
|
326
|
+
CLIENT_ID = "<your_public_application>"
|
|
327
|
+
AUDIENCE = "<your_confidential_application>"
|
|
328
|
+
CACHE_DIR = ".cache-local"
|
|
329
|
+
DIALS_URL = "http://localhost:8000/"
|
|
313
330
|
|
|
314
|
-
auth = AuthClient(
|
|
315
|
-
creds = Credentials.from_creds_file(cache_dir=
|
|
316
|
-
dials = Dials(creds, base_url=
|
|
331
|
+
auth = AuthClient(client_id=CLIENT_ID, audience=AUDIENCE)
|
|
332
|
+
creds = Credentials.from_creds_file(cache_dir=CACHE_DIR, client=auth) # Make sure to specify the auth client with overwritten values, using another cache_dir is recommended
|
|
333
|
+
dials = Dials(creds, base_url=DIALS_URL)
|
|
317
334
|
|
|
318
335
|
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)
|
|
319
336
|
```
|
|
320
337
|
|
|
338
|
+
When authenticating with a client secret, pass the same `AuthClient` so the token is issued for that instance:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from cmsdials.auth.secret_key import Credentials as SecretKeyCredentials
|
|
342
|
+
|
|
343
|
+
creds = SecretKeyCredentials(client_id="my-application", token=".....", client=auth)
|
|
344
|
+
dials = Dials(creds, base_url=DIALS_URL)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
To connect to the development instance of DIALS, use these values in the examples above:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
CLIENT_ID = "cms-dials-dev-public-app"
|
|
351
|
+
AUDIENCE = "cms-dials-dev-confidential-app"
|
|
352
|
+
CACHE_DIR = ".cache-dev"
|
|
353
|
+
DIALS_URL = "https://dev-cmsdials-api.web.cern.ch/"
|
|
354
|
+
```
|
|
355
|
+
|
|
321
356
|
## Development
|
|
322
357
|
|
|
323
358
|
Install the dependencies and the package using `uv`:
|
|
@@ -336,7 +371,7 @@ The repository has some tests written to make sure DIALS responses are compatibl
|
|
|
336
371
|
SECRET_KEY=... uv run pytest tests
|
|
337
372
|
```
|
|
338
373
|
|
|
339
|
-
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 [
|
|
374
|
+
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. Set `CLIENT_ID` to the client id of the same application to test the api-access token flow (`SECRET_KEY=... CLIENT_ID=... uv run pytest tests`); otherwise those tests are skipped and the deprecated `X-CLIENT-SECRET` header is used. Tests target the dev Keycloak applications by default, overwrite them with `AUTH_CLIENT_ID` and `AUTH_AUDIENCE`. The interactive authentication flow should be tested manually, an example for this can be found in [test_auth_client.py](/tests/integration/test_auth_client.py).
|
|
340
375
|
|
|
341
376
|
If testing against a local version of DIALS you need to specify the BASE_URL:
|
|
342
377
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-

|
|
2
|
+

|
|
3
|
+

|
|
4
4
|
[](https://badge.fury.io/py/cmsdials)
|
|
5
5
|
|
|
6
6
|
# dials-py
|
|
@@ -23,16 +23,29 @@ pandas, tqdm
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
-
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
|
|
26
|
+
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 id and secret` of an application granted API access 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.
|
|
27
|
+
|
|
28
|
+
Credentials are issued by CERN's Keycloak directly, dials-py never sends your password or client secret to DIALS.
|
|
27
29
|
|
|
28
30
|
### Generating credentials with client secret
|
|
29
31
|
|
|
30
32
|
```python
|
|
31
33
|
from cmsdials.auth.secret_key import Credentials
|
|
32
34
|
|
|
33
|
-
creds = Credentials(token=".....")
|
|
35
|
+
creds = Credentials(client_id="my-application", token=".....")
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
The token is renewed automatically before it expires.
|
|
39
|
+
|
|
40
|
+
To get API access:
|
|
41
|
+
|
|
42
|
+
1. Register an application in the [CERN Application Portal](https://application-portal.web.cern.ch) and enable "My application will need to get tokens using its own client ID and secret".
|
|
43
|
+
2. Ask the DIALS team to grant API access to the application (it is added to the DIALS API access group).
|
|
44
|
+
3. Use the application's client id and secret as shown above.
|
|
45
|
+
|
|
46
|
+
> [!WARNING]
|
|
47
|
+
> `Credentials(token=".....")` without `client_id` authenticates with the `X-CLIENT-SECRET` header, which is deprecated and emits a `DeprecationWarning`. DIALS will stop accepting it, so pass the `client_id` of your application.
|
|
48
|
+
|
|
36
49
|
### Generating credentials with device
|
|
37
50
|
|
|
38
51
|
#### Loading from AuthClient
|
|
@@ -275,9 +288,11 @@ You may need to query the `ml_models_index` client to fetch the models ids you a
|
|
|
275
288
|
|
|
276
289
|
Attention: Those endpoints doesn't return a Pydantic model, instead they are returning a plain json response. Consequently, the method `to_pandas` doesn't work on them.
|
|
277
290
|
|
|
278
|
-
## Usage with
|
|
291
|
+
## Usage with other DIALS instances
|
|
292
|
+
|
|
293
|
+
All classes that interface the DIALS service inherit the class `BaseAPIClient`, which propagates the `base_url`, `route` and `version` attributes with production values. In order to use dials-py with another DIALS instance it is possible to overwrite those attributes when instantiating the `Dials` client.
|
|
279
294
|
|
|
280
|
-
|
|
295
|
+
The `AuthClient` issues tokens for the production DIALS applications by default. For another instance, overwrite the Keycloak public application (`client_id`) and the DIALS application the tokens are issued for (`audience`). The audience must be the `DJANGO_KEYCLOAK_CLIENT_ID` configured in that DIALS backend, and that application must allow token exchange from the public application. For example, with a local instance:
|
|
281
296
|
|
|
282
297
|
```python
|
|
283
298
|
from cmsdials.auth.client import AuthClient
|
|
@@ -285,16 +300,36 @@ from cmsdials.auth.bearer import Credentials
|
|
|
285
300
|
from cmsdials import Dials
|
|
286
301
|
from cmsdials.filters import LumisectionHistogram2DFilters
|
|
287
302
|
|
|
288
|
-
|
|
289
|
-
|
|
303
|
+
CLIENT_ID = "<your_public_application>"
|
|
304
|
+
AUDIENCE = "<your_confidential_application>"
|
|
305
|
+
CACHE_DIR = ".cache-local"
|
|
306
|
+
DIALS_URL = "http://localhost:8000/"
|
|
290
307
|
|
|
291
|
-
auth = AuthClient(
|
|
292
|
-
creds = Credentials.from_creds_file(cache_dir=
|
|
293
|
-
dials = Dials(creds, base_url=
|
|
308
|
+
auth = AuthClient(client_id=CLIENT_ID, audience=AUDIENCE)
|
|
309
|
+
creds = Credentials.from_creds_file(cache_dir=CACHE_DIR, client=auth) # Make sure to specify the auth client with overwritten values, using another cache_dir is recommended
|
|
310
|
+
dials = Dials(creds, base_url=DIALS_URL)
|
|
294
311
|
|
|
295
312
|
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)
|
|
296
313
|
```
|
|
297
314
|
|
|
315
|
+
When authenticating with a client secret, pass the same `AuthClient` so the token is issued for that instance:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from cmsdials.auth.secret_key import Credentials as SecretKeyCredentials
|
|
319
|
+
|
|
320
|
+
creds = SecretKeyCredentials(client_id="my-application", token=".....", client=auth)
|
|
321
|
+
dials = Dials(creds, base_url=DIALS_URL)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
To connect to the development instance of DIALS, use these values in the examples above:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
CLIENT_ID = "cms-dials-dev-public-app"
|
|
328
|
+
AUDIENCE = "cms-dials-dev-confidential-app"
|
|
329
|
+
CACHE_DIR = ".cache-dev"
|
|
330
|
+
DIALS_URL = "https://dev-cmsdials-api.web.cern.ch/"
|
|
331
|
+
```
|
|
332
|
+
|
|
298
333
|
## Development
|
|
299
334
|
|
|
300
335
|
Install the dependencies and the package using `uv`:
|
|
@@ -313,7 +348,7 @@ The repository has some tests written to make sure DIALS responses are compatibl
|
|
|
313
348
|
SECRET_KEY=... uv run pytest tests
|
|
314
349
|
```
|
|
315
350
|
|
|
316
|
-
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 [
|
|
351
|
+
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. Set `CLIENT_ID` to the client id of the same application to test the api-access token flow (`SECRET_KEY=... CLIENT_ID=... uv run pytest tests`); otherwise those tests are skipped and the deprecated `X-CLIENT-SECRET` header is used. Tests target the dev Keycloak applications by default, overwrite them with `AUTH_CLIENT_ID` and `AUTH_AUDIENCE`. The interactive authentication flow should be tested manually, an example for this can be found in [test_auth_client.py](/tests/integration/test_auth_client.py).
|
|
317
352
|
|
|
318
353
|
If testing against a local version of DIALS you need to specify the BASE_URL:
|
|
319
354
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g395406611'
|
|
@@ -45,7 +45,11 @@ class BaseCredentials(ABC):
|
|
|
45
45
|
"""
|
|
46
46
|
if not self.expires_at:
|
|
47
47
|
return False
|
|
48
|
-
return
|
|
48
|
+
return (
|
|
49
|
+
self.expires_at
|
|
50
|
+
> datetime.now()
|
|
51
|
+
> (self.expires_at - timedelta(seconds=0.1 * self.expires_in))
|
|
52
|
+
)
|
|
49
53
|
|
|
50
54
|
@property
|
|
51
55
|
def expired(self) -> bool:
|
|
@@ -119,7 +123,10 @@ class BaseCredentials(ABC):
|
|
|
119
123
|
|
|
120
124
|
# If token is stale (token is close to expire)
|
|
121
125
|
# Re-use the token but launch a new thread to refresh the token
|
|
122
|
-
if
|
|
126
|
+
if (
|
|
127
|
+
self.token_state == TokenState.STALE
|
|
128
|
+
and self._stale_lock.locked() is False
|
|
129
|
+
):
|
|
123
130
|
worker = threading.Thread(target=self._nonblocking_refresh)
|
|
124
131
|
worker.start()
|
|
125
132
|
|
|
@@ -4,6 +4,8 @@ import os.path
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
+
from requests.exceptions import HTTPError
|
|
8
|
+
|
|
7
9
|
from ..utils._json import TokenDecoder, TokenEncoder
|
|
8
10
|
from ..utils.logger import logger
|
|
9
11
|
from ._base import BaseCredentials, TokenState
|
|
@@ -53,7 +55,9 @@ class Credentials(BaseCredentials):
|
|
|
53
55
|
|
|
54
56
|
# If loaded token is invalid, trigger authentication
|
|
55
57
|
if self.token_state == TokenState.INVALID:
|
|
56
|
-
logger.info(
|
|
58
|
+
logger.info(
|
|
59
|
+
"Access token and refresh token are expired, triggering device authentication flow..."
|
|
60
|
+
)
|
|
57
61
|
_token = self.client.device_auth_flow()
|
|
58
62
|
self._set_token(_token)
|
|
59
63
|
|
|
@@ -84,7 +88,9 @@ class Credentials(BaseCredentials):
|
|
|
84
88
|
|
|
85
89
|
@staticmethod
|
|
86
90
|
def from_authclient_token(
|
|
87
|
-
token: Token,
|
|
91
|
+
token: Token,
|
|
92
|
+
cache_dir: Optional[str] = None,
|
|
93
|
+
client: Optional[AuthClient] = None,
|
|
88
94
|
) -> "Credentials":
|
|
89
95
|
client = client or AuthClient()
|
|
90
96
|
cache_dir = cache_dir or DEFAULT_CACHE_DIR
|
|
@@ -101,7 +107,9 @@ class Credentials(BaseCredentials):
|
|
|
101
107
|
)
|
|
102
108
|
|
|
103
109
|
@staticmethod
|
|
104
|
-
def from_creds_file(
|
|
110
|
+
def from_creds_file(
|
|
111
|
+
cache_dir: Optional[str] = None, client: Optional[AuthClient] = None
|
|
112
|
+
) -> "Credentials":
|
|
105
113
|
client = client or AuthClient()
|
|
106
114
|
cache_dir = cache_dir or DEFAULT_CACHE_DIR
|
|
107
115
|
fpath = Credentials._handle_creds_filepath(cache_dir)
|
|
@@ -110,7 +118,9 @@ class Credentials(BaseCredentials):
|
|
|
110
118
|
fcontents = json.load(filebuffer, cls=TokenDecoder)
|
|
111
119
|
token = Token(**fcontents)
|
|
112
120
|
except FileNotFoundError:
|
|
113
|
-
logger.info(
|
|
121
|
+
logger.info(
|
|
122
|
+
"Credentials file not found, triggering device authentication flow..."
|
|
123
|
+
)
|
|
114
124
|
token = client.device_auth_flow()
|
|
115
125
|
|
|
116
126
|
return Credentials.from_authclient_token(token, cache_dir, client)
|
|
@@ -129,8 +139,32 @@ class Credentials(BaseCredentials):
|
|
|
129
139
|
with open(fpath, "w") as filebuffer:
|
|
130
140
|
json.dump(self._token.model_dump(), filebuffer, cls=TokenEncoder)
|
|
131
141
|
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _is_invalid_grant(err: HTTPError) -> bool:
|
|
144
|
+
if err.response is None or err.response.status_code != 400:
|
|
145
|
+
return False
|
|
146
|
+
try:
|
|
147
|
+
return err.response.json().get("error") == "invalid_grant"
|
|
148
|
+
except ValueError:
|
|
149
|
+
return False
|
|
150
|
+
|
|
132
151
|
def refresh(self) -> None:
|
|
133
|
-
|
|
152
|
+
try:
|
|
153
|
+
token: Token = self.client.refresh_token(
|
|
154
|
+
self.token_type, self.token, self.refresh_token
|
|
155
|
+
)
|
|
156
|
+
except HTTPError as err:
|
|
157
|
+
# Keycloak only accepts a refresh token from the client it was
|
|
158
|
+
# issued to. Credentials cached by releases that authenticated
|
|
159
|
+
# through DIALS hold refresh tokens of the DIALS confidential
|
|
160
|
+
# application, not the public one. Expired or revoked refresh
|
|
161
|
+
# tokens are rejected with the same error.
|
|
162
|
+
if not self._is_invalid_grant(err):
|
|
163
|
+
raise
|
|
164
|
+
logger.info(
|
|
165
|
+
"Refresh token rejected, triggering device authentication flow..."
|
|
166
|
+
)
|
|
167
|
+
token = self.client.device_auth_flow()
|
|
134
168
|
self._set_token(token)
|
|
135
169
|
self.cache_credentials()
|
|
136
170
|
|