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.
Files changed (97) hide show
  1. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.gitignore +0 -3
  2. cmsdials-2.2.0/.gitlab-ci.yml +125 -0
  3. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.pre-commit-config.yaml +1 -0
  4. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/PKG-INFO +49 -14
  5. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/README.md +48 -13
  6. cmsdials-2.2.0/cmsdials/_version.py +24 -0
  7. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/_base.py +9 -2
  8. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/bearer.py +39 -5
  9. cmsdials-2.2.0/cmsdials/auth/client.py +198 -0
  10. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/models.py +12 -0
  11. cmsdials-2.2.0/cmsdials/auth/secret_key.py +65 -0
  12. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/client.py +5 -1
  13. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/client.py +13 -2
  14. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/models.py +3 -1
  15. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/client.py +13 -2
  16. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/models.py +3 -1
  17. cmsdials-2.2.0/cmsdials/clients/ml_bad_lumisection/client.py +74 -0
  18. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/client.py +5 -1
  19. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/client.py +18 -4
  20. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/workspace/client.py +4 -1
  21. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/cmsdials.py +22 -6
  22. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/_json.py +3 -1
  23. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/api_client.py +107 -13
  24. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/base_model.py +9 -3
  25. cmsdials-2.2.0/cmsdials/utils/logger.py +8 -0
  26. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/PKG-INFO +49 -14
  27. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/SOURCES.txt +4 -0
  28. cmsdials-2.2.0/cmsdials.egg-info/scm_file_list.json +82 -0
  29. cmsdials-2.2.0/cmsdials.egg-info/scm_version.json +8 -0
  30. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/mypy.ini +1 -0
  31. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/pyproject.toml +1 -1
  32. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/ruff.toml +1 -1
  33. cmsdials-2.2.0/tests/integration/env.py +25 -0
  34. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_auth_client.py +33 -9
  35. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_file_index_client.py +15 -4
  36. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_h1d_client.py +15 -3
  37. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_h2d_client.py +15 -3
  38. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_lumisection_client.py +14 -3
  39. cmsdials-2.2.0/tests/integration/test_oms_proxy_client.py +60 -0
  40. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_resume.py +12 -4
  41. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_run_client.py +9 -3
  42. cmsdials-2.2.0/tests/integration/utils.py +23 -0
  43. cmsdials-2.2.0/tests/unit/test_auth.py +265 -0
  44. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tox.ini +1 -1
  45. cmsdials-2.2.0/uv.lock +2571 -0
  46. cmsdials-2.0.0rc4/.gitlab-ci.yml +0 -74
  47. cmsdials-2.0.0rc4/cmsdials/_version.py +0 -34
  48. cmsdials-2.0.0rc4/cmsdials/auth/client.py +0 -104
  49. cmsdials-2.0.0rc4/cmsdials/auth/secret_key.py +0 -42
  50. cmsdials-2.0.0rc4/cmsdials/clients/ml_bad_lumisection/client.py +0 -41
  51. cmsdials-2.0.0rc4/cmsdials/utils/logger.py +0 -6
  52. cmsdials-2.0.0rc4/tests/integration/env.py +0 -15
  53. cmsdials-2.0.0rc4/tests/integration/test_oms_proxy_client.py +0 -38
  54. cmsdials-2.0.0rc4/tests/integration/utils.py +0 -9
  55. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.coveragerc +0 -0
  56. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.editorconfig +0 -0
  57. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.mailmap +0 -0
  58. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/.tool-versions +0 -0
  59. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/LICENSE +0 -0
  60. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/__init__.py +0 -0
  61. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/__init__.py +0 -0
  62. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/auth/exceptions.py +0 -0
  63. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/__init__.py +0 -0
  64. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/__init__.py +0 -0
  65. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/dataset_index/models.py +0 -0
  66. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/__init__.py +0 -0
  67. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/client.py +0 -0
  68. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/file_index/models.py +0 -0
  69. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h1d/__init__.py +0 -0
  70. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/h2d/__init__.py +0 -0
  71. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/__init__.py +0 -0
  72. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/client.py +0 -0
  73. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/lumisection/models.py +0 -0
  74. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/__init__.py +0 -0
  75. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/client.py +0 -0
  76. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/mes/models.py +0 -0
  77. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_bad_lumisection/__init__.py +0 -0
  78. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_bad_lumisection/models.py +0 -0
  79. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/__init__.py +0 -0
  80. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/ml_models_index/models.py +0 -0
  81. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/__init__.py +0 -0
  82. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/oms_proxy/models.py +0 -0
  83. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/__init__.py +0 -0
  84. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/client.py +0 -0
  85. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/run/models.py +0 -0
  86. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/clients/workspace/__init__.py +0 -0
  87. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/filters.py +0 -0
  88. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials/utils/__init__.py +0 -0
  89. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/dependency_links.txt +0 -0
  90. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/requires.txt +0 -0
  91. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/cmsdials.egg-info/top_level.txt +0 -0
  92. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/pytest.ini +0 -0
  93. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/setup.cfg +0 -0
  94. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/__init__.py +0 -0
  95. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/integration/test_mes_client.py +0 -0
  96. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/unit/__init__.py +0 -0
  97. {cmsdials-2.0.0rc4 → cmsdials-2.2.0}/tests/unit/test_base_api_client.py +0 -0
