excel-list-transform 0.5__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.
- excel_list_transform/__init__.py +1 -0
- excel_list_transform/__main__.py +11 -0
- excel_list_transform/check_indata_common.py +39 -0
- excel_list_transform/commontypes.py +59 -0
- excel_list_transform/config.py +358 -0
- excel_list_transform/config_enums.py +53 -0
- excel_list_transform/config_excel_list_transform.py +278 -0
- excel_list_transform/config_factory.py +95 -0
- excel_list_transform/config_xls_list_refmt_name.py +40 -0
- excel_list_transform/config_xls_list_refmt_num.py +70 -0
- excel_list_transform/file_extension.py +30 -0
- excel_list_transform/file_must_exist.py +22 -0
- excel_list_transform/generate_cfg.py +374 -0
- excel_list_transform/generate_txt.py +259 -0
- excel_list_transform/handle_csv.py +73 -0
- excel_list_transform/handle_empty_column.py +28 -0
- excel_list_transform/handle_excel.py +161 -0
- excel_list_transform/num_named_conversion.py +60 -0
- excel_list_transform/rewrite_value.py +123 -0
- excel_list_transform/str_to_enum.py +39 -0
- excel_list_transform/transform_cmd.py +146 -0
- excel_list_transform/transform_func.py +50 -0
- excel_list_transform/transform_func_common.py +249 -0
- excel_list_transform/transform_func_named.py +136 -0
- excel_list_transform/transform_func_num.py +157 -0
- excel_list_transform-0.5.dist-info/LICENSE.txt +22 -0
- excel_list_transform-0.5.dist-info/METADATA +114 -0
- excel_list_transform-0.5.dist-info/RECORD +30 -0
- excel_list_transform-0.5.dist-info/WHEEL +5 -0
- excel_list_transform-0.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Take an excel file and and change columns before saving as excel or CSV."""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Take an excel file and and change columns before saving as excel or CSV."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from excel_list_transform.transform_cmd import transform_cmd # pragma: no cover # noqa: E501
|
|
9
|
+
|
|
10
|
+
if __name__ == '__main__': # pragma: no cover
|
|
11
|
+
transform_cmd()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Function for common indata checks.."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Callable, Optional
|
|
10
|
+
from excel_list_transform.commontypes import Data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_indata_common(indata: Data,
|
|
14
|
+
fix_indata_empty_rows: Callable[[Data], None]) \
|
|
15
|
+
-> None:
|
|
16
|
+
"""Check that the indata is well formed common part."""
|
|
17
|
+
assert isinstance(indata, list), 'Internal error. ' + \
|
|
18
|
+
f'Expected list of rows but got {type(indata).__name__}'
|
|
19
|
+
if len(indata) < 1:
|
|
20
|
+
msg = 'No rows in input data'
|
|
21
|
+
print(msg, file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
fix_indata_empty_rows(indata)
|
|
24
|
+
if len(indata) < 1:
|
|
25
|
+
msg = 'No columns in input data'
|
|
26
|
+
print(msg, file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
num_cols: Optional[int] = None
|
|
29
|
+
for row in indata:
|
|
30
|
+
rowlen: int = len(row)
|
|
31
|
+
if num_cols is None:
|
|
32
|
+
num_cols = rowlen
|
|
33
|
+
else:
|
|
34
|
+
assert num_cols is not None
|
|
35
|
+
if num_cols != rowlen:
|
|
36
|
+
msg = 'Number of columns different between lines. '
|
|
37
|
+
msg += f'Found {num_cols} and {rowlen}. Aborting.'
|
|
38
|
+
print(msg, file=sys.stderr)
|
|
39
|
+
sys.exit(1)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Common type definitions used for type hints."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import TypeAlias, Optional, cast, Sequence, Mapping, TypeVar, \
|
|
9
|
+
Any
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
# imports needed by mypy, but not by python:
|
|
12
|
+
from typing import Union, List, Dict # pylint: disable=unused-import,ungrouped-imports # noqa: E501
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# types used to describe input and output data
|
|
16
|
+
Value: TypeAlias = Optional[str | int | float | datetime]
|
|
17
|
+
NumRow: TypeAlias = list[Value]
|
|
18
|
+
NumRowSeq: TypeAlias = Sequence[Value]
|
|
19
|
+
NameRow: TypeAlias = dict[str, Value]
|
|
20
|
+
NameRowMap: TypeAlias = Mapping[str, Value]
|
|
21
|
+
NumData: TypeAlias = list[NumRow]
|
|
22
|
+
NumDataSeq: TypeAlias = list[NumRowSeq]
|
|
23
|
+
NameData: TypeAlias = list[NameRow]
|
|
24
|
+
NameDataMap: TypeAlias = Sequence[NameRowMap]
|
|
25
|
+
Data = TypeVar('Data', NumData, NameData)
|
|
26
|
+
DataCov = TypeVar('DataCov', NumDataSeq, NameDataMap)
|
|
27
|
+
|
|
28
|
+
JsonType: TypeAlias = \
|
|
29
|
+
'Union[None, int, str, bool, List[JsonType], Dict[str, JsonType]]'
|
|
30
|
+
|
|
31
|
+
# helper functions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def num_row_to_str_list(row: NumRowSeq) -> list[str]:
|
|
35
|
+
"""Convert NumRow to list of str."""
|
|
36
|
+
ret: list[str] = []
|
|
37
|
+
for i in row:
|
|
38
|
+
if isinstance(i, str):
|
|
39
|
+
ret.append(i)
|
|
40
|
+
elif i is None:
|
|
41
|
+
raise TypeError('Found None when expecting str.')
|
|
42
|
+
else:
|
|
43
|
+
ret.append(str(i))
|
|
44
|
+
return ret
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def str_list_to_num_row(row: list[str]) -> NumRow:
|
|
48
|
+
"""Convert list of str to NumRow."""
|
|
49
|
+
return cast(NumRow, row)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
T = TypeVar('T')
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_checked_type(value: Optional[Any], istype: type[T]) -> T:
|
|
56
|
+
"""Get value narrowed to be of type istype."""
|
|
57
|
+
assert value is not None
|
|
58
|
+
assert isinstance(value, istype)
|
|
59
|
+
return value
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Base class to read and write configurations."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import csv
|
|
12
|
+
from typing import Any, Optional, Type, TypeVar, Mapping, NamedTuple, Callable
|
|
13
|
+
from enum import Enum, IntEnum
|
|
14
|
+
from excel_list_transform.str_to_enum import string_to_enum_best_match
|
|
15
|
+
from excel_list_transform.config_enums import RewriteKind
|
|
16
|
+
from excel_list_transform.file_must_exist import file_must_exist
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Keya = TypeVar('Keya', str, Enum, RewriteKind)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigEncoder(json.JSONEncoder):
|
|
23
|
+
"""Encoder for enumerations in config."""
|
|
24
|
+
|
|
25
|
+
def default(self, o: object) -> object:
|
|
26
|
+
"""Define default encoding of enumerations."""
|
|
27
|
+
if isinstance(o, (Enum, IntEnum)):
|
|
28
|
+
return str(o.name)
|
|
29
|
+
return super().default(o)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfigBadJson(json.JSONDecodeError):
|
|
33
|
+
"""Exception class for bad JSON in configuration."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def over_ride_needed(stri: str) -> Any:
|
|
37
|
+
"""Tell programmer override is needed."""
|
|
38
|
+
if stri is not None:
|
|
39
|
+
msg = 'Override of Config.parse_converters needed.'
|
|
40
|
+
raise NotImplementedError(msg)
|
|
41
|
+
return 42
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
ParseConverter = NamedTuple('ParseConverter', [('result_type', type),
|
|
45
|
+
('func', Callable[..., Any]),
|
|
46
|
+
('args', dict[str, Any])])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Config():
|
|
50
|
+
"""Base class for classes that read and write configurations.
|
|
51
|
+
|
|
52
|
+
Derive from this class to and have the configuration parameters
|
|
53
|
+
as instance member variables of the derived class.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, from_json_data_text: Optional[str],
|
|
57
|
+
from_json_filename: Optional[str]) -> None:
|
|
58
|
+
"""Construct configuration base class.
|
|
59
|
+
|
|
60
|
+
Derived class __init__ must create all object variables
|
|
61
|
+
before calling super().__init__()
|
|
62
|
+
Derived class __init__ usually does checking after
|
|
63
|
+
call to super().__init__()
|
|
64
|
+
"""
|
|
65
|
+
self_keys = [i for i in vars(self).keys() if not
|
|
66
|
+
callable(getattr(self, i)) and not i.startswith('_')]
|
|
67
|
+
if not self_keys:
|
|
68
|
+
msg = 'No object variables in object of class derived from '
|
|
69
|
+
msg += 'Config. (Create object variables in __init__ before '
|
|
70
|
+
msg += 'calling super().__init__().)'
|
|
71
|
+
raise AttributeError(msg)
|
|
72
|
+
unchecked = getattr(self, '_unchecked_dicts', None)
|
|
73
|
+
if unchecked is None:
|
|
74
|
+
self._unchecked_dicts: list[str] = []
|
|
75
|
+
elif not isinstance(unchecked, list):
|
|
76
|
+
msg = '_unchecked_dicts must be a list'
|
|
77
|
+
raise TypeError(msg)
|
|
78
|
+
self._hook_dict = self.parse_converters()
|
|
79
|
+
if from_json_data_text is not None:
|
|
80
|
+
self.parse_json(from_json_data_text)
|
|
81
|
+
elif from_json_filename is not None:
|
|
82
|
+
self.read(from_json_filename)
|
|
83
|
+
|
|
84
|
+
def parse_converters(self) -> dict[str, ParseConverter]:
|
|
85
|
+
"""Get converters for use when parsing JSON.
|
|
86
|
+
|
|
87
|
+
Override in derived class.
|
|
88
|
+
Return None if no conversions.
|
|
89
|
+
Return dict of dict for use in json decoder hook.
|
|
90
|
+
Structure of return value shall be:
|
|
91
|
+
{key: {'result type': res_type, 'func': function,
|
|
92
|
+
'args': {arg_name: arg_value}}}.
|
|
93
|
+
"""
|
|
94
|
+
return {'in_type': ParseConverter(result_type=int,
|
|
95
|
+
func=over_ride_needed,
|
|
96
|
+
args={})}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def check_key_match(expected_keys: list[str],
|
|
100
|
+
j_keys: list[str],
|
|
101
|
+
ok_to_use_defaults: bool) -> None:
|
|
102
|
+
"""Check if keys in imported JSON match exptected keys."""
|
|
103
|
+
if not ok_to_use_defaults:
|
|
104
|
+
for i in expected_keys:
|
|
105
|
+
if i not in j_keys:
|
|
106
|
+
errmsg = f'No value for {i} in JSON data'
|
|
107
|
+
print(errmsg, file=sys.stderr)
|
|
108
|
+
raise KeyError(errmsg)
|
|
109
|
+
for i in j_keys:
|
|
110
|
+
if i not in expected_keys:
|
|
111
|
+
errmsg = f'Unexpected parameter {i} in JSON data'
|
|
112
|
+
print(errmsg, file=sys.stderr)
|
|
113
|
+
raise KeyError(errmsg)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def check_dict_parse(self_data: dict[str, Any], json_data: dict[str, Any],
|
|
117
|
+
key: str, ok_to_use_defaults: bool,
|
|
118
|
+
unchecked_dicts: list[str]) -> None:
|
|
119
|
+
"""Check and parse dictionary."""
|
|
120
|
+
if not isinstance(self_data, dict) and \
|
|
121
|
+
not isinstance(json_data, dict):
|
|
122
|
+
return
|
|
123
|
+
if isinstance(self_data, dict):
|
|
124
|
+
if not isinstance(json_data, dict):
|
|
125
|
+
errmsg = f'Not dictionary for {key} in JSON data'
|
|
126
|
+
print(errmsg, file=sys.stderr)
|
|
127
|
+
raise KeyError(errmsg)
|
|
128
|
+
if not isinstance(self_data, dict):
|
|
129
|
+
errmsg = f'Unexpected dictionary for {key} in JSON data'
|
|
130
|
+
print(errmsg, file=sys.stderr)
|
|
131
|
+
raise KeyError(errmsg)
|
|
132
|
+
if key in unchecked_dicts:
|
|
133
|
+
return
|
|
134
|
+
Config.check_key_match(list(self_data.keys()), list(json_data.keys()),
|
|
135
|
+
ok_to_use_defaults)
|
|
136
|
+
for i in self_data.keys():
|
|
137
|
+
if i in json_data:
|
|
138
|
+
Config.check_dict_parse(self_data[i], json_data[i], i,
|
|
139
|
+
ok_to_use_defaults, unchecked_dicts)
|
|
140
|
+
|
|
141
|
+
def _json_parse_obj_hook(self, indict: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
"""Convert str to correct type."""
|
|
143
|
+
hookd = self._hook_dict
|
|
144
|
+
if hookd is None:
|
|
145
|
+
return indict # pragma: no cover
|
|
146
|
+
ret = deepcopy(indict)
|
|
147
|
+
for key, value in ret.items():
|
|
148
|
+
if key in hookd:
|
|
149
|
+
parse_c = hookd[key]
|
|
150
|
+
if not isinstance(value, parse_c.result_type):
|
|
151
|
+
ret[key] = parse_c.func(value, **parse_c.args)
|
|
152
|
+
return ret
|
|
153
|
+
|
|
154
|
+
def parse_json(self, from_json_text: str,
|
|
155
|
+
ok_to_use_defaults: bool = False) -> None:
|
|
156
|
+
"""Parse a string of JSON data and set self to that."""
|
|
157
|
+
self._hook_dict = self.parse_converters()
|
|
158
|
+
hook = self._json_parse_obj_hook if self._hook_dict is not None \
|
|
159
|
+
else None
|
|
160
|
+
data = None
|
|
161
|
+
try:
|
|
162
|
+
data = json.loads(from_json_text, object_hook=hook)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
if isinstance(exc, NotImplementedError):
|
|
165
|
+
raise exc
|
|
166
|
+
msg = 'Config.parse_json failed to load JSON from string/file.\n'
|
|
167
|
+
msg += 'Probably incorrectly edited configuration,\n'
|
|
168
|
+
msg += 'or using wrong file (not config file) as configuration.\n'
|
|
169
|
+
msg += str(exc)
|
|
170
|
+
print(msg, file=sys.stderr)
|
|
171
|
+
if isinstance(exc, json.JSONDecodeError):
|
|
172
|
+
raise ConfigBadJson(msg=msg, doc=exc.doc, pos=exc.pos) from exc
|
|
173
|
+
raise ConfigBadJson(msg=msg, doc='', pos=0) from exc
|
|
174
|
+
self_keys = [i for i in vars(self).keys() if not
|
|
175
|
+
callable(getattr(self, i)) and not i.startswith('_')]
|
|
176
|
+
self.check_key_match(self_keys, data.keys(), ok_to_use_defaults)
|
|
177
|
+
for i in self_keys:
|
|
178
|
+
if i in data.keys():
|
|
179
|
+
self.check_dict_parse(getattr(self, i), data[i], i,
|
|
180
|
+
ok_to_use_defaults,
|
|
181
|
+
self._unchecked_dicts)
|
|
182
|
+
setattr(self, i, data[i])
|
|
183
|
+
|
|
184
|
+
def as_json_string(self) -> str:
|
|
185
|
+
"""Return string with this configuration as JSON data."""
|
|
186
|
+
data = {}
|
|
187
|
+
self_keys = [i for i in vars(self).keys() if not
|
|
188
|
+
callable(getattr(self, i)) and not i.startswith('_')]
|
|
189
|
+
for i in self_keys:
|
|
190
|
+
data[i] = getattr(self, i)
|
|
191
|
+
return json.dumps(data, sort_keys=True, indent=4, cls=ConfigEncoder)
|
|
192
|
+
|
|
193
|
+
def read(self, from_json_filename: str,
|
|
194
|
+
ok_to_use_defaults: bool = False) -> None:
|
|
195
|
+
"""Read configuration JSON from named file."""
|
|
196
|
+
file_must_exist(filename=from_json_filename,
|
|
197
|
+
with_content_txt='with configuration JSON input')
|
|
198
|
+
with open(from_json_filename, "r", encoding='UTF-8') as file:
|
|
199
|
+
data = file.read()
|
|
200
|
+
self.parse_json(data, ok_to_use_defaults)
|
|
201
|
+
|
|
202
|
+
def write(self, to_json_filename: str) -> None:
|
|
203
|
+
"""Create named file with configuration as JSON."""
|
|
204
|
+
with open(to_json_filename, "w", encoding='UTF-8') as file:
|
|
205
|
+
text = self.as_json_string()
|
|
206
|
+
file.write(text)
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def get_csv_dialect(name: Optional[str], delimiter: Optional[str], # pylint: disable=too-many-arguments, line-too-long, too-many-branches # noqa: E501
|
|
210
|
+
quoting: Optional[str], quotechar: Optional[str], # pylint: disable=too-many-arguments, line-too-long, too-many-branches # noqa: E501
|
|
211
|
+
lineterminator: Optional[str], # pylint: disable=too-many-arguments, line-too-long, too-many-branches # noqa: E501
|
|
212
|
+
escapechar: Optional[str] # pylint: disable=too-many-arguments, line-too-long, too-many-branches # noqa: E501
|
|
213
|
+
) -> type[csv.Dialect]: # pylint: disable=too-many-arguments, line-too-long, too-many-branches # noqa: E501
|
|
214
|
+
"""Get CSV dialect object matching arguments."""
|
|
215
|
+
ret: Optional[type[csv.Dialect]] = None
|
|
216
|
+
if name is None or name.lower() == 'csv.excel':
|
|
217
|
+
ret = csv.excel
|
|
218
|
+
ret.lineterminator = '\r\n'
|
|
219
|
+
elif name.lower() == 'csv.excel_tab':
|
|
220
|
+
ret = csv.excel_tab
|
|
221
|
+
ret.lineterminator = '\r\n'
|
|
222
|
+
elif name.lower() == 'csv.unix_dialect':
|
|
223
|
+
ret = csv.unix_dialect
|
|
224
|
+
ret.lineterminator = '\n'
|
|
225
|
+
else:
|
|
226
|
+
errmsg = f'Unknown csv dialect: {name}'
|
|
227
|
+
print(errmsg, file=sys.stderr)
|
|
228
|
+
raise KeyError(errmsg)
|
|
229
|
+
if delimiter is not None:
|
|
230
|
+
ret.delimiter = delimiter
|
|
231
|
+
if quoting is None:
|
|
232
|
+
ret.quoting = csv.QUOTE_MINIMAL
|
|
233
|
+
elif quoting.lower() == 'csv.quote_all':
|
|
234
|
+
ret.quoting = csv.QUOTE_ALL
|
|
235
|
+
elif quoting.lower() == 'csv.quote_minimal':
|
|
236
|
+
ret.quoting = csv.QUOTE_MINIMAL
|
|
237
|
+
elif quoting.lower() == 'csv.quote_none':
|
|
238
|
+
ret.quoting = csv.QUOTE_NONE
|
|
239
|
+
elif quoting.lower() == 'csv.quote_nonnumeric':
|
|
240
|
+
ret.quoting = csv.QUOTE_NONNUMERIC
|
|
241
|
+
else:
|
|
242
|
+
errmsg = f'Unknown csv quoting: {quoting}'
|
|
243
|
+
print(errmsg, file=sys.stderr)
|
|
244
|
+
raise KeyError(errmsg)
|
|
245
|
+
if quotechar is None:
|
|
246
|
+
ret.quotechar = '"'
|
|
247
|
+
else:
|
|
248
|
+
ret.quotechar = quotechar
|
|
249
|
+
if lineterminator is not None:
|
|
250
|
+
ret.lineterminator = lineterminator
|
|
251
|
+
if escapechar is None:
|
|
252
|
+
ret.escapechar = '\\'
|
|
253
|
+
else:
|
|
254
|
+
ret.escapechar = escapechar
|
|
255
|
+
return ret
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def check_array_keys(name_of_cfg: str, array: list[dict[str, Any]],
|
|
259
|
+
mandatory_keys: list[str],
|
|
260
|
+
allowed_keys: Optional[list[str]] = None) -> None:
|
|
261
|
+
"""Check an array of dicts that all keys match allowed/mandatory.
|
|
262
|
+
|
|
263
|
+
Check that all keys of the list of dict are allowed.
|
|
264
|
+
A key is allowed if listed in mandatory_keys of allowed_keys.
|
|
265
|
+
Check that all mandatory keys exist in all dicts in the list.
|
|
266
|
+
@param nam_of_cfg The name of the configuration used in error message.
|
|
267
|
+
@param array The array of dicts to check.
|
|
268
|
+
@param mandatory_keys An array of the mandatory keys.
|
|
269
|
+
@param allowed_keys An array of allowed keys excluding madatory keys.
|
|
270
|
+
"""
|
|
271
|
+
to_allow = deepcopy(mandatory_keys)
|
|
272
|
+
in_cfg = f' in config of {name_of_cfg}'
|
|
273
|
+
if allowed_keys is not None:
|
|
274
|
+
to_allow += deepcopy(allowed_keys)
|
|
275
|
+
for i in array:
|
|
276
|
+
for used_key in list(i.keys()):
|
|
277
|
+
if used_key not in to_allow:
|
|
278
|
+
bad_k = f'Found non-allowed key "{used_key}"'
|
|
279
|
+
print(bad_k + in_cfg, file=sys.stderr)
|
|
280
|
+
raise KeyError(bad_k + in_cfg)
|
|
281
|
+
for k in mandatory_keys:
|
|
282
|
+
if k not in list(i.keys()):
|
|
283
|
+
miss = f'Missing key "{k}"'
|
|
284
|
+
print(miss + in_cfg, file=sys.stderr)
|
|
285
|
+
raise KeyError(miss + in_cfg)
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def value_of_type(input_value: Any, to_type: Any) -> Any:
|
|
289
|
+
"""Convert input to given type."""
|
|
290
|
+
if isinstance(input_value, to_type):
|
|
291
|
+
return input_value
|
|
292
|
+
return to_type(input_value)
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def check_array_dicts(name_of_cfg: str, # pylint: disable=too-many-locals
|
|
296
|
+
array: list[dict[str, Any]],
|
|
297
|
+
kind_key: str, kind_type: type,
|
|
298
|
+
dict_of_templates: Mapping[Keya, Mapping[str, type]]
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Check a list of dicts that all dicts matches a template.
|
|
301
|
+
|
|
302
|
+
Check that all dicts in list of dicts matches one of the
|
|
303
|
+
templates. The templates are each a dict and all keys must
|
|
304
|
+
match one of the template dicts. For each key in matching
|
|
305
|
+
template the value must be of the template value type.
|
|
306
|
+
@param nam_of_cfg The name of the configuration used in error message.
|
|
307
|
+
@param array The array of dicts to check.
|
|
308
|
+
@param kind_key The key of the kind enum that determines template.
|
|
309
|
+
@param kind_type The type of enum that is used as kind enum.
|
|
310
|
+
@param dict_of_templates The dict of template dicts.
|
|
311
|
+
"""
|
|
312
|
+
Msgs = NamedTuple('Msgs', [('in_cfg', str), ('bad_arg', str),
|
|
313
|
+
('bad_templ', str)])
|
|
314
|
+
msgs = Msgs(in_cfg=f' in config of {name_of_cfg} ',
|
|
315
|
+
bad_arg='argument not list of dicts',
|
|
316
|
+
bad_templ='Internal error: template not dict of dicts')
|
|
317
|
+
if not isinstance(array, list):
|
|
318
|
+
print(msgs.bad_arg + msgs.in_cfg + '(list_of_dicts)',
|
|
319
|
+
file=sys.stderr)
|
|
320
|
+
raise KeyError(msgs.bad_arg + msgs.in_cfg + '(list_of_dicts)')
|
|
321
|
+
if not isinstance(dict_of_templates, dict):
|
|
322
|
+
print(msgs.bad_templ + msgs.in_cfg + '(dict_of_templates)',
|
|
323
|
+
file=sys.stderr)
|
|
324
|
+
raise KeyError(msgs.bad_templ + msgs.in_cfg +
|
|
325
|
+
'(dict_of_templates)')
|
|
326
|
+
for key1, template in dict_of_templates.items():
|
|
327
|
+
if not isinstance(template, dict):
|
|
328
|
+
msg = f' in template for {key1.name}'
|
|
329
|
+
print(msgs.bad_templ + msgs.in_cfg + msg, file=sys.stderr)
|
|
330
|
+
raise KeyError(msgs.bad_templ + msgs.in_cfg + msg)
|
|
331
|
+
for i, row in enumerate(array):
|
|
332
|
+
litem = f'(list index {i})'
|
|
333
|
+
if not isinstance(row, dict):
|
|
334
|
+
print(msgs.bad_arg + msgs.in_cfg + litem, file=sys.stderr)
|
|
335
|
+
raise KeyError(msgs.bad_arg + msgs.in_cfg + litem)
|
|
336
|
+
if kind_key not in row:
|
|
337
|
+
msg = f'Key {kind_key} not in dict'
|
|
338
|
+
print(msg + msgs.in_cfg + litem, file=sys.stderr)
|
|
339
|
+
raise KeyError(msg + msgs.in_cfg + litem)
|
|
340
|
+
kind = Config.value_of_type(row[kind_key], kind_type)
|
|
341
|
+
for key2, valtype in dict_of_templates[kind].items():
|
|
342
|
+
if key2 not in row:
|
|
343
|
+
msg = f'Key {key2} not in dict'
|
|
344
|
+
print(msg + msgs.in_cfg + litem, file=sys.stderr)
|
|
345
|
+
raise KeyError(msg + msgs.in_cfg + litem)
|
|
346
|
+
if not isinstance(row[key2], valtype):
|
|
347
|
+
msg = f'Value for key {key2} = {row[key2]} '
|
|
348
|
+
msg += f'is not {valtype.__name__} '
|
|
349
|
+
msg += f'it is {type(row[key2]).__name__} '
|
|
350
|
+
print(msg + msgs.in_cfg + litem, file=sys.stderr)
|
|
351
|
+
raise KeyError(msg + msgs.in_cfg + litem)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def get_converter_dict(enum_type: Type[Enum]) -> ParseConverter:
|
|
355
|
+
"""Get dict for converting to given enum_type."""
|
|
356
|
+
return ParseConverter(result_type=enum_type,
|
|
357
|
+
func=string_to_enum_best_match,
|
|
358
|
+
args={'num_type': enum_type})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Enumerations used in configuration of excel list transform."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from enum import Enum, auto
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileType(Enum):
|
|
12
|
+
"""Is I/O file excel or CSV."""
|
|
13
|
+
|
|
14
|
+
EXCEL = auto()
|
|
15
|
+
CSV = auto()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SplitWhere(Enum):
|
|
19
|
+
"""Use leftmost or rightmost separator for splitting."""
|
|
20
|
+
|
|
21
|
+
LEFTMOST = auto()
|
|
22
|
+
RIGHTMOST = auto()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExcelLib(Enum):
|
|
26
|
+
"""Use specified library for excel reading or writing."""
|
|
27
|
+
|
|
28
|
+
OPENPYXL = auto()
|
|
29
|
+
XLSXWRITER = auto()
|
|
30
|
+
PYLIGHTXL = auto()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RewriteKind(Enum):
|
|
34
|
+
"""Kind of write operation to apply."""
|
|
35
|
+
|
|
36
|
+
STRIP = auto()
|
|
37
|
+
REMOVECHARS = auto()
|
|
38
|
+
STR_SUBSTITUTE = auto()
|
|
39
|
+
REGEX_SUBSTITUTE = auto()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CaseSensitivity(Enum):
|
|
43
|
+
"""Shall matching be case sensitive or not."""
|
|
44
|
+
|
|
45
|
+
MATCH_CASE = auto()
|
|
46
|
+
IGNORE_CASE = auto()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ColumnRef(Enum):
|
|
50
|
+
"""Are columns referenced by number or name."""
|
|
51
|
+
|
|
52
|
+
BY_NUMBER = auto()
|
|
53
|
+
BY_NAME = auto()
|