hito_tools 24.8.dev1__py3-none-any.whl
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.
Potentially problematic release.
This version of hito_tools might be problematic. Click here for more details.
- hito_tools/ad.py +11 -0
- hito_tools/agents.py +440 -0
- hito_tools/core.py +66 -0
- hito_tools/exceptions.py +103 -0
- hito_tools/nsip.py +371 -0
- hito_tools/projects.py +175 -0
- hito_tools/teams.py +40 -0
- hito_tools/utils.py +231 -0
- hito_tools-24.8.dev1.dist-info/LICENSE +29 -0
- hito_tools-24.8.dev1.dist-info/METADATA +49 -0
- hito_tools-24.8.dev1.dist-info/RECORD +12 -0
- hito_tools-24.8.dev1.dist-info/WHEEL +4 -0
hito_tools/nsip.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
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
|
+
# Required config params describing the NSIP API in the configuration file
|
|
34
|
+
# The key is the API subpart and the value a set of required parameters describing the sub-urls.
|
|
35
|
+
# 'base_url' is an implicitly required parameter for each API part.
|
|
36
|
+
NSIP_API_REQUIRED_CONFIG = {
|
|
37
|
+
"agent_api": ["declaration_add", "declaration_delete", "declaration_update"],
|
|
38
|
+
"institute_api": ["declaration_period_list"],
|
|
39
|
+
"lab_api": ["agent_list", "declaration_list"],
|
|
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.agent_api = agent_api
|
|
69
|
+
self.institute_api = institute_api
|
|
70
|
+
self.lab_api = lab_api
|
|
71
|
+
|
|
72
|
+
def get_agent_list(self, context: str = "NSIP"):
|
|
73
|
+
"""
|
|
74
|
+
Retrieve NSIP agents from NSIP API and return a dict built from the retrieved JSON
|
|
75
|
+
|
|
76
|
+
:param context: either 'NSIP' (all agents presents at least one day during the semester)
|
|
77
|
+
or 'DIRECTORY' (only agents with an active contract)
|
|
78
|
+
:return: dict representing the JSON anwser
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
url = f"{self.server_url}{self.lab_api['base_url']}{self.lab_api['agent_list']}"
|
|
82
|
+
r = requests.get(
|
|
83
|
+
url,
|
|
84
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
85
|
+
params={"context": context},
|
|
86
|
+
)
|
|
87
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
88
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
89
|
+
|
|
90
|
+
agents = r.json()
|
|
91
|
+
|
|
92
|
+
return agents
|
|
93
|
+
|
|
94
|
+
def update_agent(
|
|
95
|
+
self,
|
|
96
|
+
reseda_email: str,
|
|
97
|
+
team_id: str = None,
|
|
98
|
+
email: str = None,
|
|
99
|
+
phones: List[str] = None,
|
|
100
|
+
offices: List[str] = None,
|
|
101
|
+
) -> int:
|
|
102
|
+
"""
|
|
103
|
+
Update agent attributes. Every attribute can be omitted if it should not
|
|
104
|
+
be modified. reseda_email is used to identify the user to modify and is
|
|
105
|
+
the only required parameter. An agent cannot be added through the API, it
|
|
106
|
+
has to exist before.
|
|
107
|
+
|
|
108
|
+
:param reseda_email: the user resedaEmail, used to identify the user
|
|
109
|
+
:param team_id: ID of user's new team
|
|
110
|
+
:param email: user's new email
|
|
111
|
+
:param phones: user's new phone list
|
|
112
|
+
:param offices: user's new office list
|
|
113
|
+
:return: status (0 for successful update, a positive value if an error occured),
|
|
114
|
+
http_status, http_reason
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
params = {"emailReseda": reseda_email, "context": "DIRECTORY"}
|
|
118
|
+
url = f"{self.server_url}{self.agent_api['base_url']}{self.agent_api['agent_update']}"
|
|
119
|
+
if team_id is not None:
|
|
120
|
+
params["teamId"] = team_id
|
|
121
|
+
if email is not None:
|
|
122
|
+
params["contactEmail"] = email
|
|
123
|
+
if phones is not None:
|
|
124
|
+
# To clear the phones, an empty string must be passed to work around an API error
|
|
125
|
+
if len(phones) == 0:
|
|
126
|
+
phones.add("")
|
|
127
|
+
params["phoneNumbers"] = json.dumps(phones, iterable_as_array=True)
|
|
128
|
+
if offices is not None:
|
|
129
|
+
# To clear the offices, an empty string must be passed to work around an API error
|
|
130
|
+
if len(offices) == 0:
|
|
131
|
+
offices.add("")
|
|
132
|
+
params["offices"] = json.dumps(offices, iterable_as_array=True)
|
|
133
|
+
r = requests.put(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
134
|
+
if r.content:
|
|
135
|
+
reason = r.content.decode("utf-8")
|
|
136
|
+
else:
|
|
137
|
+
reason = ""
|
|
138
|
+
|
|
139
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
140
|
+
status = 0
|
|
141
|
+
else:
|
|
142
|
+
status = 1
|
|
143
|
+
return status, r.status_code, reason
|
|
144
|
+
|
|
145
|
+
def update_declaration(
|
|
146
|
+
self,
|
|
147
|
+
email: str,
|
|
148
|
+
project_id: str,
|
|
149
|
+
project_type: bool,
|
|
150
|
+
time: int,
|
|
151
|
+
validation_date: datetime.date = None,
|
|
152
|
+
contract: int = None,
|
|
153
|
+
) -> int:
|
|
154
|
+
"""
|
|
155
|
+
Add or update a project declaration for a user specified by its RESEDA email
|
|
156
|
+
|
|
157
|
+
:param email: RESEDA email of the selected user
|
|
158
|
+
:param project_id: ID of the selected project
|
|
159
|
+
:param project_type: if True, it is a project, else it is a reference (other activities)
|
|
160
|
+
:param time: time spent on the project in the unit appropriate for the project (hour or
|
|
161
|
+
week)
|
|
162
|
+
:param validation_date: validation date
|
|
163
|
+
:param contract: contract ID in case the agent has multiple contracts for the period
|
|
164
|
+
:return: status (0 for successful add, -1 for successful update, positive value if errors),
|
|
165
|
+
http_status if errors else declaration ID added/modified, http_reason
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
url = f"{self.server_url}{self.agent_api['base_url']}{self.agent_api['declaration_add']}"
|
|
169
|
+
if project_type:
|
|
170
|
+
params = {"projectId": int(project_id), "referenceId": ""}
|
|
171
|
+
else:
|
|
172
|
+
params = {"projectId": "", "referenceId": int(project_id)}
|
|
173
|
+
params["context"] = "NSIP"
|
|
174
|
+
if contract:
|
|
175
|
+
print(f"INFO: updating {email} declaration using contract {contract}")
|
|
176
|
+
params["idAgentContract"] = contract
|
|
177
|
+
else:
|
|
178
|
+
params["emailReseda"] = email
|
|
179
|
+
params["time"] = time
|
|
180
|
+
if validation_date:
|
|
181
|
+
validation_date_str = validation_date.date().isoformat()
|
|
182
|
+
params["managerValidationDate"] = validation_date_str
|
|
183
|
+
r = requests.post(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
184
|
+
if r.content:
|
|
185
|
+
reason = r.content.decode("utf-8")
|
|
186
|
+
else:
|
|
187
|
+
reason = ""
|
|
188
|
+
|
|
189
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
190
|
+
m = DECLARATION_ADDED_PATTERN.match(reason)
|
|
191
|
+
if m:
|
|
192
|
+
declaration_id = m.group("decl_id")
|
|
193
|
+
else:
|
|
194
|
+
print(f"ERROR: unable to extract declaration number from request reason ({reason})")
|
|
195
|
+
status = 0
|
|
196
|
+
|
|
197
|
+
elif r.status_code == HTTP_STATUS_FORBIDDEN:
|
|
198
|
+
# If http status is Forbidden, parse the associated message. If it is the expected
|
|
199
|
+
# message for an already existing declaration, retrieve the declaration ID and
|
|
200
|
+
# update it.
|
|
201
|
+
m = DECLARATION_EXISTS_PATTERN.match(reason)
|
|
202
|
+
if m:
|
|
203
|
+
declaration_id = m.group("decl_id")
|
|
204
|
+
url = (
|
|
205
|
+
f"{self.server_url}{self.agent_api['base_url']}"
|
|
206
|
+
f"{self.agent_api['declaration_update']}"
|
|
207
|
+
)
|
|
208
|
+
params = {"id": declaration_id, "time": time, "context": "NSIP"}
|
|
209
|
+
if validation_date_str:
|
|
210
|
+
params["managerValidationDate"] = validation_date_str
|
|
211
|
+
status = -1
|
|
212
|
+
r = requests.put(
|
|
213
|
+
url,
|
|
214
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
215
|
+
params=params,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if r.status_code == HTTP_STATUS_OK:
|
|
219
|
+
return status, declaration_id, reason
|
|
220
|
+
else:
|
|
221
|
+
return 1, r.status_code, reason
|
|
222
|
+
|
|
223
|
+
def get_declaration_period_id(self, period_date: datetime):
|
|
224
|
+
"""
|
|
225
|
+
Return the declaration ID for the declaration period matching a given date (the date must
|
|
226
|
+
be included in the period.
|
|
227
|
+
|
|
228
|
+
:param period_date: date that must be inside the period
|
|
229
|
+
:return: declaration period ID
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
url = (
|
|
233
|
+
f"{self.server_url}{self.institute_api['base_url']}"
|
|
234
|
+
f"{self.institute_api['declaration_period_list']}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"})
|
|
238
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
239
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
240
|
+
|
|
241
|
+
periods = pd.read_json(StringIO(r.content.decode()))
|
|
242
|
+
periods["startDateDeclaration"] = pd.to_datetime(periods.startDateDeclaration)
|
|
243
|
+
periods["endDateDeclaration"] = pd.to_datetime(periods.endDateDeclaration)
|
|
244
|
+
selected_period = periods.loc[
|
|
245
|
+
(periods.startDateDeclaration <= period_date)
|
|
246
|
+
& (periods.endDateDeclaration > period_date)
|
|
247
|
+
]
|
|
248
|
+
if len(selected_period) == 0:
|
|
249
|
+
raise NSIPPeriodMissing(period_date)
|
|
250
|
+
elif len(selected_period) > 1:
|
|
251
|
+
raise NSIPPeriodAmbiguous(period_date, len(selected_period))
|
|
252
|
+
|
|
253
|
+
return selected_period.iloc[0]["id"]
|
|
254
|
+
|
|
255
|
+
def get_declarations(self, period_date: datetime):
|
|
256
|
+
"""
|
|
257
|
+
Return the NSIP declaration list for the declaration period matching a given date (the
|
|
258
|
+
date must be included in the period).
|
|
259
|
+
|
|
260
|
+
:param period_date: date that must be inside the period
|
|
261
|
+
:return: declaration list as a dict
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
period_id = self.get_declaration_period_id(period_date)
|
|
265
|
+
|
|
266
|
+
url = f"{self.server_url}{self.lab_api['base_url']}{self.lab_api['declaration_list']}"
|
|
267
|
+
params = {"idPeriod": period_id}
|
|
268
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"}, params=params)
|
|
269
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
270
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
271
|
+
|
|
272
|
+
declarations = r.json()
|
|
273
|
+
|
|
274
|
+
return declarations
|
|
275
|
+
|
|
276
|
+
def get_activities(self, project_activity: bool):
|
|
277
|
+
"""
|
|
278
|
+
Return the list of projects for the laboratory defined in NSIP
|
|
279
|
+
|
|
280
|
+
:param project_activity: true for projects, false for other activities
|
|
281
|
+
:return: activity list as a list
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
if project_activity:
|
|
285
|
+
activity_api = self.lab_api["project_list"]
|
|
286
|
+
else:
|
|
287
|
+
activity_api = self.lab_api["reference_list"]
|
|
288
|
+
|
|
289
|
+
url = f"{self.server_url}{self.lab_api['base_url']}{activity_api}"
|
|
290
|
+
r = requests.get(
|
|
291
|
+
url,
|
|
292
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
293
|
+
)
|
|
294
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
295
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
296
|
+
|
|
297
|
+
activities = r.json()
|
|
298
|
+
|
|
299
|
+
return activities
|
|
300
|
+
|
|
301
|
+
def get_teams(self):
|
|
302
|
+
"""
|
|
303
|
+
Return the list of lab teams as a list of dict
|
|
304
|
+
|
|
305
|
+
:return: list
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
url = f"{self.server_url}{self.lab_api['base_url']}{self.lab_api['team_list']}"
|
|
309
|
+
r = requests.get(url, headers={"Authorization": f"Bearer {self.token}"})
|
|
310
|
+
if r.status_code != HTTP_STATUS_OK:
|
|
311
|
+
raise NSIPRequestFailure(r.status_code, url)
|
|
312
|
+
|
|
313
|
+
teams = r.json()
|
|
314
|
+
|
|
315
|
+
return teams
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def check_nsip_config(nsip_config) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Check the NSIP parameters and return True if it os or raise an exception otherwise
|
|
321
|
+
|
|
322
|
+
:param nsip_config: dict containing NSIP parameters
|
|
323
|
+
:return:
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
required_keys = set(NSIP_API_REQUIRED_CONFIG.keys())
|
|
327
|
+
required_keys.update(["server_url", "token"])
|
|
328
|
+
for k in required_keys:
|
|
329
|
+
if k not in nsip_config:
|
|
330
|
+
raise ConfigMissingParam(f"nsip/{k}")
|
|
331
|
+
if k in NSIP_API_REQUIRED_CONFIG:
|
|
332
|
+
required_subkeys = set(NSIP_API_REQUIRED_CONFIG[k])
|
|
333
|
+
required_subkeys.add("base_url")
|
|
334
|
+
for sk in required_subkeys:
|
|
335
|
+
if sk not in nsip_config[k]:
|
|
336
|
+
raise ConfigMissingParam(f"nsip/{k}/{sk}")
|
|
337
|
+
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def nsip_session_init(nsip_config):
|
|
342
|
+
"""
|
|
343
|
+
Initialize the NSIP session, using the configuration parameters. It is valid for an
|
|
344
|
+
application not to initalize all APIs.
|
|
345
|
+
|
|
346
|
+
:param nsip_config: dict containing the NSIP configuration
|
|
347
|
+
:return: a NSIPConnection object
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
if "agent_api" in nsip_config:
|
|
351
|
+
agent_api = nsip_config["agent_api"]
|
|
352
|
+
else:
|
|
353
|
+
agent_api = None
|
|
354
|
+
|
|
355
|
+
if "lab_api" in nsip_config:
|
|
356
|
+
lab_api = nsip_config["lab_api"]
|
|
357
|
+
else:
|
|
358
|
+
lab_api = None
|
|
359
|
+
|
|
360
|
+
if "institute_api" in nsip_config:
|
|
361
|
+
institute_api = nsip_config["institute_api"]
|
|
362
|
+
else:
|
|
363
|
+
institute_api = None
|
|
364
|
+
|
|
365
|
+
return NSIPConnection(
|
|
366
|
+
nsip_config["server_url"],
|
|
367
|
+
nsip_config["token"],
|
|
368
|
+
agent_api,
|
|
369
|
+
lab_api,
|
|
370
|
+
institute_api,
|
|
371
|
+
)
|
hito_tools/projects.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, Set
|
|
4
|
+
|
|
5
|
+
from .agents import Agent
|
|
6
|
+
|
|
7
|
+
NSIP_ACTIVIY_NAME = "name"
|
|
8
|
+
NSIP_ACTIVITY_ID = "id"
|
|
9
|
+
NSIP_ACTIVIY_TYPE = "type"
|
|
10
|
+
NSIP_MASTERPROJECT_FIELD = "Master projet"
|
|
11
|
+
NSIP_PROJECT_FIELD = "Projet"
|
|
12
|
+
NSIP_PROJECT_ID_FIELD = "id projet"
|
|
13
|
+
|
|
14
|
+
LOCAL_MASTERPROJECT_NAME = "Local projects"
|
|
15
|
+
LOCAL_PROJECT_NAME_FIELD = "PROJET"
|
|
16
|
+
LOCAL_CSV_LAST_FIELD_BEFORE_AGENTS = "TOTAL \nnb semaines"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProjectActivity:
|
|
20
|
+
def __init__(self, master_project: str, project_name: str, nsip_id: str = None) -> None:
|
|
21
|
+
# Ensure that the project name in Hito starts by the master project name to ensure unicity.
|
|
22
|
+
# Use / as separator between master project and project names (used by Hito to identify
|
|
23
|
+
# project groups)
|
|
24
|
+
m = re.match(rf"{master_project}\s+\-\s+(?P<project>.*)", project_name)
|
|
25
|
+
if m:
|
|
26
|
+
project_name = m.group("project")
|
|
27
|
+
if len(master_project) == 0:
|
|
28
|
+
self._name = project_name
|
|
29
|
+
elif len(project_name) == 0:
|
|
30
|
+
self._name = master_project
|
|
31
|
+
else:
|
|
32
|
+
self._name = f"{master_project} / {project_name}"
|
|
33
|
+
self._id = nsip_id
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def id(self) -> str:
|
|
37
|
+
return self._id
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
return self._name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Project(ProjectActivity):
|
|
45
|
+
def __init__(self, masterproject: str, project: str, nsip_id: str = None) -> None:
|
|
46
|
+
super(Project, self).__init__(masterproject, project, nsip_id)
|
|
47
|
+
self._teams = set()
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def teams(self) -> Set:
|
|
51
|
+
return self._teams
|
|
52
|
+
|
|
53
|
+
@teams.setter
|
|
54
|
+
def teams(self, name: str) -> None:
|
|
55
|
+
self._teams.add(name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_nsip_projects(file: str) -> Dict[str, ProjectActivity]:
|
|
59
|
+
"""
|
|
60
|
+
Read a NSIP projects CSV file and return the project list as a dict where the key is the
|
|
61
|
+
project name and the value is a Project object.
|
|
62
|
+
|
|
63
|
+
:param file: NSIP project CSV
|
|
64
|
+
:return: list of projects as a dict
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
project_list: Dict[str, ProjectActivity] = {}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
71
|
+
nsip_reader = csv.DictReader(f, delimiter=";")
|
|
72
|
+
for e in nsip_reader:
|
|
73
|
+
project = Project(
|
|
74
|
+
e[NSIP_MASTERPROJECT_FIELD],
|
|
75
|
+
e[NSIP_PROJECT_FIELD],
|
|
76
|
+
e[NSIP_PROJECT_ID_FIELD],
|
|
77
|
+
)
|
|
78
|
+
project_list[e[NSIP_PROJECT_ID_FIELD]] = project
|
|
79
|
+
except: # noqa: E722
|
|
80
|
+
print(f"Error reading NSIP projects CSV ({file})")
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
return project_list
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_nsip_activities(
|
|
87
|
+
file: str, masterproject_names: Dict[str, str] = {}
|
|
88
|
+
) -> Dict[str, ProjectActivity]:
|
|
89
|
+
"""
|
|
90
|
+
Read a NSIP activities CSV file and return the activity list as a dict where the key is
|
|
91
|
+
the activity name and the value is a Project object. The master project is derived from
|
|
92
|
+
the activity type, the actual name being defined in the configuration. When the actual name
|
|
93
|
+
is None, the category is disabled.
|
|
94
|
+
|
|
95
|
+
:param file: NSIP project CSV
|
|
96
|
+
:param masterproject_names: a dict that defines the actual name of a NSIP category or disable
|
|
97
|
+
its use.
|
|
98
|
+
:return: list of projects as a dict
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
project_list: Dict[str, ProjectActivity] = {}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
105
|
+
nsip_reader = csv.DictReader(f, delimiter=";")
|
|
106
|
+
for e in nsip_reader:
|
|
107
|
+
m = re.match(r"(?P<category>.*)reference$", e[NSIP_ACTIVIY_TYPE])
|
|
108
|
+
if m:
|
|
109
|
+
category = m.group("category")
|
|
110
|
+
else:
|
|
111
|
+
category = e[NSIP_ACTIVIY_TYPE]
|
|
112
|
+
if category in masterproject_names:
|
|
113
|
+
master_project = masterproject_names[category]
|
|
114
|
+
if master_project is None:
|
|
115
|
+
continue
|
|
116
|
+
else:
|
|
117
|
+
master_project = category
|
|
118
|
+
project = Project(master_project, e[NSIP_ACTIVIY_NAME], e[NSIP_ACTIVITY_ID])
|
|
119
|
+
project_list[project.name] = project
|
|
120
|
+
except: # noqa: E722
|
|
121
|
+
print(f"Error reading NSIP activities CSV ({file})")
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
return project_list
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def read_local_projects(file: str) -> Dict[str, ProjectActivity]:
|
|
128
|
+
"""
|
|
129
|
+
Read the CSV of local projects maintained by CeMaP and return the project list and agent list.
|
|
130
|
+
The project list is as a dict where the key is the project name and the value is a Project
|
|
131
|
+
object. In this CSV, agents assigned to projects are column names.
|
|
132
|
+
|
|
133
|
+
:param file: local project CSV
|
|
134
|
+
:return: list of projects as a dict
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
project_list: Dict[str, ProjectActivity] = {}
|
|
138
|
+
agent_list: Dict[str, Agent] = {}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
142
|
+
nsip_reader = csv.DictReader(f, delimiter=";")
|
|
143
|
+
agents_found = False
|
|
144
|
+
|
|
145
|
+
# Retrieve agents
|
|
146
|
+
for field in nsip_reader.fieldnames:
|
|
147
|
+
if agents_found:
|
|
148
|
+
agent = Agent("", field)
|
|
149
|
+
agent_list[agent.get_fullname()] = agent
|
|
150
|
+
elif field == LOCAL_CSV_LAST_FIELD_BEFORE_AGENTS:
|
|
151
|
+
agents_found = True
|
|
152
|
+
if len(agent_list.keys()) == 0:
|
|
153
|
+
raise Exception(
|
|
154
|
+
(
|
|
155
|
+
f"Malformed CSV for local projects: column"
|
|
156
|
+
f" '{LOCAL_CSV_LAST_FIELD_BEFORE_AGENTS}' missing"
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Retrieve projects and set the project list for each agent
|
|
161
|
+
# Ignore entries with an empty project name
|
|
162
|
+
for e in nsip_reader:
|
|
163
|
+
if len(e[LOCAL_PROJECT_NAME_FIELD]) == 0:
|
|
164
|
+
continue
|
|
165
|
+
project = Project(LOCAL_MASTERPROJECT_NAME, e[LOCAL_PROJECT_NAME_FIELD])
|
|
166
|
+
project_list[project.name] = project
|
|
167
|
+
for agent in agent_list.values():
|
|
168
|
+
if e[agent.get_fullname()]:
|
|
169
|
+
agent.add_project(project.name)
|
|
170
|
+
|
|
171
|
+
except: # noqa: E722
|
|
172
|
+
print(f"Error reading NSIP projects CSV ({file})")
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
return project_list, agent_list
|
hito_tools/teams.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
TEAM_CSV_TEAM_ID = "id"
|
|
4
|
+
TEAM_CSV_TEAM_NAME = "name"
|
|
5
|
+
NSIP_NO_TEAM_ID = "770"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Team:
|
|
9
|
+
def __init__(self, name: str, id: str) -> None:
|
|
10
|
+
self._name = name
|
|
11
|
+
self._id = id
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def id(self) -> str:
|
|
15
|
+
return self._id
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return self._name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_nsip_team_ids(nsip_session) -> Dict[str, Team]:
|
|
23
|
+
"""
|
|
24
|
+
Return the team list read from NSIP as a dict where the key is the team name and
|
|
25
|
+
the value a Team object. The team name is the friendlyName attribute if defined, else
|
|
26
|
+
the name attribute.
|
|
27
|
+
|
|
28
|
+
:return: dict
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
teams = {}
|
|
32
|
+
team_list = nsip_session.get_teams()
|
|
33
|
+
for t in team_list:
|
|
34
|
+
if "friendlyName" in t and t["friendlyName"]:
|
|
35
|
+
team_name = t["friendlyName"]
|
|
36
|
+
else:
|
|
37
|
+
team_name = t["name"]
|
|
38
|
+
teams[team_name] = Team(team_name, t["id"])
|
|
39
|
+
|
|
40
|
+
return teams
|