@@ -163,8 +163,5 @@ cython_debug/
163
163
  # ruff
164
164
  .ruff_cache/
165
165
 
166
- # uv
167
- uv.lock
168
-
169
166
  # setuptools_scm
170
167
  cmsdials/_version.py
@@ -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
@@ -31,3 +31,4 @@ repos:
31
31
  - pandas-stubs>=2.2.2.240807
32
32
  - types-requests>=2.31.0.6
33
33
  - types-tqdm>=4.67.0.20250516
34
+ - numpy<2.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmsdials
3
- Version: 2.0.0rc4
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
- ![Build Status](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/badges/develop/pipeline.svg)
25
- ![Coverage](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/badges/develop/coverage.svg)
26
- ![Latest Release](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/-/badges/release.svg)
24
+ ![Build Status](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/badges/develop/pipeline.svg)
25
+ ![Coverage](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/badges/develop/coverage.svg)
26
+ ![Latest Release](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/-/badges/release.svg)
27
27
  [![PyPI version](https://badge.fury.io/py/cmsdials.png)](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 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.
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 local DIALS
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
- 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:
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
- DEV_URL = "http://localhost:8000/"
312
- DEV_CACHE_DIR = ".cache-dev"
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(base_url=DEV_URL)
315
- creds = Credentials.from_creds_file(cache_dir=DEV_CACHE_DIR, client=auth) # Make sure to specify the auth client with overwritten values, using another cache_dir is recommended
316
- dials = Dials(creds, base_url=DEV_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 [this line](/tests/integration/test_auth_client.py#L33).
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
- ![Build Status](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/badges/develop/pipeline.svg)
2
- ![Coverage](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/badges/develop/coverage.svg)
3
- ![Latest Release](https://gitlab.cern.ch/cms-dqmdc/libraries/dials-py/-/badges/release.svg)
1
+ ![Build Status](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/badges/develop/pipeline.svg)
2
+ ![Coverage](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/badges/develop/coverage.svg)
3
+ ![Latest Release](https://gitlab.cern.ch/cms-ppd/technical-support/libraries/dials-py/-/badges/release.svg)
4
4
  [![PyPI version](https://badge.fury.io/py/cmsdials.png)](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 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.
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 local DIALS
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
- 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:
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
- DEV_URL = "http://localhost:8000/"
289
- DEV_CACHE_DIR = ".cache-dev"
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(base_url=DEV_URL)
292
- creds = Credentials.from_creds_file(cache_dir=DEV_CACHE_DIR, client=auth) # Make sure to specify the auth client with overwritten values, using another cache_dir is recommended
293
- dials = Dials(creds, base_url=DEV_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 [this line](/tests/integration/test_auth_client.py#L33).
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 self.expires_at > datetime.now() > (self.expires_at - timedelta(seconds=0.1 * self.expires_in))
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 self.token_state == TokenState.STALE and self._stale_lock.locked() is False:
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("Access token and refresh token are expired, triggering device authentication flow...")
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, cache_dir: Optional[str] = None, client: Optional[AuthClient] = None
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(cache_dir: Optional[str] = None, client: Optional[AuthClient] = None) -> "Credentials":
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("Credentials file not found, triggering device authentication flow...")
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
- token: Token = self.client.refresh_token(self.token_type, self.token, self.refresh_token)
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