hito_tools 24.8.dev1__tar.gz → 26.1__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.
- {hito_tools-24.8.dev1 → hito_tools-26.1}/PKG-INFO +8 -10
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/exceptions.py +105 -103
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/nsip.py +499 -371
- {hito_tools-24.8.dev1 → hito_tools-26.1}/pyproject.toml +20 -31
- {hito_tools-24.8.dev1 → hito_tools-26.1}/LICENSE +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/README.md +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/ad.py +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/agents.py +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/core.py +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/projects.py +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/teams.py +0 -0
- {hito_tools-24.8.dev1 → hito_tools-26.1}/hito_tools/utils.py +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: hito_tools
|
|
3
|
-
Version:
|
|
3
|
+
Version: 26.1
|
|
4
4
|
Summary: Modules for interacting with Hito and NSIP
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
5
7
|
Author: Michel Jouvin
|
|
6
8
|
Author-email: michel.jouvin@ijclab.in2p3.fr
|
|
7
|
-
Requires-Python:
|
|
9
|
+
Requires-Python: >3.11
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
8
11
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: pandas (>=2.2)
|
|
15
|
-
Requires-Dist: requests (>=2.28,<3.0)
|
|
12
|
+
Project-URL: Bug Tracker, https://gitlab.in2p3.fr/hito/hito_tools/-/issues
|
|
13
|
+
Project-URL: Homepage, https://gitlab.in2p3.fr/hito/hito_tools
|
|
16
14
|
Description-Content-Type: text/markdown
|
|
17
15
|
|
|
18
16
|
# hito_tools module
|
|
@@ -1,103 +1,105 @@
|
|
|
1
|
-
# Exceptions for applications based on this module
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
# Status code in case of errors
|
|
6
|
-
EXIT_STATUS_CONFIG_ERROR = 1
|
|
7
|
-
EXIT_STATUS_INVALID_SQL_VALUE = 20
|
|
8
|
-
EXIT_STATUS_GENERAL_ERROR = 30
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ConfigFileEmpty(Exception):
|
|
12
|
-
def __init__(self, file):
|
|
13
|
-
self.msg = f"No configuration parameter defined in {file}"
|
|
14
|
-
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
15
|
-
|
|
16
|
-
def __str__(self):
|
|
17
|
-
return repr(self.msg)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ConfigInvalidParamValue(Exception):
|
|
21
|
-
def __init__(self, param, value, file=None):
|
|
22
|
-
if file:
|
|
23
|
-
file_msg = " (file={file})"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
file_msg = ""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
# Exceptions for applications based on this module
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
# Status code in case of errors
|
|
6
|
+
EXIT_STATUS_CONFIG_ERROR = 1
|
|
7
|
+
EXIT_STATUS_INVALID_SQL_VALUE = 20
|
|
8
|
+
EXIT_STATUS_GENERAL_ERROR = 30
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigFileEmpty(Exception):
|
|
12
|
+
def __init__(self, file):
|
|
13
|
+
self.msg = f"No configuration parameter defined in {file}"
|
|
14
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return repr(self.msg)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigInvalidParamValue(Exception):
|
|
21
|
+
def __init__(self, param, value, file=None):
|
|
22
|
+
if file:
|
|
23
|
+
file_msg = f" (file={file})"
|
|
24
|
+
else:
|
|
25
|
+
file_msg = ""
|
|
26
|
+
self.msg = f"Invalid configuration parameter value ({value}) for '{param}'{file_msg}"
|
|
27
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
return repr(self.msg)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ConfigMissingParam(Exception):
|
|
34
|
+
def __init__(self, param, file=None):
|
|
35
|
+
if file:
|
|
36
|
+
file_msg = f" (file={file})"
|
|
37
|
+
else:
|
|
38
|
+
file_msg = ""
|
|
39
|
+
self.msg = f"Missing required configuration parameter '{param}'{file_msg}"
|
|
40
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return repr(self.msg)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OptionMissing(Exception):
|
|
47
|
+
def __init__(self, option):
|
|
48
|
+
self.msg = f"Option '{option}' required but missing"
|
|
49
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return repr(self.msg)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NSIPPeriodAmbiguous(Exception):
|
|
56
|
+
def __init__(self, date, num_matches):
|
|
57
|
+
if date is None:
|
|
58
|
+
date = datetime.now()
|
|
59
|
+
self.msg = f"Several declaration periods ({num_matches}) found in NSIP matching {date}"
|
|
60
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
return repr(self.msg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NSIPPeriodMissing(Exception):
|
|
67
|
+
def __init__(self, date):
|
|
68
|
+
self.msg = f"No declaration period found in NSIP matching {date}"
|
|
69
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
70
|
+
|
|
71
|
+
def __str__(self):
|
|
72
|
+
return repr(self.msg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SQLArrayMalformedValue(Exception):
|
|
76
|
+
def __init__(self, longtext, value, index):
|
|
77
|
+
self.msg = (
|
|
78
|
+
f"SQL longtext array: malformed value ({value}) at index {index}"
|
|
79
|
+
f" (longtext={longtext})"
|
|
80
|
+
)
|
|
81
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
82
|
+
|
|
83
|
+
def __str__(self):
|
|
84
|
+
return repr(self.msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SQLInconsistentArrayLen(Exception):
|
|
88
|
+
def __init__(self, longtext, expected_length, actual_length):
|
|
89
|
+
self.msg = (
|
|
90
|
+
f"SQL longtext array: expected length is {expected_length} but actual length"
|
|
91
|
+
f" is {actual_length} (longtext={longtext})"
|
|
92
|
+
)
|
|
93
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
94
|
+
|
|
95
|
+
def __str__(self):
|
|
96
|
+
return repr(self.msg)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SQLInvalidArray(Exception):
|
|
100
|
+
def __init__(self, longtext):
|
|
101
|
+
self.msg = f"Invalid SQL longtext array: {longtext}"
|
|
102
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
return repr(self.msg)
|
|
@@ -1,371 +1,499 @@
|
|
|
1
|
-
# Module to handle NSIP interaction
|
|
2
|
-
import datetime
|
|
3
|
-
import re
|
|
4
|
-
from io import StringIO
|
|
5
|
-
from typing import Dict, List
|
|
6
|
-
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
# FIXME: would be better to allow proper verification of the host certificate...
|
|
10
|
-
import requests
|
|
11
|
-
import simplejson as json
|
|
12
|
-
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
13
|
-
|
|
14
|
-
from .exceptions import ConfigMissingParam, NSIPPeriodAmbiguous, NSIPPeriodMissing
|
|
15
|
-
|
|
16
|
-
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
17
|
-
|
|
18
|
-
# Define exit code in case of errors
|
|
19
|
-
EXIT_STATUS_NSIP_API_PARAMS = 10
|
|
20
|
-
EXIT_STATUS_NSIP_API_ERROR = 11
|
|
21
|
-
|
|
22
|
-
# Define some constants related to HTTP
|
|
23
|
-
HTTP_STATUS_OK = 200
|
|
24
|
-
HTTP_STATUS_CREATED = 201
|
|
25
|
-
HTTP_STATUS_ACCEPTED = 202
|
|
26
|
-
HTTP_STATUS_NO_CONTENT = 204
|
|
27
|
-
HTTP_STATUS_BAD_REQUEST = 400
|
|
28
|
-
HTTP_STATUS_UNAUTHORIZED = 401
|
|
29
|
-
HTTP_STATUS_FORBIDDEN = 403
|
|
30
|
-
HTTP_STATUS_NOT_FOUND = 404
|
|
31
|
-
HTTP_STATUS_CONFLICT = 409
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
DECLARATION_EXISTS_PATTERN = re.compile(
|
|
43
|
-
r'"A declaration (?P<decl_id>\d+) already exists for this agent.*"$'
|
|
44
|
-
)
|
|
45
|
-
DECLARATION_ADDED_PATTERN = re.compile(r'"Declaration (?P<decl_id>\d+) successfully created"$')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class NSIPRequestFailure(Exception):
|
|
49
|
-
def __init__(self, code, url):
|
|
50
|
-
self.msg = f"NSIP agent API request failure (Status={code}, URL={url})"
|
|
51
|
-
self.status = EXIT_STATUS_NSIP_API_ERROR
|
|
52
|
-
|
|
53
|
-
def __str__(self):
|
|
54
|
-
return repr(self.msg)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class NSIPConnection:
|
|
58
|
-
def __init__(
|
|
59
|
-
self,
|
|
60
|
-
server_url: str,
|
|
61
|
-
bearer_token: Dict[str, str],
|
|
62
|
-
agent_api: str,
|
|
63
|
-
lab_api: Dict[str, str],
|
|
64
|
-
institute_api: Dict[str, str],
|
|
65
|
-
) -> None:
|
|
66
|
-
self.server_url = server_url
|
|
67
|
-
self.token = bearer_token
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1
|
+
# Module to handle NSIP interaction
|
|
2
|
+
import datetime
|
|
3
|
+
import re
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
# FIXME: would be better to allow proper verification of the host certificate...
|
|
10
|
+
import requests
|
|
11
|
+
import simplejson as json
|
|
12
|
+
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
13
|
+
|
|
14
|
+
from .exceptions import ConfigMissingParam, NSIPPeriodAmbiguous, NSIPPeriodMissing
|
|
15
|
+
|
|
16
|
+
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
17
|
+
|
|
18
|
+
# Define exit code in case of errors
|
|
19
|
+
EXIT_STATUS_NSIP_API_PARAMS = 10
|
|
20
|
+
EXIT_STATUS_NSIP_API_ERROR = 11
|
|
21
|
+
|
|
22
|
+
# Define some constants related to HTTP
|
|
23
|
+
HTTP_STATUS_OK = 200
|
|
24
|
+
HTTP_STATUS_CREATED = 201
|
|
25
|
+
HTTP_STATUS_ACCEPTED = 202
|
|
26
|
+
HTTP_STATUS_NO_CONTENT = 204
|
|
27
|
+
HTTP_STATUS_BAD_REQUEST = 400
|
|
28
|
+
HTTP_STATUS_UNAUTHORIZED = 401
|
|
29
|
+
HTTP_STATUS_FORBIDDEN = 403
|
|
30
|
+
HTTP_STATUS_NOT_FOUND = 404
|
|
31
|
+
HTTP_STATUS_CONFLICT = 409
|
|
32
|
+
|
|
33
|
+
AGENT_HAS_MULTIPLE_CONTRACTS_PATTERN = re.compile(
|
|
34
|
+
(
|
|
35
|
+
r'"Agent has active multi-contracts in same laboratory - manual action needed\s+'
|
|
36
|
+
r"\|\s+idAgentContract\s+:\s+(?P<id1>\d+)\s+\|\s+idAgentContract\s+:\s+(?P<id2>\d+)"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
AGENT_NOT_IN_PROJECT_PATTERN = re.compile(
|
|
40
|
+
r'"Error : This project has not been affected to this agent, declaration forbidden"$'
|
|
41
|
+
)
|
|
42
|
+
DECLARATION_EXISTS_PATTERN = re.compile(
|
|
43
|
+
r'"A declaration (?P<decl_id>\d+) already exists for this agent.*"$'
|
|
44
|
+
)
|
|
45
|
+
DECLARATION_ADDED_PATTERN = re.compile(r'"Declaration (?P<decl_id>\d+) successfully created"$')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NSIPRequestFailure(Exception):
|
|
49
|
+
def __init__(self, code, url):
|
|
50
|
+
self.msg = f"NSIP agent API request failure (Status={code}, URL={url})"
|
|
51
|
+
self.status = EXIT_STATUS_NSIP_API_ERROR
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return repr(self.msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NSIPConnection:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
server_url: str,
|
|
61
|
+
bearer_token: Dict[str, str],
|
|
62
|
+
agent_api: str,
|
|
63
|
+
lab_api: Dict[str, str],
|
|
64
|
+
institute_api: Dict[str, str],
|
|
65
|
+
) -> None:
|
|
66
|
+
self.server_url = server_url
|
|
67
|
+
self.token = bearer_token
|
|
68
|
+
self.api_params = {
|
|
69
|
+
"agent_api": agent_api,
|
|
70
|
+
"institute_api": institute_api,
|
|
71
|
+
"lab_api": lab_api,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def _get_api_url(self, api_category: str, api_name: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Return the API URL for the specified api_name or raise an exception if
|
|
77
|
+
the API name parameter is missing in the configuration. Assumes that base
|
|
78
|
+
configuration (server_url, base_url...) has already been checked.
|
|
79
|
+
|
|
80
|
+
:param api_category: name of the API configuration category (key in self.api_params)
|
|
81
|
+
:param api_name: API name
|
|
82
|
+
:return: API URL
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if api_name not in self.api_params[api_category]:
|
|
86
|
+
raise ConfigMissingParam(f"nsip/{api_category}/{api_name}")
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
f"{self.server_url}{self.api_params[api_category]['base_url']}"
|
|
90
|
+
f"{self.api_params[api_category][api_name]}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _check_multiple_contracts(self, reason: str) -> Optional[str]:
|
|
94
|
+
"""
|
|
95
|
+
One of the possible error during agent-related updates is that the user has
|
|
96
|
+
multiple contracts attached to the lab for the current period. In this case
|
|
97
|
+
the reason contains the different contract IDs: used the last one
|
|
98
|
+
the update is retried with the second contract (generally the current one)
|
|
99
|
+
mentioned in the error message.
|
|
100
|
+
|
|
101
|
+
:param reason: error message returned by API
|
|
102
|
+
:return: contract ID if it is the multiple contract error or None
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
m = AGENT_HAS_MULTIPLE_CONTRACTS_PATTERN.match(reason)
|
|
106
|
+
if m:
|
|
107
|
+
contract = m.group("id2")
|
|
108
|
+
else:
|
|
109
|
+
contract = None
|
|
110
|
+
|
|
111
|
+
return contract
|
|
112
|
+
|
|
113
|
+
def get_agent_list(self, context: str = "NSIP"):
|
|
114
|
+
"""
|
|
115
|
+
Retrieve NSIP agents from NSIP API and return a dict built from the retrieved JSON
|
|
116
|
+
|
|
117
|
+
:param context: either 'NSIP' (all agents presents at least one day during the semester)
|
|
118
|
+
or 'DIRECTORY' (only agents with an active contract)
|
|
119
|
+
:return: dict representing the JSON anwser
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
url = self._get_api_url("lab_api", "agent_list")
|
|
123
|
+
r = requests.get(
|
|
124
|
+
url,
|
|
125
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
126
|
+
params={"context": context},
|
|
127
|
+
)
|
|
128
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
129
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
130
|
+
|
|
131
|
+
agents = r.json()
|
|
132
|
+
|
|
133
|
+
return agents
|
|
134
|
+
|
|
135
|
+
def add_agent_to_project(self, project_id: str, email: str, start_date: datetime.datetime):
|
|
136
|
+
"""
|
|
137
|
+
Add an agent to an NSIP project
|
|
138
|
+
|
|
139
|
+
:param project_id: project ID
|
|
140
|
+
:param email: user's email
|
|
141
|
+
:param start_date: start date for agent in the project
|
|
142
|
+
:return: status (0 for successful update, a positive value if an error occured),
|
|
143
|
+
http_status, http_reason
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
contract = None
|
|
147
|
+
url = self._get_api_url("agent_api", "project_assign")
|
|
148
|
+
params = {
|
|
149
|
+
"projectId": project_id,
|
|
150
|
+
"startDate": start_date.date().isoformat(),
|
|
151
|
+
"context": "NSIP",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
retry = True
|
|
155
|
+
retry_attempts = 0
|
|
156
|
+
while retry:
|
|
157
|
+
# adding agent is retried only for some specific errors
|
|
158
|
+
retry = False
|
|
159
|
+
|
|
160
|
+
if contract:
|
|
161
|
+
print(
|
|
162
|
+
f"INFO: adding agent {email} to project {project_id} using contract {contract}"
|
|
163
|
+
)
|
|
164
|
+
params["idAgentContract"] = contract
|
|
165
|
+
else:
|
|
166
|
+
params["emailReseda"] = email
|
|
167
|
+
|
|
168
|
+
r = requests.post(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
169
|
+
|
|
170
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
171
|
+
status = 0
|
|
172
|
+
elif r.status_code == HTTP_STATUS_CREATED:
|
|
173
|
+
status = 0
|
|
174
|
+
print(f"INFO: agent {email} successfully added to project {project_id}")
|
|
175
|
+
elif r.status_code == HTTP_STATUS_NOT_FOUND:
|
|
176
|
+
contract = self._check_multiple_contracts(r.text)
|
|
177
|
+
if retry_attempts == 0 and contract is not None:
|
|
178
|
+
retry = True
|
|
179
|
+
print(
|
|
180
|
+
(
|
|
181
|
+
f"Agent {email} has several contracts for the"
|
|
182
|
+
f" current period: retrying update with contract {contract}"
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
status = 1
|
|
187
|
+
else:
|
|
188
|
+
status = 1
|
|
189
|
+
|
|
190
|
+
return status, r.status_code, r.text
|
|
191
|
+
|
|
192
|
+
def update_agent(
|
|
193
|
+
self,
|
|
194
|
+
reseda_email: str,
|
|
195
|
+
team_id: str = None,
|
|
196
|
+
email: str = None,
|
|
197
|
+
phones: List[str] = None,
|
|
198
|
+
offices: List[str] = None,
|
|
199
|
+
) -> int:
|
|
200
|
+
"""
|
|
201
|
+
Update agent attributes. Every attribute can be omitted if it should not
|
|
202
|
+
be modified. reseda_email is used to identify the user to modify and is
|
|
203
|
+
the only required parameter. An agent cannot be added through the API, it
|
|
204
|
+
has to exist before.
|
|
205
|
+
|
|
206
|
+
:param reseda_email: the user resedaEmail, used to identify the user
|
|
207
|
+
:param team_id: ID of user's new team
|
|
208
|
+
:param email: user's new email
|
|
209
|
+
:param phones: user's new phone list
|
|
210
|
+
:param offices: user's new office list
|
|
211
|
+
:return: status (0 for successful update, a positive value if an error occured),
|
|
212
|
+
http_status, http_reason
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
params = {"emailReseda": reseda_email, "context": "DIRECTORY"}
|
|
216
|
+
url = self._get_api_url("agent_api", "agent_update")
|
|
217
|
+
if team_id is not None:
|
|
218
|
+
params["teamId"] = team_id
|
|
219
|
+
if email is not None:
|
|
220
|
+
params["contactEmail"] = email
|
|
221
|
+
if phones is not None:
|
|
222
|
+
# To clear the phones, an empty string must be passed to work around an API error
|
|
223
|
+
if len(phones) == 0:
|
|
224
|
+
phones.add("")
|
|
225
|
+
params["phoneNumbers"] = json.dumps(phones, iterable_as_array=True)
|
|
226
|
+
if offices is not None:
|
|
227
|
+
# To clear the offices, an empty string must be passed to work around an API error
|
|
228
|
+
if len(offices) == 0:
|
|
229
|
+
offices.add("")
|
|
230
|
+
params["offices"] = json.dumps(offices, iterable_as_array=True)
|
|
231
|
+
r = requests.put(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
232
|
+
|
|
233
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
234
|
+
status = 0
|
|
235
|
+
else:
|
|
236
|
+
status = 1
|
|
237
|
+
return status, r.status_code, r.text
|
|
238
|
+
|
|
239
|
+
def update_declaration(
|
|
240
|
+
self,
|
|
241
|
+
email: str,
|
|
242
|
+
project_id: str,
|
|
243
|
+
project_type: bool,
|
|
244
|
+
time: int,
|
|
245
|
+
period_start: str,
|
|
246
|
+
validation_date: datetime.date = None,
|
|
247
|
+
) -> int:
|
|
248
|
+
"""
|
|
249
|
+
Add or update a project declaration for a user specified by its RESEDA email
|
|
250
|
+
|
|
251
|
+
:param email: RESEDA email of the selected user
|
|
252
|
+
:param project_id: ID of the selected project
|
|
253
|
+
:param project_type: if True, it is a project, else it is a reference (other activities)
|
|
254
|
+
:param time: time spent on the project in the unit appropriate for the project (hour or
|
|
255
|
+
week)
|
|
256
|
+
:param period_start: start date of the validation period
|
|
257
|
+
:param validation_date: validation date
|
|
258
|
+
:return: status (0 for successful add, -1 for successful update, positive value if errors),
|
|
259
|
+
http_status if errors else declaration ID added/modified, http_reason
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
contract = None
|
|
263
|
+
url = self._get_api_url("agent_api", "declaration_add")
|
|
264
|
+
if project_type:
|
|
265
|
+
params = {"projectId": int(project_id), "referenceId": ""}
|
|
266
|
+
else:
|
|
267
|
+
params = {"projectId": "", "referenceId": int(project_id)}
|
|
268
|
+
params["context"] = "NSIP"
|
|
269
|
+
params["time"] = time
|
|
270
|
+
if validation_date:
|
|
271
|
+
validation_date_str = validation_date.date().isoformat()
|
|
272
|
+
params["managerValidationDate"] = validation_date_str
|
|
273
|
+
|
|
274
|
+
retry = True
|
|
275
|
+
while retry:
|
|
276
|
+
declaration_id = ""
|
|
277
|
+
# declaration update is retried only for some specific errors
|
|
278
|
+
retry = False
|
|
279
|
+
|
|
280
|
+
if contract:
|
|
281
|
+
print(f"INFO: updating {email} declaration using contract {contract}")
|
|
282
|
+
params["idAgentContract"] = contract
|
|
283
|
+
else:
|
|
284
|
+
params["emailReseda"] = email
|
|
285
|
+
|
|
286
|
+
r = requests.post(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
287
|
+
r.text
|
|
288
|
+
|
|
289
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
290
|
+
m = DECLARATION_ADDED_PATTERN.match(r.text)
|
|
291
|
+
if m:
|
|
292
|
+
declaration_id = m.group("decl_id")
|
|
293
|
+
else:
|
|
294
|
+
print(
|
|
295
|
+
f"ERROR: unable to extract declaration number from request"
|
|
296
|
+
f" reason ({r.text})"
|
|
297
|
+
)
|
|
298
|
+
status = 0
|
|
299
|
+
|
|
300
|
+
elif r.status_code == HTTP_STATUS_FORBIDDEN:
|
|
301
|
+
# If http status is Forbidden, parse the associated message. If it is the expected
|
|
302
|
+
# message for an already existing declaration, retrieve the declaration ID and
|
|
303
|
+
# update it.
|
|
304
|
+
m = DECLARATION_EXISTS_PATTERN.match(r.text)
|
|
305
|
+
if m:
|
|
306
|
+
declaration_id = m.group("decl_id")
|
|
307
|
+
url = self._get_api_url("agent_api", "declaration_update")
|
|
308
|
+
params = {"id": declaration_id, "time": time, "context": "NSIP"}
|
|
309
|
+
if validation_date_str:
|
|
310
|
+
params["managerValidationDate"] = validation_date_str
|
|
311
|
+
status = -1
|
|
312
|
+
r = requests.put(
|
|
313
|
+
url,
|
|
314
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
315
|
+
params=params,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
elif r.status_code == HTTP_STATUS_NOT_FOUND:
|
|
319
|
+
m = AGENT_NOT_IN_PROJECT_PATTERN.match(r.text)
|
|
320
|
+
if m:
|
|
321
|
+
user_add_status, http_status, http_reason = self.add_agent_to_project(
|
|
322
|
+
project_id,
|
|
323
|
+
email,
|
|
324
|
+
period_start,
|
|
325
|
+
)
|
|
326
|
+
if user_add_status == 0:
|
|
327
|
+
retry = True
|
|
328
|
+
else:
|
|
329
|
+
print(
|
|
330
|
+
f"ERROR: failed to add agent {email} to project {project_id}"
|
|
331
|
+
f" (error={http_status}, reason={http_reason})"
|
|
332
|
+
)
|
|
333
|
+
status = -2
|
|
334
|
+
else:
|
|
335
|
+
contract = self._check_multiple_contracts(r.text)
|
|
336
|
+
if contract:
|
|
337
|
+
retry = True
|
|
338
|
+
print(
|
|
339
|
+
(
|
|
340
|
+
f"Agent {email} has several contracts for the"
|
|
341
|
+
f" current period: retrying update with contract {contract}"
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
346
|
+
return status, declaration_id, r.text
|
|
347
|
+
else:
|
|
348
|
+
return 1, r.status_code, r.text
|
|
349
|
+
|
|
350
|
+
def get_declaration_period_id(self, period_date: datetime):
|
|
351
|
+
"""
|
|
352
|
+
Return the declaration ID for the declaration period matching a given date (the date must
|
|
353
|
+
be included in the period.
|
|
354
|
+
|
|
355
|
+
:param period_date: date that must be inside the period
|
|
356
|
+
:return: declaration period ID
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
url = self._get_api_url("institute_api", "declaration_period_list")
|
|
360
|
+
|
|
361
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"})
|
|
362
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
363
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
364
|
+
|
|
365
|
+
periods = pd.read_json(StringIO(r.content.decode()))
|
|
366
|
+
periods["startDateDeclaration"] = pd.to_datetime(periods.startDateDeclaration)
|
|
367
|
+
periods["endDateDeclaration"] = pd.to_datetime(periods.endDateDeclaration)
|
|
368
|
+
selected_period = periods.loc[
|
|
369
|
+
(periods.startDateDeclaration <= period_date)
|
|
370
|
+
& (periods.endDateDeclaration > period_date)
|
|
371
|
+
]
|
|
372
|
+
if len(selected_period) == 0:
|
|
373
|
+
raise NSIPPeriodMissing(period_date)
|
|
374
|
+
elif len(selected_period) > 1:
|
|
375
|
+
raise NSIPPeriodAmbiguous(period_date, len(selected_period))
|
|
376
|
+
|
|
377
|
+
return selected_period.iloc[0]["id"]
|
|
378
|
+
|
|
379
|
+
def get_declarations(self, period_date: datetime):
|
|
380
|
+
"""
|
|
381
|
+
Return the NSIP declaration list for the declaration period matching a given date (the
|
|
382
|
+
date must be included in the period).
|
|
383
|
+
|
|
384
|
+
:param period_date: date that must be inside the period
|
|
385
|
+
:return: declaration list as a dict
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
period_id = self.get_declaration_period_id(period_date)
|
|
389
|
+
|
|
390
|
+
url = self._get_api_url("lab_api", "declaration_list")
|
|
391
|
+
params = {"idPeriod": period_id}
|
|
392
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
393
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
394
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
395
|
+
|
|
396
|
+
declarations = r.json()
|
|
397
|
+
|
|
398
|
+
return declarations
|
|
399
|
+
|
|
400
|
+
def get_activities(self, project_activity: bool):
|
|
401
|
+
"""
|
|
402
|
+
Return the list of projects for the laboratory defined in NSIP
|
|
403
|
+
|
|
404
|
+
:param project_activity: true for projects, false for other activities
|
|
405
|
+
:return: activity list as a list
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
if project_activity:
|
|
409
|
+
activity_api = "project_list"
|
|
410
|
+
else:
|
|
411
|
+
activity_api = "reference_list"
|
|
412
|
+
|
|
413
|
+
url = self._get_api_url("lab_api", activity_api)
|
|
414
|
+
r = requests.get(
|
|
415
|
+
url,
|
|
416
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
417
|
+
)
|
|
418
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
419
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
420
|
+
|
|
421
|
+
activities = r.json()
|
|
422
|
+
|
|
423
|
+
return activities
|
|
424
|
+
|
|
425
|
+
def get_teams(self):
|
|
426
|
+
"""
|
|
427
|
+
Return the list of lab teams as a list of dict
|
|
428
|
+
|
|
429
|
+
:return: list
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
url = self._get_api_url("lab_api", "team_list")
|
|
433
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"})
|
|
434
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
435
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
436
|
+
|
|
437
|
+
teams = r.json()
|
|
438
|
+
|
|
439
|
+
return teams
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def check_nsip_base_config(nsip_config) -> bool:
|
|
443
|
+
"""
|
|
444
|
+
Check that main parameters are present in the NSIP configuration.
|
|
445
|
+
Return True if it is the case or raise an exception otherwise
|
|
446
|
+
|
|
447
|
+
:param nsip_config: dict containing NSIP parameters
|
|
448
|
+
:return: None
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
required_keys = {"server_url", "token"}
|
|
452
|
+
api_keys = [k for k in nsip_config if re.match(r".*_api$", k)]
|
|
453
|
+
|
|
454
|
+
for k in required_keys:
|
|
455
|
+
if k not in nsip_config:
|
|
456
|
+
raise ConfigMissingParam(f"nsip/{k}")
|
|
457
|
+
|
|
458
|
+
for k in api_keys:
|
|
459
|
+
required_subkeys = {"base_url"}
|
|
460
|
+
for sk in required_subkeys:
|
|
461
|
+
if sk not in nsip_config[k]:
|
|
462
|
+
raise ConfigMissingParam(f"nsip/{k}/{sk}")
|
|
463
|
+
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def nsip_session_init(nsip_config):
|
|
468
|
+
"""
|
|
469
|
+
Initialize the NSIP session, using the configuration parameters. It is valid for an
|
|
470
|
+
application not to initalize all APIs.
|
|
471
|
+
|
|
472
|
+
:param nsip_config: dict containing the NSIP configuration
|
|
473
|
+
:return: a NSIPConnection object
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
check_nsip_base_config(nsip_config)
|
|
477
|
+
|
|
478
|
+
if "agent_api" in nsip_config:
|
|
479
|
+
agent_api = nsip_config["agent_api"]
|
|
480
|
+
else:
|
|
481
|
+
agent_api = None
|
|
482
|
+
|
|
483
|
+
if "lab_api" in nsip_config:
|
|
484
|
+
lab_api = nsip_config["lab_api"]
|
|
485
|
+
else:
|
|
486
|
+
lab_api = None
|
|
487
|
+
|
|
488
|
+
if "institute_api" in nsip_config:
|
|
489
|
+
institute_api = nsip_config["institute_api"]
|
|
490
|
+
else:
|
|
491
|
+
institute_api = None
|
|
492
|
+
|
|
493
|
+
return NSIPConnection(
|
|
494
|
+
nsip_config["server_url"],
|
|
495
|
+
nsip_config["token"],
|
|
496
|
+
agent_api,
|
|
497
|
+
lab_api,
|
|
498
|
+
institute_api,
|
|
499
|
+
)
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = [ "poetry-core",]
|
|
3
|
-
build-backend = "poetry.core.masonry.api"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
[tool.poetry.dependencies]
|
|
22
|
-
python = "^3.8"
|
|
23
|
-
pandas = ">=2.2"
|
|
24
|
-
requests = "^2.28"
|
|
25
|
-
|
|
26
|
-
[project.license]
|
|
27
|
-
text = "BSD 3-Clause License"
|
|
28
|
-
|
|
29
|
-
[project.urls]
|
|
30
|
-
Homepage = "https://gitlab.in2p3.fr/hito/hito_tools"
|
|
31
|
-
"Bug Tracker" = "https://gitlab.in2p3.fr/hito/hito_tools/-/issues"
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "poetry-core",]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hito_tools"
|
|
7
|
+
version = "26.1"
|
|
8
|
+
description = "Modules for interacting with Hito and NSIP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [ { name = "Michel Jouvin", email = "michel.jouvin@ijclab.in2p3.fr" } ]
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
|
+
requires-python = ">3.11"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://gitlab.in2p3.fr/hito/hito_tools"
|
|
20
|
+
"Bug Tracker" = "https://gitlab.in2p3.fr/hito/hito_tools/-/issues"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|