DIRACCommon 9.0.0a66__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.
@@ -0,0 +1,93 @@
1
+ # Python
2
+ *.py[cod]
3
+
4
+ # C extensions
5
+ *.so
6
+ var
7
+ sdist
8
+ lib
9
+ lib64
10
+
11
+ # Packages
12
+ *.egg
13
+ *.egg-info
14
+ dist
15
+ build
16
+ eggs
17
+ parts
18
+ bin
19
+ develop-eggs
20
+ .installed.cfg
21
+
22
+ # Translations
23
+ *.mo
24
+
25
+ # Mr Developer
26
+ .mr.developer.cfg
27
+
28
+ # Installer logs
29
+ pip-log.txt
30
+
31
+ # Unit test / coverage reports
32
+ .coverage
33
+ .tox
34
+
35
+ # Eclipse
36
+ .project
37
+ .pydevproject
38
+ .pyproject
39
+ .settings
40
+ .metadata
41
+
42
+ # Vim
43
+ .*.sw[a-z]
44
+ *.un~
45
+ Session.vim
46
+
47
+ # Emacs
48
+ *~
49
+
50
+ # Intellij
51
+ .idea/
52
+ DIRAC.iml
53
+
54
+ # MaxOSX files
55
+ .DS_Store
56
+
57
+ # test stuff
58
+ .pytest_cache
59
+ .cache
60
+ cache.db
61
+ __pycache__
62
+ pytests.xml
63
+ nosetests.xml
64
+ coverage.xml
65
+ Local_*
66
+ .hypothesis
67
+ virtualmachine/.vagrant/
68
+
69
+ integration_test_results/
70
+ tests/CI/CLIENTCONFIG
71
+ tests/CI/SERVERCONFIG
72
+ src/etc/
73
+
74
+ pilot.cfg
75
+ DIRAC_containers
76
+ docs/source/dirac.cfg
77
+ # VSCode
78
+ .vscode
79
+ .env
80
+
81
+ # docs
82
+ # this is auto generated
83
+ docs/source/CodeDocumentation/
84
+ docs/source/AdministratorGuide/Configuration/ExampleConfig.rst
85
+ docs/source/AdministratorGuide/CommandReference
86
+ docs/source/UserGuide/CommandReference
87
+ docs/_build
88
+ docs/source/_build
89
+
90
+ # pixi environments
91
+ .pixi
92
+ *.egg-info
93
+ pixi.lock
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: DIRACCommon
3
+ Version: 9.0.0a66
4
+ Summary: Stateless utilities extracted from DIRAC for use by DiracX and other projects
5
+ Project-URL: Homepage, https://github.com/DIRACGrid/DIRAC
6
+ Project-URL: Documentation, https://dirac.readthedocs.io/
7
+ Project-URL: Source Code, https://github.com/DIRACGrid/DIRAC
8
+ Author-email: DIRAC Collaboration <dirac-dev@cern.ch>
9
+ License: GPL-3.0-only
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Topic :: System :: Distributed Computing
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: typing-extensions>=4.0.0
18
+ Provides-Extra: testing
19
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'testing'
20
+ Requires-Dist: pytest>=7.0.0; extra == 'testing'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # DIRACCommon
24
+
25
+ Stateless utilities extracted from DIRAC for use by DiracX and other projects without triggering DIRAC's global state initialization.
26
+
27
+ ## Purpose
28
+
29
+ This package solves the circular dependency issue where DiracX needs DIRAC utilities but importing DIRAC triggers global state initialization. DIRACCommon contains only stateless utilities that can be safely imported without side effects.
30
+
31
+ ## Contents
32
+
33
+ - `DIRACCommon.Utils.ReturnValues`: DIRAC's S_OK/S_ERROR return value system
34
+ - `DIRACCommon.Utils.DErrno`: DIRAC error codes and utilities
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install DIRACCommon
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from DIRACCommon.Utils.ReturnValues import S_OK, S_ERROR
46
+
47
+ def my_function():
48
+ if success:
49
+ return S_OK("Operation successful")
50
+ else:
51
+ return S_ERROR("Operation failed")
52
+ ```
53
+
54
+ ## Development
55
+
56
+ This package is part of the DIRAC project and shares its version number. When DIRAC is released, DIRACCommon is also released with the same version.
57
+
58
+ ```bash
59
+ pixi install
60
+ pixi run pytest
61
+ ```
62
+
63
+ ## Guidelines for Adding Code
64
+
65
+ Code added to DIRACCommon must:
66
+ - Be completely stateless
67
+ - Not import or use any of DIRAC's global objects (`gConfig`, `gLogger`, `gMonitor`, `Operations`)
68
+ - Not establish database connections
69
+ - Not have side effects on import
@@ -0,0 +1,47 @@
1
+ # DIRACCommon
2
+
3
+ Stateless utilities extracted from DIRAC for use by DiracX and other projects without triggering DIRAC's global state initialization.
4
+
5
+ ## Purpose
6
+
7
+ This package solves the circular dependency issue where DiracX needs DIRAC utilities but importing DIRAC triggers global state initialization. DIRACCommon contains only stateless utilities that can be safely imported without side effects.
8
+
9
+ ## Contents
10
+
11
+ - `DIRACCommon.Utils.ReturnValues`: DIRAC's S_OK/S_ERROR return value system
12
+ - `DIRACCommon.Utils.DErrno`: DIRAC error codes and utilities
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install DIRACCommon
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from DIRACCommon.Utils.ReturnValues import S_OK, S_ERROR
24
+
25
+ def my_function():
26
+ if success:
27
+ return S_OK("Operation successful")
28
+ else:
29
+ return S_ERROR("Operation failed")
30
+ ```
31
+
32
+ ## Development
33
+
34
+ This package is part of the DIRAC project and shares its version number. When DIRAC is released, DIRACCommon is also released with the same version.
35
+
36
+ ```bash
37
+ pixi install
38
+ pixi run pytest
39
+ ```
40
+
41
+ ## Guidelines for Adding Code
42
+
43
+ Code added to DIRACCommon must:
44
+ - Be completely stateless
45
+ - Not import or use any of DIRAC's global objects (`gConfig`, `gLogger`, `gMonitor`, `Operations`)
46
+ - Not establish database connections
47
+ - Not have side effects on import
@@ -0,0 +1,133 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "DIRACCommon"
7
+ description = "Stateless utilities extracted from DIRAC for use by DiracX and other projects"
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ license = {text = "GPL-3.0-only"}
11
+ authors = [
12
+ {name = "DIRAC Collaboration", email = "dirac-dev@cern.ch"},
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Scientific/Engineering",
20
+ "Topic :: System :: Distributed Computing",
21
+ ]
22
+ dependencies = [
23
+ "typing-extensions>=4.0.0",
24
+ ]
25
+ dynamic = ["version"]
26
+
27
+ [project.optional-dependencies]
28
+ testing = [
29
+ "pytest>=7.0.0",
30
+ "pytest-cov>=4.0.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/DIRACGrid/DIRAC"
35
+ Documentation = "https://dirac.readthedocs.io/"
36
+ "Source Code" = "https://github.com/DIRACGrid/DIRAC"
37
+
38
+ [tool.hatch.version]
39
+ source = "vcs"
40
+
41
+ [tool.hatch.version.raw-options]
42
+ root = ".."
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "/src",
47
+ "/tests",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/DIRACCommon"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ python_files = "test_*.py"
56
+ python_classes = "Test*"
57
+ python_functions = "test_*"
58
+ addopts = ["-v", "--cov=DIRACCommon", "--cov-report=term-missing"]
59
+
60
+ [tool.coverage.run]
61
+ source = ["src/DIRACCommon"]
62
+ omit = ["*/tests/*"]
63
+
64
+ [tool.coverage.report]
65
+ exclude_lines = [
66
+ "pragma: no cover",
67
+ "def __repr__",
68
+ "raise AssertionError",
69
+ "raise NotImplementedError",
70
+ "if __name__ == .__main__.:",
71
+ "if TYPE_CHECKING:",
72
+ ]
73
+
74
+ [tool.mypy]
75
+ python_version = "3.11"
76
+ files = ["src/DIRACCommon"]
77
+ strict = true
78
+ warn_return_any = true
79
+ warn_unused_configs = true
80
+ disallow_untyped_defs = true
81
+ disallow_incomplete_defs = true
82
+ check_untyped_defs = true
83
+ no_implicit_optional = true
84
+ warn_redundant_casts = true
85
+ warn_unused_ignores = true
86
+ warn_no_return = true
87
+ warn_unreachable = true
88
+ strict_equality = true
89
+
90
+ [tool.ruff]
91
+ line-length = 120
92
+ target-version = "py311"
93
+ select = [
94
+ "E", # pycodestyle errors
95
+ "F", # pyflakes
96
+ "B", # flake8-bugbear
97
+ "I", # isort
98
+ "PLE", # pylint errors
99
+ "UP", # pyupgrade
100
+ ]
101
+ ignore = [
102
+ "B905", # zip without explicit strict parameter
103
+ "B008", # do not perform function calls in argument defaults
104
+ "B006", # do not use mutable data structures for argument defaults
105
+ ]
106
+
107
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
108
+ # This ensures DIRACCommon never imports from DIRAC
109
+ "DIRAC" = {msg = "DIRACCommon must not import from DIRAC to avoid global state initialization"}
110
+
111
+ [tool.black]
112
+ line-length = 120
113
+ target-version = ['py311']
114
+
115
+ [tool.isort]
116
+ profile = "black"
117
+ line_length = 120
118
+
119
+ [tool.pixi.workspace]
120
+ channels = ["conda-forge"]
121
+ platforms = ["linux-64", "linux-aarch64", "osx-arm64"]
122
+
123
+ [tool.pixi.pypi-dependencies]
124
+ DIRACCommon = { path = ".", editable = true }
125
+
126
+ [tool.pixi.feature.testing.tasks.pytest]
127
+ cmd = "pytest"
128
+
129
+ [tool.pixi.environments]
130
+ default = { solve-group = "default" }
131
+ testing = { features = ["testing"], solve-group = "default" }
132
+
133
+ [tool.pixi.tasks]
@@ -0,0 +1,327 @@
1
+ """ :mod: DErrno
2
+
3
+ ==========================
4
+
5
+ .. module: DErrno
6
+
7
+ :synopsis: Error list and utilities for handling errors in DIRAC
8
+
9
+
10
+ This module contains list of errors that can be encountered in DIRAC.
11
+ It complements the errno module of python.
12
+
13
+ It also contains utilities to manipulate these errors.
14
+
15
+ This is a stateless version extracted to DIRACCommon to avoid circular dependencies.
16
+ The extension loading functionality has been removed.
17
+ """
18
+ import os
19
+
20
+ # To avoid conflict, the error numbers should be greater than 1000
21
+ # We decided to group the by range of 100 per system
22
+
23
+ # 1000: Generic
24
+ # 1100: Core
25
+ # 1200: Framework
26
+ # 1300: Interfaces
27
+ # 1400: Config
28
+ # 1500: WMS + Workflow
29
+ # 1600: DMS + StorageManagement
30
+ # 1700: RMS
31
+ # 1800: Accounting + Monitoring
32
+ # 1900: TS + Production
33
+ # 2000: Resources + RSS
34
+
35
+ # ## Generic (10XX)
36
+ # Python related: 0X
37
+ ETYPE = 1000
38
+ EIMPERR = 1001
39
+ ENOMETH = 1002
40
+ ECONF = 1003
41
+ EVALUE = 1004
42
+ EEEXCEPTION = 1005
43
+ # Files manipulation: 1X
44
+ ECTMPF = 1010
45
+ EOF = 1011
46
+ ERF = 1012
47
+ EWF = 1013
48
+ ESPF = 1014
49
+
50
+ # ## Core (11XX)
51
+ # Certificates and Proxy: 0X
52
+ EX509 = 1100
53
+ EPROXYFIND = 1101
54
+ EPROXYREAD = 1102
55
+ ECERTFIND = 1103
56
+ ECERTREAD = 1104
57
+ ENOCERT = 1105
58
+ ENOCHAIN = 1106
59
+ ENOPKEY = 1107
60
+ ENOGROUP = 1108
61
+ # DISET: 1X
62
+ EDISET = 1110
63
+ ENOAUTH = 1111
64
+ # 3rd party security: 2X
65
+ E3RDPARTY = 1120
66
+ EVOMS = 1121
67
+ # Databases : 3X
68
+ EDB = 1130
69
+ EMYSQL = 1131
70
+ ESQLA = 1132
71
+ # Message Queues: 4X
72
+ EMQUKN = 1140
73
+ EMQNOM = 1141
74
+ EMQCONN = 1142
75
+ # OpenSearch
76
+ EELNOFOUND = 1146
77
+ # Tokens
78
+ EATOKENFIND = 1150
79
+ EATOKENREAD = 1151
80
+ ETOKENTYPE = 1152
81
+
82
+ # config
83
+ ESECTION = 1400
84
+
85
+ # processes
86
+ EEZOMBIE = 1147
87
+ EENOPID = 1148
88
+
89
+ # ## WMS/Workflow
90
+ EWMSUKN = 1500
91
+ EWMSJDL = 1501
92
+ EWMSRESC = 1502
93
+ EWMSSUBM = 1503
94
+ EWMSJMAN = 1504
95
+ EWMSSTATUS = 1505
96
+ EWMSNOMATCH = 1510
97
+ EWMSPLTVER = 1511
98
+ EWMSNOPILOT = 1550
99
+
100
+ # ## DMS/StorageManagement (16XX)
101
+ EFILESIZE = 1601
102
+ EGFAL = 1602
103
+ EBADCKS = 1603
104
+ EFCERR = 1604
105
+
106
+ # ## RMS (17XX)
107
+ ERMSUKN = 1700
108
+
109
+ # ## TS (19XX)
110
+ ETSUKN = 1900
111
+ ETSDATA = 1901
112
+
113
+ # ## Resources and RSS (20XX)
114
+ ERESGEN = 2000
115
+ ERESUNA = 2001
116
+ ERESUNK = 2002
117
+
118
+ # This translates the integer number into the name of the variable
119
+ dErrorCode = {
120
+ # ## Generic (10XX)
121
+ # 100X: Python related
122
+ 1000: "ETYPE",
123
+ 1001: "EIMPERR",
124
+ 1002: "ENOMETH",
125
+ 1003: "ECONF",
126
+ 1004: "EVALUE",
127
+ 1005: "EEEXCEPTION",
128
+ # 101X: Files manipulation
129
+ 1010: "ECTMPF",
130
+ 1011: "EOF",
131
+ 1012: "ERF",
132
+ 1013: "EWF",
133
+ 1014: "ESPF",
134
+ # ## Core
135
+ # 110X: Certificates and Proxy
136
+ 1100: "EX509",
137
+ 1101: "EPROXYFIND",
138
+ 1102: "EPROXYREAD",
139
+ 1103: "ECERTFIND",
140
+ 1104: "ECERTREAD",
141
+ 1105: "ENOCERT",
142
+ 1106: "ENOCHAIN",
143
+ 1107: "ENOPKEY",
144
+ 1108: "ENOGROUP",
145
+ # 111X: DISET
146
+ 1110: "EDISET",
147
+ 1111: "ENOAUTH",
148
+ # 112X: 3rd party security
149
+ 1120: "E3RDPARTY",
150
+ 1121: "EVOMS",
151
+ # 113X: Databases
152
+ 1130: "EDB",
153
+ 1131: "EMYSQL",
154
+ 1132: "ESQLA",
155
+ # 114X: Message Queues
156
+ 1140: "EMQUKN",
157
+ 1141: "EMQNOM",
158
+ 1142: "EMQCONN",
159
+ # OpenSearch
160
+ 1146: "EELNOFOUND",
161
+ # 115X: Tokens
162
+ 1150: "EATOKENFIND",
163
+ 1151: "EATOKENREAD",
164
+ 1152: "ETOKENTYPE",
165
+ # Config
166
+ 1400: "ESECTION",
167
+ # Processes
168
+ 1147: "EEZOMBIE",
169
+ 1148: "EENOPID",
170
+ # WMS/Workflow
171
+ 1500: "EWMSUKN",
172
+ 1501: "EWMSJDL",
173
+ 1502: "EWMSRESC",
174
+ 1503: "EWMSSUBM",
175
+ 1504: "EWMSJMAN",
176
+ 1505: "EWMSSTATUS",
177
+ 1510: "EWMSNOMATCH",
178
+ 1511: "EWMSPLTVER",
179
+ 1550: "EWMSNOPILOT",
180
+ # DMS/StorageManagement
181
+ 1601: "EFILESIZE",
182
+ 1602: "EGFAL",
183
+ 1603: "EBADCKS",
184
+ 1604: "EFCERR",
185
+ # RMS
186
+ 1700: "ERMSUKN",
187
+ # Resources and RSS
188
+ 2000: "ERESGEN",
189
+ 2001: "ERESUNA",
190
+ 2002: "ERESUNK",
191
+ # TS
192
+ 1900: "ETSUKN",
193
+ 1901: "ETSDATA",
194
+ }
195
+
196
+
197
+ dStrError = { # Generic (10XX)
198
+ # 100X: Python related
199
+ ETYPE: "Object Type Error",
200
+ EIMPERR: "Failed to import library",
201
+ ENOMETH: "No such method or function",
202
+ ECONF: "Configuration error",
203
+ EVALUE: "Wrong value passed",
204
+ EEEXCEPTION: "runtime general exception",
205
+ # 101X: Files manipulation
206
+ ECTMPF: "Failed to create temporary file",
207
+ EOF: "Cannot open file",
208
+ ERF: "Cannot read from file",
209
+ EWF: "Cannot write to file",
210
+ ESPF: "Cannot set permissions to file",
211
+ # ## Core
212
+ # 110X: Certificates and Proxy
213
+ EX509: "Generic Error with X509",
214
+ EPROXYFIND: "Can't find proxy",
215
+ EPROXYREAD: "Can't read proxy",
216
+ ECERTFIND: "Can't find certificate",
217
+ ECERTREAD: "Can't read certificate",
218
+ ENOCERT: "No certificate loaded",
219
+ ENOCHAIN: "No chain loaded",
220
+ ENOPKEY: "No private key loaded",
221
+ ENOGROUP: "No DIRAC group",
222
+ # 111X: DISET
223
+ EDISET: "DISET Error",
224
+ ENOAUTH: "Unauthorized query",
225
+ # 112X: 3rd party security
226
+ E3RDPARTY: "3rd party security service error",
227
+ EVOMS: "VOMS Error",
228
+ # 113X: Databases
229
+ EDB: "Database Error",
230
+ EMYSQL: "MySQL Error",
231
+ ESQLA: "SQLAlchemy Error",
232
+ # 114X: Message Queues
233
+ EMQUKN: "Unknown MQ Error",
234
+ EMQNOM: "No messages",
235
+ EMQCONN: "MQ connection failure",
236
+ # 114X OpenSearch
237
+ EELNOFOUND: "Index not found",
238
+ # 115X: Tokens
239
+ EATOKENFIND: "Can't find a bearer access token.",
240
+ EATOKENREAD: "Can't read a bearer access token.",
241
+ ETOKENTYPE: "Unsupported access token type.",
242
+ # Config
243
+ ESECTION: "Section is not found",
244
+ # processes
245
+ EEZOMBIE: "Zombie process",
246
+ EENOPID: "No PID of process",
247
+ # WMS/Workflow
248
+ EWMSUKN: "Unknown WMS error",
249
+ EWMSJDL: "Invalid job description",
250
+ EWMSRESC: "Job to reschedule",
251
+ EWMSSUBM: "Job submission error",
252
+ EWMSJMAN: "Job management error",
253
+ EWMSSTATUS: "Job status error",
254
+ EWMSNOPILOT: "No pilots found",
255
+ EWMSPLTVER: "Pilot version does not match",
256
+ EWMSNOMATCH: "No match found",
257
+ # DMS/StorageManagement
258
+ EFILESIZE: "Bad file size",
259
+ EGFAL: "Error with the gfal call",
260
+ EBADCKS: "Bad checksum",
261
+ EFCERR: "FileCatalog error",
262
+ # RMS
263
+ ERMSUKN: "Unknown RMS error",
264
+ # Resources and RSS
265
+ ERESGEN: "Unknown Resource Failure",
266
+ ERESUNA: "Resource not available",
267
+ ERESUNK: "Unknown Resource",
268
+ # TS
269
+ ETSUKN: "Unknown Transformation System Error",
270
+ ETSDATA: "Invalid Input Data definition",
271
+ }
272
+
273
+
274
+ def strerror(code: int) -> str:
275
+ """This method wraps up os.strerror, and behave the same way.
276
+ It completes it with the DIRAC specific errors.
277
+ """
278
+
279
+ if code == 0:
280
+ return "Undefined error"
281
+
282
+ errMsg = f"Unknown error {code}"
283
+
284
+ try:
285
+ errMsg = dStrError[code]
286
+ except KeyError:
287
+ # It is not a DIRAC specific error, try the os one
288
+ try:
289
+ errMsg = os.strerror(code)
290
+ # On some system, os.strerror raises an exception with unknown code,
291
+ # on others, it returns a message...
292
+ except ValueError:
293
+ pass
294
+
295
+ return errMsg
296
+
297
+
298
+ def cmpError(inErr: str | int | dict, candidate: int) -> bool:
299
+ """This function compares an error (in its old form (a string or dictionary) or in its int form
300
+ with a candidate error code.
301
+
302
+ :param inErr: a string, an integer, a S_ERROR dictionary
303
+ :type inErr: str or int or S_ERROR
304
+ :param int candidate: error code to compare with
305
+
306
+ :return: True or False
307
+
308
+ If an S_ERROR instance is passed, we compare the code with S_ERROR['Errno']
309
+ If it is a Integer, we do a direct comparison
310
+ If it is a String, we use strerror to check the error string
311
+ """
312
+
313
+ if isinstance(inErr, str): # old style
314
+ # Compare error message strings
315
+ errMsg = strerror(candidate)
316
+ return errMsg in inErr
317
+ elif isinstance(inErr, dict): # if the S_ERROR structure is given
318
+ # Check if Errno defined in the dict
319
+ errorNumber = inErr.get("Errno")
320
+ if errorNumber:
321
+ return errorNumber == candidate
322
+ errMsg = strerror(candidate)
323
+ return errMsg in inErr.get("Message", "")
324
+ elif isinstance(inErr, int):
325
+ return inErr == candidate
326
+ else:
327
+ raise TypeError(f"Unknown input error type {type(inErr)}")
@@ -0,0 +1,255 @@
1
+ """
2
+ DIRAC return dictionary
3
+
4
+ Message values are converted to string
5
+
6
+ keys are converted to string
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import sys
12
+ import traceback
13
+ from types import TracebackType
14
+ from typing import Any, Callable, cast, Generic, Literal, overload, Type, TypeVar, Union
15
+ from typing_extensions import TypedDict, ParamSpec, NotRequired
16
+
17
+ from DIRACCommon.Utils.DErrno import strerror
18
+
19
+
20
+ T = TypeVar("T")
21
+ P = ParamSpec("P")
22
+
23
+
24
+ class DOKReturnType(TypedDict, Generic[T]):
25
+ """used for typing the DIRAC return structure"""
26
+
27
+ OK: Literal[True]
28
+ Value: T
29
+
30
+
31
+ class DErrorReturnType(TypedDict):
32
+ """used for typing the DIRAC return structure"""
33
+
34
+ OK: Literal[False]
35
+ Message: str
36
+ Errno: int
37
+ ExecInfo: NotRequired[tuple[type[BaseException], BaseException, TracebackType]]
38
+ CallStack: NotRequired[list[str]]
39
+
40
+
41
+ DReturnType = Union[DOKReturnType[T], DErrorReturnType]
42
+
43
+
44
+ def S_ERROR(*args: Any, **kwargs: Any) -> DErrorReturnType:
45
+ """return value on error condition
46
+
47
+ Arguments are either Errno and ErrorMessage or just ErrorMessage fro backward compatibility
48
+
49
+ :param int errno: Error number
50
+ :param string message: Error message
51
+ :param list callStack: Manually override the CallStack attribute better performance
52
+ """
53
+ callStack = kwargs.pop("callStack", None)
54
+
55
+ result: DErrorReturnType = {"OK": False, "Errno": 0, "Message": ""}
56
+
57
+ message = ""
58
+ if args:
59
+ if isinstance(args[0], int):
60
+ result["Errno"] = args[0]
61
+ if len(args) > 1:
62
+ message = args[1]
63
+ else:
64
+ message = args[0]
65
+
66
+ if result["Errno"]:
67
+ message = f"{strerror(result['Errno'])} ( {result['Errno']} : {message})"
68
+ result["Message"] = message
69
+
70
+ if callStack is None:
71
+ try:
72
+ callStack = traceback.format_stack()
73
+ callStack.pop()
74
+ except Exception:
75
+ callStack = []
76
+
77
+ result["CallStack"] = callStack
78
+
79
+ return result
80
+
81
+
82
+ # mypy doesn't understand default parameter values with generics so use overloads (python/mypy#3737)
83
+ @overload
84
+ def S_OK() -> DOKReturnType[None]:
85
+ ...
86
+
87
+
88
+ @overload
89
+ def S_OK(value: T) -> DOKReturnType[T]:
90
+ ...
91
+
92
+
93
+ def S_OK(value=None): # type: ignore
94
+ """return value on success
95
+
96
+ :param value: value of the 'Value'
97
+ :return: dictionary { 'OK' : True, 'Value' : value }
98
+ """
99
+ return {"OK": True, "Value": value}
100
+
101
+
102
+ def isReturnStructure(unk: Any) -> bool:
103
+ """Check if value is an `S_OK`/`S_ERROR` object"""
104
+ if not isinstance(unk, dict):
105
+ return False
106
+ if "OK" not in unk:
107
+ return False
108
+ if unk["OK"]:
109
+ return "Value" in unk
110
+ else:
111
+ return "Message" in unk
112
+
113
+
114
+ def isSError(value: Any) -> bool:
115
+ """Check if value is an `S_ERROR` object"""
116
+ if not isinstance(value, dict):
117
+ return False
118
+ if "OK" not in value:
119
+ return False
120
+ return "Message" in value
121
+
122
+
123
+ def reprReturnErrorStructure(struct: DErrorReturnType, full: bool = False) -> str:
124
+ errorNumber = struct.get("Errno", 0)
125
+ message = struct.get("Message", "")
126
+ if errorNumber:
127
+ reprStr = f"{strerror(errorNumber)} ( {errorNumber} : {message})"
128
+ else:
129
+ reprStr = message
130
+
131
+ if full:
132
+ callStack = struct.get("CallStack")
133
+ if callStack:
134
+ reprStr += "\n" + "".join(callStack)
135
+
136
+ return reprStr
137
+
138
+
139
+ def returnSingleResult(dictRes: DReturnType[Any]) -> DReturnType[Any]:
140
+ """Transform the S_OK{Successful/Failed} dictionary convention into
141
+ an S_OK/S_ERROR return. To be used when a single returned entity
142
+ is expected from a generally bulk call.
143
+
144
+ :param dictRes: S_ERROR or S_OK( "Failed" : {}, "Successful" : {})
145
+ :returns: S_ERROR or S_OK(value)
146
+
147
+ The following rules are applied:
148
+
149
+ - if dictRes is an S_ERROR: returns it as is
150
+ - we start by looking at the Failed directory
151
+ - if there are several items in a dictionary, we return the first one
152
+ - if both dictionaries are empty, we return S_ERROR
153
+ - For an item in Failed, we return S_ERROR
154
+ - Far an item in Successful we return S_OK
155
+
156
+ Behavior examples (would be perfect unit test :-) )::
157
+
158
+ {'Message': 'Kaput', 'OK': False} -> {'Message': 'Kaput', 'OK': False}
159
+ {'OK': True, 'Value': {'Successful': {}, 'Failed': {'a': 1}}} -> {'Message': '1', 'OK': False}
160
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {}}} -> {'OK': True, 'Value': 2}
161
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {'a': 1}}} -> {'Message': '1', 'OK': False}
162
+ {'OK': True, 'Value': {'Successful': {'b': 2}, 'Failed': {'a': 1, 'c': 3}}} -> {'Message': '1', 'OK': False}
163
+ {'OK': True, 'Value': {'Successful': {'b': 2, 'd': 4}, 'Failed': {}}} -> {'OK': True, 'Value': 2}
164
+ {'OK': True, 'Value': {'Successful': {}, 'Failed': {}}} ->
165
+ {'Message': 'returnSingleResult: Failed and Successful dictionaries are empty', 'OK': False}
166
+ """
167
+ # if S_ERROR was returned, we return it as well
168
+ if not dictRes["OK"]:
169
+ return dictRes
170
+ # if there is a Failed, we return the first one in an S_ERROR
171
+ if "Failed" in dictRes["Value"] and len(dictRes["Value"]["Failed"]):
172
+ errorMessage = list(dictRes["Value"]["Failed"].values())[0]
173
+ if isinstance(errorMessage, dict):
174
+ if isReturnStructure(errorMessage):
175
+ return cast(DErrorReturnType, errorMessage)
176
+ else:
177
+ return S_ERROR(str(errorMessage))
178
+ return S_ERROR(errorMessage)
179
+ # if there is a Successful, we return the first one in an S_OK
180
+ elif "Successful" in dictRes["Value"] and len(dictRes["Value"]["Successful"]):
181
+ return S_OK(list(dictRes["Value"]["Successful"].values())[0])
182
+ else:
183
+ return S_ERROR("returnSingleResult: Failed and Successful dictionaries are empty")
184
+
185
+
186
+ class SErrorException(Exception):
187
+ """Exception class for use with `convertToReturnValue`"""
188
+
189
+ def __init__(self, result: DErrorReturnType | str, errCode: int = 0):
190
+ """Create a new exception return value
191
+
192
+ If `result` is a `S_ERROR` return it directly else convert it to an
193
+ appropriate value using `S_ERROR(errCode, result)`.
194
+
195
+ :param result: The error to propagate
196
+ :param errCode: the error code to propagate
197
+ """
198
+ if not isSError(result):
199
+ result = S_ERROR(errCode, result)
200
+ self.result = cast(DErrorReturnType, result)
201
+
202
+
203
+ def returnValueOrRaise(result: DReturnType[T], *, errorCode: int = 0) -> T:
204
+ """Unwrap an S_OK/S_ERROR response into a value or Exception
205
+
206
+ This method assists with using exceptions in DIRAC code by raising
207
+ :exc:`SErrorException` if `result` is an error. This can then by propagated
208
+ automatically as an `S_ERROR` by wrapping public facing functions with
209
+ `@convertToReturnValue`.
210
+
211
+ :param result: Result of a DIRAC function which returns `S_OK`/`S_ERROR`
212
+ :returns: The value associated with the `S_OK` object
213
+ :raises: If `result["OK"]` is falsey the original exception is re-raised.
214
+ If no exception is known an :exc:`SErrorException` is raised.
215
+ """
216
+ if not result["OK"]:
217
+ if "ExecInfo" in result:
218
+ raise result["ExecInfo"][0]
219
+ else:
220
+ raise SErrorException(result, errorCode)
221
+ return result["Value"]
222
+
223
+
224
+ def convertToReturnValue(func: Callable[P, T]) -> Callable[P, DReturnType[T]]:
225
+ """Decorate a function to convert return values to `S_OK`/`S_ERROR`
226
+
227
+ If `func` returns, wrap the return value in `S_OK`.
228
+ If `func` raises :exc:`SErrorException`, return the associated `S_ERROR`
229
+ If `func` raises any other exception type, convert it to an `S_ERROR` object
230
+
231
+ :param result: The bare result of a function call
232
+ :returns: `S_OK`/`S_ERROR`
233
+ """
234
+
235
+ @functools.wraps(func)
236
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> DReturnType[T]:
237
+ try:
238
+ value = func(*args, **kwargs)
239
+ except SErrorException as e:
240
+ return e.result
241
+ except Exception as e:
242
+ retval = S_ERROR(f"{repr(e)}: {e}")
243
+ # Replace CallStack with the one from the exception
244
+ # Use cast as mypy doesn't understand that sys.exc_info can't return None in an exception block
245
+ retval["ExecInfo"] = cast(tuple[type[BaseException], BaseException, TracebackType], sys.exc_info())
246
+ exc_type, exc_value, exc_tb = retval["ExecInfo"]
247
+ retval["CallStack"] = traceback.format_tb(exc_tb)
248
+ return retval
249
+ else:
250
+ return S_OK(value)
251
+
252
+ # functools will copy the annotations. Since we change the return type
253
+ # we have to update it
254
+ wrapped.__annotations__["return"] = DReturnType
255
+ return wrapped
@@ -0,0 +1,3 @@
1
+ """
2
+ DIRACCommon.Utils - Stateless utility functions
3
+ """
@@ -0,0 +1,21 @@
1
+ """
2
+ DIRACCommon - Stateless utilities for DIRAC
3
+
4
+ This package contains stateless utilities extracted from DIRAC that can be used
5
+ by DiracX and other projects without triggering DIRAC's global state initialization.
6
+
7
+ The utilities here should not depend on:
8
+ - gConfig (Configuration system)
9
+ - gLogger (Global logging)
10
+ - gMonitor (Monitoring)
11
+ - Database connections
12
+ - Any other global state
13
+ """
14
+
15
+ import importlib.metadata
16
+
17
+ try:
18
+ __version__ = importlib.metadata.version(__name__)
19
+ except importlib.metadata.PackageNotFoundError:
20
+ # package is not installed
21
+ __version__ = "Unknown"
@@ -0,0 +1,50 @@
1
+ """Tests for DErrno module"""
2
+
3
+ import pytest
4
+ from DIRACCommon.Utils import DErrno
5
+
6
+
7
+ def test_strerror():
8
+ """Test strerror function"""
9
+ # Test DIRAC specific errors
10
+ assert DErrno.strerror(DErrno.ETYPE) == "Object Type Error"
11
+ assert DErrno.strerror(DErrno.EIMPERR) == "Failed to import library"
12
+ assert DErrno.strerror(DErrno.EOF) == "Cannot open file"
13
+
14
+ # Test unknown error
15
+ assert "Unknown error" in DErrno.strerror(999999)
16
+
17
+ # Test zero error
18
+ assert DErrno.strerror(0) == "Undefined error"
19
+
20
+ # Test OS errors (should fall back to os.strerror)
21
+ # Error code 2 is usually "No such file or directory" on Unix
22
+ import errno
23
+
24
+ assert DErrno.strerror(errno.ENOENT) == "No such file or directory"
25
+
26
+
27
+ def test_cmpError():
28
+ """Test cmpError function"""
29
+ # Test with integer
30
+ assert DErrno.cmpError(DErrno.ETYPE, DErrno.ETYPE) is True
31
+ assert DErrno.cmpError(DErrno.ETYPE, DErrno.EIMPERR) is False
32
+
33
+ # Test with string (old style)
34
+ assert DErrno.cmpError("Object Type Error", DErrno.ETYPE) is True
35
+ assert DErrno.cmpError("Some error with Object Type Error in it", DErrno.ETYPE) is True
36
+ assert DErrno.cmpError("Different error", DErrno.ETYPE) is False
37
+
38
+ # Test with S_ERROR dictionary
39
+ error_dict = {"OK": False, "Message": "Object Type Error", "Errno": DErrno.ETYPE}
40
+ assert DErrno.cmpError(error_dict, DErrno.ETYPE) is True
41
+
42
+ error_dict_no_errno = {"OK": False, "Message": "Object Type Error"}
43
+ assert DErrno.cmpError(error_dict_no_errno, DErrno.ETYPE) is True
44
+
45
+ error_dict_wrong = {"OK": False, "Message": "Different error", "Errno": DErrno.EIMPERR}
46
+ assert DErrno.cmpError(error_dict_wrong, DErrno.ETYPE) is False
47
+
48
+ # Test with invalid type
49
+ with pytest.raises(TypeError):
50
+ DErrno.cmpError([], DErrno.ETYPE)
@@ -0,0 +1,73 @@
1
+ import pytest
2
+
3
+ from DIRACCommon.Utils.ReturnValues import S_OK, S_ERROR, SErrorException, convertToReturnValue, returnValueOrRaise
4
+
5
+
6
+ def test_Ok():
7
+ retVal = S_OK("Hello world")
8
+ assert retVal["OK"] is True
9
+ assert retVal["Value"] == "Hello world"
10
+
11
+
12
+ def test_Error():
13
+ retVal = S_ERROR("This is bad")
14
+ assert retVal["OK"] is False
15
+ assert retVal["Message"] == "This is bad"
16
+ callStack = "".join(retVal["CallStack"])
17
+ assert "test_ReturnValues" in callStack
18
+ assert "test_Error" in callStack
19
+
20
+
21
+ def test_ErrorWithCustomTraceback():
22
+ retVal = S_ERROR("This is bad", callStack=["My callstack"])
23
+ assert retVal["OK"] is False
24
+ assert retVal["Message"] == "This is bad"
25
+ assert retVal["CallStack"] == ["My callstack"]
26
+
27
+
28
+ class CustomException(Exception):
29
+ pass
30
+
31
+
32
+ @convertToReturnValue
33
+ def _happyFunction():
34
+ return {"12345": "Success"}
35
+
36
+
37
+ @convertToReturnValue
38
+ def _sadFunction():
39
+ raise CustomException("I am sad")
40
+ return {}
41
+
42
+
43
+ @convertToReturnValue
44
+ def _verySadFunction():
45
+ raise SErrorException("I am very sad")
46
+
47
+
48
+ @convertToReturnValue
49
+ def _sadButPreciseFunction():
50
+ raise SErrorException("I am sad, yet precise", errCode=123)
51
+
52
+
53
+ def test_convertToReturnValue():
54
+ retVal = _happyFunction()
55
+ assert retVal["OK"] is True
56
+ assert retVal["Value"] == {"12345": "Success"}
57
+ # Make sure exceptions are captured correctly
58
+ retVal = _sadFunction()
59
+ assert retVal["OK"] is False
60
+ assert "CustomException" in retVal["Message"]
61
+ # Make sure the exception is re-raised
62
+ with pytest.raises(CustomException):
63
+ returnValueOrRaise(_sadFunction())
64
+
65
+ retVal = _verySadFunction()
66
+ assert retVal["OK"] is False
67
+ assert retVal["Errno"] == 0
68
+ assert retVal["Message"] == "I am very sad"
69
+
70
+ retVal = _sadButPreciseFunction()
71
+ assert retVal["OK"] is False
72
+ assert retVal["Errno"] == 123
73
+ assert "I am sad, yet precise" in retVal["Message"]
@@ -0,0 +1 @@
1
+ """DIRACCommon test suite"""