hito_tools 24.8__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.dist-info/LICENSE +29 -0
- hito_tools-24.8.dist-info/METADATA +49 -0
- hito_tools-24.8.dist-info/RECORD +12 -0
- hito_tools-24.8.dist-info/WHEEL +4 -0
hito_tools/ad.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def convert_ad_datetime(timestamp):
|
|
5
|
+
"""
|
|
6
|
+
Convert an AD timestamp to a standard one
|
|
7
|
+
|
|
8
|
+
:param timestamp: AD timestamp
|
|
9
|
+
:return: standard timestamp
|
|
10
|
+
"""
|
|
11
|
+
return datetime.datetime(1601, 1, 1) + datetime.timedelta(seconds=int(timestamp) / 10000000)
|
hito_tools/agents.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import re
|
|
3
|
+
import unicodedata
|
|
4
|
+
from string import capwords
|
|
5
|
+
from typing import Dict, Set, Tuple
|
|
6
|
+
|
|
7
|
+
from .utils import str_to_list
|
|
8
|
+
|
|
9
|
+
HITO_ARCHIVED_FIELD = "Archivé ?"
|
|
10
|
+
HITO_EMAIL_FIELD = "email"
|
|
11
|
+
HITO_FIRSTNAME_FIELD = "Prénom"
|
|
12
|
+
HITO_LASTNAME_FIELD = "Nom"
|
|
13
|
+
HITO_OFFICE_FIELD = "Bureau"
|
|
14
|
+
HITO_PHONE_FIELD = "Téléphone"
|
|
15
|
+
HITO_RESEDA_EMAIL_FIELD = "ID Connexion"
|
|
16
|
+
HITO_TEAM_FIELD = "Équipe"
|
|
17
|
+
|
|
18
|
+
MAPPINGS_HITO_NAME = "Hito"
|
|
19
|
+
MAPPINGS_RESEDA_NAME = "RESEDA"
|
|
20
|
+
|
|
21
|
+
NSIP_AGENT_PROJECT_FIELD = "Id Projet"
|
|
22
|
+
NSIP_CONTACT_EMAIL_FIELD = "contact_email"
|
|
23
|
+
NSIP_DEPARTURE_DATE = "Date de fin de contrat"
|
|
24
|
+
NSIP_FIRSTNAME_FIELD = "firstname"
|
|
25
|
+
NSIP_LASTNAME_FIELD = "lastname"
|
|
26
|
+
NSIP_OFFICES_FIELD = "offices"
|
|
27
|
+
NSIP_RESEDA_EMAIL_FIELD = "username"
|
|
28
|
+
NSIP_PHONE_NUMBERS = "phone_numbers"
|
|
29
|
+
NSIP_TEAM_ID_FIELD = "team ID"
|
|
30
|
+
|
|
31
|
+
NSIP_INVALID_EMAIL_PATTERN = r"[0-9a-f]+(@in2p3\.fr)?$"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Agent:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
first_name: str,
|
|
38
|
+
last_name: str,
|
|
39
|
+
team: str = None,
|
|
40
|
+
email: str = None,
|
|
41
|
+
reseda_email: str = None,
|
|
42
|
+
team_id: int = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.firstname = first_name
|
|
45
|
+
self.lastname = last_name
|
|
46
|
+
self._email = email
|
|
47
|
+
# email_alias is the lab official email for the agent
|
|
48
|
+
self._email_alias = None
|
|
49
|
+
self._reseda_email = reseda_email
|
|
50
|
+
if len(self.firstname) == 0:
|
|
51
|
+
self.fullname = self.lastname
|
|
52
|
+
elif len(self.lastname) == 0:
|
|
53
|
+
self.fullname = self.firstname
|
|
54
|
+
else:
|
|
55
|
+
self.fullname = f"{self.firstname} {self.lastname}"
|
|
56
|
+
self._projects = set()
|
|
57
|
+
self._team = team
|
|
58
|
+
self._team_id = team_id
|
|
59
|
+
self._offices = set()
|
|
60
|
+
self._phones = set()
|
|
61
|
+
self._matched = False
|
|
62
|
+
self._disabled = False
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ascii_firstname(self) -> str:
|
|
66
|
+
return self._ascii_firstname
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def ascii_lastname(self) -> str:
|
|
70
|
+
return self._ascii_lastname
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def ascii_name(self) -> str:
|
|
74
|
+
return self._ascii_fullname
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def disabled(self) -> bool:
|
|
78
|
+
return self._disabled
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def email(self) -> str:
|
|
82
|
+
return self._email
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def email_alias(self) -> str:
|
|
86
|
+
return self._email_alias
|
|
87
|
+
|
|
88
|
+
@email_alias.setter
|
|
89
|
+
def email_alias(self, alias: str) -> None:
|
|
90
|
+
self._email_alias = alias
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def firstname(self) -> str:
|
|
94
|
+
return self._firstname
|
|
95
|
+
|
|
96
|
+
@firstname.setter
|
|
97
|
+
def firstname(self, firstname: str) -> None:
|
|
98
|
+
self._firstname = firstname
|
|
99
|
+
self._ascii_firstname = ascii_lower_nohyphen(firstname)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def fullname(self) -> str:
|
|
103
|
+
return self._fullname
|
|
104
|
+
|
|
105
|
+
@fullname.setter
|
|
106
|
+
def fullname(self, fullname: str) -> None:
|
|
107
|
+
self._fullname = fullname
|
|
108
|
+
self._ascii_fullname = ascii_lower_nohyphen(fullname)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def lastname(self) -> str:
|
|
112
|
+
return self._lastname
|
|
113
|
+
|
|
114
|
+
@lastname.setter
|
|
115
|
+
def lastname(self, lastname: str) -> None:
|
|
116
|
+
self._lastname = lastname
|
|
117
|
+
self._ascii_lastname = ascii_lower_nohyphen(lastname)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def matched(self) -> bool:
|
|
121
|
+
return self._matched
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def offices(self) -> Set[str]:
|
|
125
|
+
return self._offices
|
|
126
|
+
|
|
127
|
+
@offices.setter
|
|
128
|
+
def offices(self, office: str) -> None:
|
|
129
|
+
self._offices.add(office)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def phones(self) -> Set[str]:
|
|
133
|
+
return self._phones
|
|
134
|
+
|
|
135
|
+
@phones.setter
|
|
136
|
+
def phones(self, phone: str) -> None:
|
|
137
|
+
self._phones.add(phone)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def projects(self) -> Set[str]:
|
|
141
|
+
return self._projects
|
|
142
|
+
|
|
143
|
+
@projects.setter
|
|
144
|
+
def projects(self, project: str) -> None:
|
|
145
|
+
self._projects.add(project)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def reseda_email(self) -> str:
|
|
149
|
+
return self._reseda_email
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def team(self) -> str:
|
|
153
|
+
return self._team
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def team_id(self) -> str:
|
|
157
|
+
return self._team_id
|
|
158
|
+
|
|
159
|
+
def get_emails(self) -> Tuple[str, str]:
|
|
160
|
+
return self._email, self._reseda_email
|
|
161
|
+
|
|
162
|
+
def remove_office(self, office: str) -> None:
|
|
163
|
+
if office in self.offices:
|
|
164
|
+
self._offices.remove(office)
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def set_matched(self) -> None:
|
|
168
|
+
self._matched = True
|
|
169
|
+
|
|
170
|
+
def set_disabled(self) -> None:
|
|
171
|
+
self._disabled = True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def nsip_agent_active(agent_email: str):
|
|
175
|
+
"""
|
|
176
|
+
Returns true if the email is a valid email, meaning that the agent is active, False otherwise.
|
|
177
|
+
|
|
178
|
+
:param agent_email: email of the agent
|
|
179
|
+
:return: True or False
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
if re.match(NSIP_INVALID_EMAIL_PATTERN, agent_email):
|
|
183
|
+
return False
|
|
184
|
+
else:
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def capitalize_name(name):
|
|
189
|
+
"""
|
|
190
|
+
Captitalize a name composed of several words separated by withespaces or hyphens
|
|
191
|
+
|
|
192
|
+
:param name: name to capitalize
|
|
193
|
+
:return: the capitalized name
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
m = re.match(r".*(?P<sep>\s*-\s*)\S", name)
|
|
197
|
+
if m:
|
|
198
|
+
return capwords(name, m.group("sep"))
|
|
199
|
+
else:
|
|
200
|
+
return capwords(name)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def ascii_name(string: str) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Helper function to replace accented characters by ASCII characters and replace
|
|
206
|
+
multiple withespaces by only one space
|
|
207
|
+
|
|
208
|
+
:param string: string to convert
|
|
209
|
+
:return: string with only ASCII characters
|
|
210
|
+
"""
|
|
211
|
+
ascii_string = unicodedata.normalize("NFD", string).encode("ascii", "ignore").decode("ascii")
|
|
212
|
+
ascii_string = re.sub(r"\s+", " ", ascii_string)
|
|
213
|
+
return ascii_string
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def translate_to_ascii(string: str) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Helper function to translate a string with accented characters to ASCII and replace all
|
|
219
|
+
consecutive white spaces by a single '-' and suppress existing withespaces around '-'
|
|
220
|
+
|
|
221
|
+
:param string: string to convert
|
|
222
|
+
:return: string with only ASCII characters
|
|
223
|
+
"""
|
|
224
|
+
ascii_string = unicodedata.normalize("NFD", string).encode("ascii", "ignore").decode("ascii")
|
|
225
|
+
ascii_string = re.sub(r"\s+", "-", ascii_string)
|
|
226
|
+
return re.sub("-+", "-", ascii_string)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def ascii_lower(string: str) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Helper function to translate a string with accented characters to ASCII lowercase
|
|
232
|
+
characters and replace all consecutive white spaces by a single '-'.
|
|
233
|
+
|
|
234
|
+
:param string: string to convert
|
|
235
|
+
:return: string with only ASCII characters
|
|
236
|
+
"""
|
|
237
|
+
return translate_to_ascii(string).lower()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def ascii_lower_nohyphen(string: str) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Helper function that removes hyphens and whitespaces after all the transformation done by
|
|
243
|
+
ascii-lower()
|
|
244
|
+
|
|
245
|
+
:param string: string to convert
|
|
246
|
+
:return: string with only ASCII characters
|
|
247
|
+
"""
|
|
248
|
+
string = re.sub(r"\s", "", string)
|
|
249
|
+
return re.sub("-", "", ascii_lower(string))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def read_hito_agents(
|
|
253
|
+
file: str,
|
|
254
|
+
ignore_if_no_team: bool = False,
|
|
255
|
+
use_archived: bool = False,
|
|
256
|
+
ascii_name_only: bool = False,
|
|
257
|
+
) -> Dict[str, Agent]:
|
|
258
|
+
"""
|
|
259
|
+
Read a Hito export CSV and return a list of Agent as a dict where for each agent 2 keys are
|
|
260
|
+
added: one corresponding ot the full name (givenname + lastname) and the other to the
|
|
261
|
+
lowercase ASCII version of the fullname.
|
|
262
|
+
|
|
263
|
+
:param file: Hito export CSV
|
|
264
|
+
:param ignore_if_no_team: if True ignore agents not assigned to a team
|
|
265
|
+
:param use_archived: if true, also use archived users in the CSV file (if any)
|
|
266
|
+
:param ascii_name_only: use only the ASCII lowercase full name as a key in the agent list
|
|
267
|
+
:return: dict representing the agent list
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
agent_list: Dict[str, Agent] = {}
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
274
|
+
csv_reader = csv.DictReader(f, delimiter=";")
|
|
275
|
+
for e in csv_reader:
|
|
276
|
+
if (
|
|
277
|
+
HITO_ARCHIVED_FIELD in e
|
|
278
|
+
and e[HITO_ARCHIVED_FIELD].lower() == "o"
|
|
279
|
+
and not use_archived
|
|
280
|
+
):
|
|
281
|
+
continue
|
|
282
|
+
agent = Agent(
|
|
283
|
+
e[HITO_FIRSTNAME_FIELD],
|
|
284
|
+
e[HITO_LASTNAME_FIELD],
|
|
285
|
+
e[HITO_TEAM_FIELD],
|
|
286
|
+
e[HITO_EMAIL_FIELD].lower(),
|
|
287
|
+
e[HITO_RESEDA_EMAIL_FIELD].lower(),
|
|
288
|
+
)
|
|
289
|
+
if HITO_OFFICE_FIELD in e:
|
|
290
|
+
for office in str_to_list(e[HITO_OFFICE_FIELD]):
|
|
291
|
+
agent.offices = office
|
|
292
|
+
if HITO_PHONE_FIELD in e:
|
|
293
|
+
for phone_number in str_to_list(e[HITO_PHONE_FIELD]):
|
|
294
|
+
agent.phones = phone_number
|
|
295
|
+
# Ignore agents in Hito not assigned to a team
|
|
296
|
+
if ignore_if_no_team and len(e[HITO_TEAM_FIELD]) == 0:
|
|
297
|
+
print(f"INFO: '{agent.fullname}' doesn't belong to a team: ignoring it.")
|
|
298
|
+
continue
|
|
299
|
+
if not ascii_name_only:
|
|
300
|
+
agent_list[agent.fullname] = agent
|
|
301
|
+
# Also add a lowercase with no hyphen and no accented chars entry to help
|
|
302
|
+
# with matching
|
|
303
|
+
agent_list[agent.ascii_name] = agent
|
|
304
|
+
except: # noqa: E722
|
|
305
|
+
print(f"Error reading Hito CSV ({file})")
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
return agent_list
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_nsip_agents(nsip_session, context: str = "NSIP") -> Dict[str, Agent]:
|
|
312
|
+
"""
|
|
313
|
+
Function to retrieve agents from NSIP through the NSIP API and return a list of Agent as a
|
|
314
|
+
dict where for each agent 2 keys are added: one corresponding ot the full name (givenname
|
|
315
|
+
+ lastname) and the other to the lowercase ASCII version of the fullname.
|
|
316
|
+
|
|
317
|
+
:param nsip_session: a NSIPConnection object
|
|
318
|
+
:param context: either 'NSIP' (all agents presents at least one day during the semester)
|
|
319
|
+
or 'DIRECTORY' (only agents with an active contract)
|
|
320
|
+
:return: dict representing the list of agents
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
agent_list: Dict[str, Agent] = {}
|
|
324
|
+
|
|
325
|
+
agents = nsip_session.get_agent_list(context)
|
|
326
|
+
|
|
327
|
+
for e in agents:
|
|
328
|
+
agent = Agent(
|
|
329
|
+
e["firstname"], e["lastname"], e["team"], e["email"], e["email_reseda"], e["team_id"]
|
|
330
|
+
)
|
|
331
|
+
if e["offices"]:
|
|
332
|
+
for office in e["offices"]:
|
|
333
|
+
if len(office) > 0:
|
|
334
|
+
agent.offices = office
|
|
335
|
+
if e["phoneNumbers"]:
|
|
336
|
+
for number in e["phoneNumbers"]:
|
|
337
|
+
if len(number) > 0:
|
|
338
|
+
agent.phones = number
|
|
339
|
+
if not nsip_agent_active(agent.reseda_email):
|
|
340
|
+
agent.set_disabled()
|
|
341
|
+
agent_list[agent.fullname] = agent
|
|
342
|
+
|
|
343
|
+
return agent_list
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def name_mapping_exceptions(file: str) -> Dict[str, str]:
|
|
347
|
+
"""
|
|
348
|
+
Read a CSV defining the RESEDA/Hito name mapping for special cases and return them as a dict
|
|
349
|
+
where the key is the NSIP name and the value is the Hito name.
|
|
350
|
+
|
|
351
|
+
:param file: CSV file defining the exceptions
|
|
352
|
+
:return: dict
|
|
353
|
+
"""
|
|
354
|
+
hito_nsip_explicit_mappings: Dict[str, str] = {}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
358
|
+
mappings_reader = csv.DictReader(f, delimiter=";")
|
|
359
|
+
for e in mappings_reader:
|
|
360
|
+
hito_nsip_explicit_mappings[e[MAPPINGS_RESEDA_NAME]] = e[MAPPINGS_HITO_NAME]
|
|
361
|
+
except: # noqa: E722
|
|
362
|
+
print(f"Error reading Hito/RESEDA mappings CSV ({file})")
|
|
363
|
+
raise
|
|
364
|
+
|
|
365
|
+
return hito_nsip_explicit_mappings
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def match_hito_agent_name(
|
|
369
|
+
project_agent: Agent,
|
|
370
|
+
hito_agent_list: Dict[str, Agent],
|
|
371
|
+
hito_nsip_explicit_mappings: Dict[str, str] = {},
|
|
372
|
+
global_users: Dict[str, str] = {},
|
|
373
|
+
verbose: bool = False,
|
|
374
|
+
) -> Tuple[str, bool, str]:
|
|
375
|
+
"""
|
|
376
|
+
Function returning the Hito agent name matching a NSIP agent name, taking into account a list
|
|
377
|
+
of explicit mappins.
|
|
378
|
+
|
|
379
|
+
:param project_agent: project agent object
|
|
380
|
+
:param hito_agent_list: Hito agent list (a dict where the key is the agent full name)
|
|
381
|
+
:param hito_nsip_explicit_mappings: a list of explicit mappings
|
|
382
|
+
:return: Hito agent name or None if no match found, approximate_match flag (boolean),
|
|
383
|
+
approximate match criteria
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
approximate_matching = False
|
|
387
|
+
match_criteria = ""
|
|
388
|
+
project_agent_name = project_agent.fullname
|
|
389
|
+
|
|
390
|
+
if project_agent_name in hito_nsip_explicit_mappings:
|
|
391
|
+
hito_name = hito_nsip_explicit_mappings[project_agent_name]
|
|
392
|
+
print(
|
|
393
|
+
f"INFO: explicit Hito match defined for NSIP agent '{project_agent_name}': {hito_name}"
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
hito_name = project_agent_name
|
|
397
|
+
hito_ascii_name = ascii_lower_nohyphen(hito_name)
|
|
398
|
+
# global_users are pseudo-users used in the local project CSV that match a team
|
|
399
|
+
if (
|
|
400
|
+
hito_name in hito_agent_list
|
|
401
|
+
or hito_ascii_name in hito_agent_list
|
|
402
|
+
or hito_name in global_users
|
|
403
|
+
):
|
|
404
|
+
if hito_name not in hito_agent_list and hito_name not in global_users:
|
|
405
|
+
approximate_matching = True
|
|
406
|
+
match_criteria = "fullname spelling"
|
|
407
|
+
hito_name = hito_agent_list[hito_ascii_name].fullname
|
|
408
|
+
else:
|
|
409
|
+
_, project_agent_email = project_agent.get_emails()
|
|
410
|
+
# Remove duplicated entries
|
|
411
|
+
# First attempt to match by emails: as every entry is duplicated (full name + ascii name),
|
|
412
|
+
# 2 matches are expected
|
|
413
|
+
matching_agents = [
|
|
414
|
+
x.fullname
|
|
415
|
+
for x in hito_agent_list.values()
|
|
416
|
+
if re.match(f".*{project_agent_email}$", x.get_emails()[1])
|
|
417
|
+
]
|
|
418
|
+
expected_matches = 2
|
|
419
|
+
match_criteria = "email"
|
|
420
|
+
if len(matching_agents) == 0:
|
|
421
|
+
# If it failed, attempt a partial match (Hito name at the end of the project name)
|
|
422
|
+
matching_agents = [x for x in hito_agent_list.keys() if re.match(f".*{hito_name}$", x)]
|
|
423
|
+
expected_matches = 1
|
|
424
|
+
match_criteria = "partial fullname"
|
|
425
|
+
if len(matching_agents) == expected_matches:
|
|
426
|
+
approximate_matching = True
|
|
427
|
+
hito_name = matching_agents[0]
|
|
428
|
+
else:
|
|
429
|
+
if len(matching_agents) > 1:
|
|
430
|
+
print(
|
|
431
|
+
(
|
|
432
|
+
f"ERROR: approximate match for project agent '{hito_name}'"
|
|
433
|
+
f" failed, too many matches"
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
elif verbose:
|
|
437
|
+
print(f"ERROR: agent {hito_name} not found in Hito.")
|
|
438
|
+
hito_name = None
|
|
439
|
+
|
|
440
|
+
return hito_name, approximate_matching, match_criteria
|
hito_tools/core.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Definition of constants and helper functions shared by all scripts
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Singleton decorator definition
|
|
12
|
+
def singleton(cls):
|
|
13
|
+
instances = {}
|
|
14
|
+
|
|
15
|
+
def getinstance():
|
|
16
|
+
if cls not in instances:
|
|
17
|
+
instances[cls] = cls()
|
|
18
|
+
return instances[cls]
|
|
19
|
+
|
|
20
|
+
return getinstance
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@singleton
|
|
24
|
+
class GlobalParams:
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.logger = None
|
|
27
|
+
self.this_script = None
|
|
28
|
+
self.verbose = False
|
|
29
|
+
self.windows = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_os():
|
|
33
|
+
global_params = GlobalParams()
|
|
34
|
+
if os.name == "nt":
|
|
35
|
+
global_params.windows = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def debug(msg):
|
|
39
|
+
global_params = GlobalParams()
|
|
40
|
+
if global_params.logger:
|
|
41
|
+
global_params.logger.debug("{}".format(msg))
|
|
42
|
+
elif global_params.verbose:
|
|
43
|
+
print(msg)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def exception_handler(exception_type, exception, traceback, debug_hook=sys.excepthook):
|
|
47
|
+
global_params = GlobalParams()
|
|
48
|
+
if global_params.verbose:
|
|
49
|
+
debug_hook(exception_type, exception, traceback)
|
|
50
|
+
else:
|
|
51
|
+
print("{}: {} (use --verbose for details)".format(exception_type.__name__, exception))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def info(msg):
|
|
55
|
+
global_params = GlobalParams()
|
|
56
|
+
if global_params.logger:
|
|
57
|
+
global_params.logger.info("{}".format(msg))
|
|
58
|
+
else:
|
|
59
|
+
print(msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_windows():
|
|
63
|
+
global_params = GlobalParams()
|
|
64
|
+
if global_params.windows is None:
|
|
65
|
+
check_os()
|
|
66
|
+
return global_params.windows
|
hito_tools/exceptions.py
ADDED
|
@@ -0,0 +1,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 = " (file={file})"
|
|
24
|
+
self.msg = f"Invalid configuration parameter value ({value}) for '{param}'{file_msg}"
|
|
25
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
26
|
+
|
|
27
|
+
def __str__(self):
|
|
28
|
+
return repr(self.msg)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigMissingParam(Exception):
|
|
32
|
+
def __init__(self, param, file=None):
|
|
33
|
+
if file:
|
|
34
|
+
file_msg = " (file={file})"
|
|
35
|
+
else:
|
|
36
|
+
file_msg = ""
|
|
37
|
+
self.msg = f"Missing required configuration parameter '{param}'{file_msg}"
|
|
38
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return repr(self.msg)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OptionMissing(Exception):
|
|
45
|
+
def __init__(self, option):
|
|
46
|
+
self.msg = f"Option '{option}' required but missing"
|
|
47
|
+
self.status = EXIT_STATUS_CONFIG_ERROR
|
|
48
|
+
|
|
49
|
+
def __str__(self):
|
|
50
|
+
return repr(self.msg)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NSIPPeriodAmbiguous(Exception):
|
|
54
|
+
def __init__(self, date, num_matches):
|
|
55
|
+
if date is None:
|
|
56
|
+
date = datetime.now()
|
|
57
|
+
self.msg = f"Several declaration periods ({num_matches}) found in NSIP matching {date}"
|
|
58
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return repr(self.msg)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NSIPPeriodMissing(Exception):
|
|
65
|
+
def __init__(self, date):
|
|
66
|
+
self.msg = f"No declaration period found in NSIP matching {date}"
|
|
67
|
+
self.status = EXIT_STATUS_GENERAL_ERROR
|
|
68
|
+
|
|
69
|
+
def __str__(self):
|
|
70
|
+
return repr(self.msg)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SQLArrayMalformedValue(Exception):
|
|
74
|
+
def __init__(self, longtext, value, index):
|
|
75
|
+
self.msg = (
|
|
76
|
+
f"SQL longtext array: malformed value ({value}) at index {index}"
|
|
77
|
+
f" (longtext={longtext})"
|
|
78
|
+
)
|
|
79
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return repr(self.msg)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SQLInconsistentArrayLen(Exception):
|
|
86
|
+
def __init__(self, longtext, expected_length, actual_length):
|
|
87
|
+
self.msg = (
|
|
88
|
+
f"SQL longtext array: expected length is {expected_length} but actual length"
|
|
89
|
+
f" is {actual_length} (longtext={longtext})"
|
|
90
|
+
)
|
|
91
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
92
|
+
|
|
93
|
+
def __str__(self):
|
|
94
|
+
return repr(self.msg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SQLInvalidArray(Exception):
|
|
98
|
+
def __init__(self, longtext):
|
|
99
|
+
self.msg = f"Invalid SQL longtext array: {longtext}"
|
|
100
|
+
self.status = EXIT_STATUS_INVALID_SQL_VALUE
|
|
101
|
+
|
|
102
|
+
def __str__(self):
|
|
103
|
+
return repr(self.msg)
|