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/utils.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Dict, List, Set
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .core import debug
|
|
10
|
+
from .exceptions import SQLArrayMalformedValue, SQLInconsistentArrayLen, SQLInvalidArray
|
|
11
|
+
|
|
12
|
+
# Address fix CSV columns
|
|
13
|
+
ADDR_FIX_BUILT_ADDR = "Hito-based email"
|
|
14
|
+
ADDR_FIX_FIXED_ADDR = "Fixed email"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_config_path_default(input_file_dir=None, main_script=None):
|
|
18
|
+
"""
|
|
19
|
+
Compute the default location to use for the configuration file path, using the directory of
|
|
20
|
+
the input file if the configuration file exists in it else the script directory. The file name
|
|
21
|
+
is based on the script name with the .cfg extension. The file path returned is an
|
|
22
|
+
absolute path so that it is handled properly by load_config_file (that adds the current
|
|
23
|
+
script directory if the path is relative).
|
|
24
|
+
|
|
25
|
+
:param input_file_dir: directory where the input file resides
|
|
26
|
+
:param main_script: path of the main script, defaults to sys.modules["__main__"]
|
|
27
|
+
:return: actual default for the configuration file absolute path + default file name
|
|
28
|
+
"""
|
|
29
|
+
if main_script is None:
|
|
30
|
+
main_script = sys.modules["__main__"].__file__
|
|
31
|
+
config_file_name = "{}.cfg".format(os.path.splitext(os.path.basename(main_script))[0])
|
|
32
|
+
if input_file_dir is None:
|
|
33
|
+
config_file_path = None
|
|
34
|
+
else:
|
|
35
|
+
if input_file_dir == "":
|
|
36
|
+
input_file_dir = os.getcwd()
|
|
37
|
+
config_file_path = os.path.join(input_file_dir, config_file_name)
|
|
38
|
+
if config_file_path is None or not os.path.exists(config_file_path):
|
|
39
|
+
config_file_path = os.path.join(os.path.dirname(main_script), config_file_name)
|
|
40
|
+
config_file_path = os.path.abspath(config_file_path)
|
|
41
|
+
debug("DEBUG: using configuration file {}".format(config_file_path))
|
|
42
|
+
return config_file_path, config_file_name
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_config_file(config_file: str, required: bool = False):
|
|
46
|
+
"""
|
|
47
|
+
Load the config file, apply defaults and return the corresponding dict.
|
|
48
|
+
If config file is not absolute, prefix with directory where this script resides
|
|
49
|
+
|
|
50
|
+
:param config_file: config file name
|
|
51
|
+
:param required: if True and the file is missing, raise an Exception
|
|
52
|
+
:return: dict containing the options
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if not os.path.isabs(config_file):
|
|
56
|
+
this_script_dir = os.path.dirname(sys.modules["__main__"].__file__)
|
|
57
|
+
if len(this_script_dir) == 0:
|
|
58
|
+
this_script_dir = os.path.curdir
|
|
59
|
+
config_file = os.path.join(this_script_dir, config_file)
|
|
60
|
+
else:
|
|
61
|
+
config_file = config_file
|
|
62
|
+
try:
|
|
63
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
64
|
+
config_options = yaml.safe_load(f)
|
|
65
|
+
except IOError as e:
|
|
66
|
+
if e.errno == 2:
|
|
67
|
+
if required:
|
|
68
|
+
print("ERROR: Configuration file ({}) is missing.".format(config_file))
|
|
69
|
+
raise e
|
|
70
|
+
else:
|
|
71
|
+
print("WARNING: Configuration file ({}) is missing.".format(config_file))
|
|
72
|
+
# Return an empty dict if config file is missing
|
|
73
|
+
config_options = {}
|
|
74
|
+
else:
|
|
75
|
+
raise Exception(
|
|
76
|
+
"Error opening configuration file ({}): {} (errno={})".format(
|
|
77
|
+
config_file, e.strerror, e.errno
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
|
|
81
|
+
raise Exception(
|
|
82
|
+
"Configuration file ({}) has an invalid format: ({})".format(config_file, e)
|
|
83
|
+
)
|
|
84
|
+
except: # noqa: E722
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
return config_options
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_email_fixes(file: str) -> Dict[str, str]:
|
|
91
|
+
"""
|
|
92
|
+
Read a CSV file mapping the email defined in Hito to theIJCLab (prenom.nom@ijclab.in2p3.fr)
|
|
93
|
+
adresses for persons who the IJCLab address cannot be guessed from the firstname/lastname
|
|
94
|
+
defined in Hito. Returns a dict where the key is the address built from Hito name and the
|
|
95
|
+
value the actual address to use.
|
|
96
|
+
|
|
97
|
+
:param file: CSV file name
|
|
98
|
+
:return: dict with the mapping to the correct email address
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
address_fixes: Dict[str, str] = {}
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
105
|
+
fix_reader = csv.DictReader(f, delimiter=";")
|
|
106
|
+
for e in fix_reader:
|
|
107
|
+
if (
|
|
108
|
+
re.match(r"\w[\w\-\.]+@.+\..+", e[ADDR_FIX_FIXED_ADDR])
|
|
109
|
+
or e[ADDR_FIX_FIXED_ADDR] == "-"
|
|
110
|
+
):
|
|
111
|
+
address_fixes[e[ADDR_FIX_BUILT_ADDR]] = e[ADDR_FIX_FIXED_ADDR]
|
|
112
|
+
elif len(e[ADDR_FIX_FIXED_ADDR]) == 0:
|
|
113
|
+
print(f"WARNING: fixed address empty for {e[ADDR_FIX_BUILT_ADDR]}")
|
|
114
|
+
else:
|
|
115
|
+
print(f"ERROR: invalid fixed address for {e[ADDR_FIX_BUILT_ADDR]}")
|
|
116
|
+
except: # noqa: E722
|
|
117
|
+
print(f"Error reading address fixes CSV ({file})")
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
return address_fixes
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def str_to_list(string: str) -> List[str]:
|
|
124
|
+
"""
|
|
125
|
+
Tokenize a string using / as a separator. Used to transform an Hito office or phone numer
|
|
126
|
+
into a list. Return an empty list is the string is empty.
|
|
127
|
+
|
|
128
|
+
:param string: string to parse
|
|
129
|
+
:return: list of string
|
|
130
|
+
"""
|
|
131
|
+
if len(string) == 0:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
tokens = string.split("/")
|
|
135
|
+
str_list = [tokens[0].strip()]
|
|
136
|
+
if len(tokens) > 1:
|
|
137
|
+
m = re.match(r"(?P<prefix>(?:\d\d(?:\.| )*){4})\d\d$", str_list[0])
|
|
138
|
+
if not m:
|
|
139
|
+
m = re.match(r"(?P<prefix>\w+\-)\w+$", str_list[0])
|
|
140
|
+
if m:
|
|
141
|
+
prefix = m.group("prefix")
|
|
142
|
+
else:
|
|
143
|
+
prefix = ""
|
|
144
|
+
for tok in tokens[1:]:
|
|
145
|
+
tok = tok.strip()
|
|
146
|
+
if re.match(r"\w+$", tok):
|
|
147
|
+
str_list.append(f"{prefix}{tok}")
|
|
148
|
+
else:
|
|
149
|
+
str_list.append(tok)
|
|
150
|
+
return str_list
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def list_to_str(object_list: List[str], separator: str = "|") -> str:
|
|
154
|
+
"""
|
|
155
|
+
Take a list of strings and returns a string with each element separated by the given separator.
|
|
156
|
+
|
|
157
|
+
:param object_list: the list of strings to convert
|
|
158
|
+
:param separator: the separator to use in the return string
|
|
159
|
+
:return: input list as a string
|
|
160
|
+
"""
|
|
161
|
+
if object_list is None:
|
|
162
|
+
return ""
|
|
163
|
+
else:
|
|
164
|
+
return separator.join(object_list)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def sql_serialize_list(values: Set[str]) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Build a SQL longtext value from a list of string
|
|
170
|
+
|
|
171
|
+
:param values: set of strings to serialize
|
|
172
|
+
:return: string in SQL longtext format
|
|
173
|
+
"""
|
|
174
|
+
# Add an empty value if none are present
|
|
175
|
+
if len(values) == 0:
|
|
176
|
+
values.add("")
|
|
177
|
+
longtext = f"a:{len(values)}:{{"
|
|
178
|
+
i = 0
|
|
179
|
+
for v in values:
|
|
180
|
+
longtext += f'i:{i};s:{len(v)}:"{v}";'
|
|
181
|
+
i += 1
|
|
182
|
+
longtext += "}"
|
|
183
|
+
return longtext
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def sql_longtext_to_list(value: str) -> List[str]:
|
|
187
|
+
"""
|
|
188
|
+
Deserialize a SQL longtext value and return it as a list. If the value is NULL or (null),
|
|
189
|
+
return an empty list: this value will be updated only if there is a non-null value in Hito.
|
|
190
|
+
|
|
191
|
+
:param value: SQL longtext string
|
|
192
|
+
:return: list of string
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
if re.match(r"\(*null", value.lower()):
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
m = re.match(r"a:(?P<list_len>\d+):\{(?P<list>.*)\}", value)
|
|
199
|
+
if not m:
|
|
200
|
+
raise SQLInvalidArray(value)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
list_length = int(m.group("list_len"))
|
|
204
|
+
tokens = m.group("list").split(";")
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print(repr(e))
|
|
207
|
+
raise SQLInvalidArray(value)
|
|
208
|
+
|
|
209
|
+
# The string tokens is separated by ';' and there is one ';' at the end.
|
|
210
|
+
# Each token is made of two parts separated also by a ';' : the index and the value
|
|
211
|
+
actual_length = (len(tokens) - 1) / 2.0
|
|
212
|
+
if list_length != actual_length:
|
|
213
|
+
raise SQLInconsistentArrayLen(value, list_length, actual_length)
|
|
214
|
+
|
|
215
|
+
i = 0
|
|
216
|
+
value_list = []
|
|
217
|
+
for i in range(1, 2 * list_length, 2):
|
|
218
|
+
m = re.match(
|
|
219
|
+
(
|
|
220
|
+
r"s:(\d+):(?P<backslashes>\\\\)*(?:(?P<sq>')|(?P<dq>\"))(?P<string>.*)"
|
|
221
|
+
r"(?(backslashes)P=backslashes)(?(sq)(?P<fsq>')|(?(dq)(?P<fdq>\")))$"
|
|
222
|
+
),
|
|
223
|
+
tokens[i],
|
|
224
|
+
)
|
|
225
|
+
if not m:
|
|
226
|
+
raise SQLArrayMalformedValue(value, tokens[i], i / 2)
|
|
227
|
+
# Do not add empty values
|
|
228
|
+
if len(m.group("string")) > 0:
|
|
229
|
+
value_list.append(m.group("string"))
|
|
230
|
+
|
|
231
|
+
return value_list
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Michel Jouvin
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: hito_tools
|
|
3
|
+
Version: 24.8.dev1
|
|
4
|
+
Summary: Modules for interacting with Hito and NSIP
|
|
5
|
+
Author: Michel Jouvin
|
|
6
|
+
Author-email: michel.jouvin@ijclab.in2p3.fr
|
|
7
|
+
Requires-Python: >=3.8,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
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)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# hito_tools module
|
|
19
|
+
|
|
20
|
+
Ce repository contient le module Pyhton `hito_tools` utilisé par les autres outils Python autour de Hito (OSITAH, hito2lists...).
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Environnement Python
|
|
26
|
+
|
|
27
|
+
L'installation de [hito_tools](https://pypi.org/project/hito-tools) nécessite un environnement Python avec une version >= 3.8.
|
|
28
|
+
Il est conseillé d'utiliser un environnement
|
|
29
|
+
virtuel pour chaque groupe d'applications et de déployer le module `hito_tools` dans cet environnemnt. Il est recommandé d'utiliser
|
|
30
|
+
une distribution de Python totalement indépendante du système d'exploitation comme [pyenv](https://github.com/pyenv/pyenv),
|
|
31
|
+
[poetry](https://python-poetry.org) ou [Anaconda](https://www.anaconda.com/products/individual). Pour la création d'un
|
|
32
|
+
environnement virtuel avec Conda, voir la
|
|
33
|
+
[documentation spécifique](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands).
|
|
34
|
+
|
|
35
|
+
Les modules Python requis par ce module sont :
|
|
36
|
+
* pandas (conda-forge)
|
|
37
|
+
* requests (conda-forge)
|
|
38
|
+
|
|
39
|
+
Avec `conda`, il faut utiliser l'option `-c conda-forge` lors de la commande `conda install`.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Installation du module hito_tools
|
|
43
|
+
|
|
44
|
+
L'installation se fait avec la commande `pip` de l'environnement Python utilisé :
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install hito_tools
|
|
48
|
+
```
|
|
49
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
hito_tools/ad.py,sha256=sfpIhJ7ZcxtYOW6ZH491xY7vHvhlwNtK4qZeEGRFyAI,292
|
|
2
|
+
hito_tools/agents.py,sha256=VmVG6Lm5CmHIsEaEn9A9hYPHWt5s2D4lIONNswr0uC8,14582
|
|
3
|
+
hito_tools/core.py,sha256=ulwB4pxHSYRw7V4wormmJcGHkXQ1k5j9u93jCUbZWbY,1505
|
|
4
|
+
hito_tools/exceptions.py,sha256=iBqHwI_800nYcXaujafRQes0z5_AkeyCVFw2wKVjPRU,3041
|
|
5
|
+
hito_tools/nsip.py,sha256=yDs0RjwJK94tYOpl4--GeARCWXQ9LNbz6TdOhE206k4,13691
|
|
6
|
+
hito_tools/projects.py,sha256=FuY2ow75mu9O--uG7ahrDoIEoJCmjIK7EOxaL9RXmIE,6316
|
|
7
|
+
hito_tools/teams.py,sha256=Iyc3c483fo-PsN-_gTO5sG_Pw3hYinnvsdZRMz9mZHE,974
|
|
8
|
+
hito_tools/utils.py,sha256=XfDFYOs5W8CpSBpcLAsePnJQILXIuVaoH2j3avj4Mzo,8395
|
|
9
|
+
hito_tools-24.8.dev1.dist-info/LICENSE,sha256=2C86YWCx1fvz92WySupcb6_t4NhHCVPE_ucy0YMTuoc,1550
|
|
10
|
+
hito_tools-24.8.dev1.dist-info/METADATA,sha256=ehNzFkgp-TDzt4cBMTaJ5LSDKiF5VPGA8d6XUqcXZow,1918
|
|
11
|
+
hito_tools-24.8.dev1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
12
|
+
hito_tools-24.8.dev1.dist-info/RECORD,,
|