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,278 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Read and write configuration of excel list transform."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from typing import Optional, Callable, TypeAlias, TypeVar, NamedTuple, Generic
|
|
13
|
+
from csv import Dialect
|
|
14
|
+
from excel_list_transform.config import Config, ParseConverter
|
|
15
|
+
from excel_list_transform.config_enums import FileType, SplitWhere, \
|
|
16
|
+
ExcelLib, RewriteKind, CaseSensitivity, ColumnRef
|
|
17
|
+
from excel_list_transform.str_to_enum import string_to_enum_best_match
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CsvSpec: TypeAlias = dict[str, Optional[str]]
|
|
21
|
+
Column = TypeVar('Column', int, str)
|
|
22
|
+
SingleRule: TypeAlias = dict[str, Optional[Column | str]]
|
|
23
|
+
Rule: TypeAlias = list[SingleRule[Column]]
|
|
24
|
+
SingleRuleSplit: TypeAlias = dict[str, Optional[Column | str | SplitWhere]]
|
|
25
|
+
RuleSplit: TypeAlias = list[SingleRuleSplit[Column]]
|
|
26
|
+
RuleOrder: TypeAlias = list[str]
|
|
27
|
+
RulePlace: TypeAlias = list[int]
|
|
28
|
+
RuleRemove: TypeAlias = RulePlace
|
|
29
|
+
RulePlaceOrOrder: TypeAlias = RulePlace | RuleOrder
|
|
30
|
+
SingleRuleRewrite: TypeAlias = dict[str,
|
|
31
|
+
Optional[Column | str |
|
|
32
|
+
list[str] |
|
|
33
|
+
RewriteKind |
|
|
34
|
+
CaseSensitivity]]
|
|
35
|
+
RuleRewrite: TypeAlias = list[SingleRuleRewrite[Column]]
|
|
36
|
+
SingleRuleMerge: TypeAlias = dict[str, list[Column] | str]
|
|
37
|
+
RuleMerge: TypeAlias = list[SingleRuleMerge[Column]]
|
|
38
|
+
DupCheckKeyArg: TypeAlias = \
|
|
39
|
+
SingleRule[Column] | SingleRuleMerge[Column] | SingleRuleSplit[Column]
|
|
40
|
+
DupCheckKey: TypeAlias = \
|
|
41
|
+
Callable[[DupCheckKeyArg[Column]], Column | list[Column]]
|
|
42
|
+
IncrCheckKey: TypeAlias = \
|
|
43
|
+
Callable[[SingleRuleMerge[Column]], Column | list[Column]]
|
|
44
|
+
NoDupKeydType: TypeAlias = \
|
|
45
|
+
Rule[Column] | RuleMerge[Column] | RuleSplit[Column]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ColInfo(NamedTuple, Generic[Column]):
|
|
49
|
+
"""Information about columns to pass to ConfigExcelListTransform init."""
|
|
50
|
+
|
|
51
|
+
split_last: str
|
|
52
|
+
insert_last: Optional[str]
|
|
53
|
+
s1: RuleSplit[Column]
|
|
54
|
+
s6: Rule[Column]
|
|
55
|
+
col_to_use: list[Column]
|
|
56
|
+
tinfo: Column
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConfigExcelListTransform(Config, Generic[Column]): # pylint: disable=too-many-instance-attributes, line-too-long # noqa: E501
|
|
60
|
+
"""Class with configuration for excel list transform."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, col_ref: ColumnRef, # pylint: disable=too-many-arguments # noqa: E501
|
|
63
|
+
colinfo: ColInfo[Column], tinfo: Column,
|
|
64
|
+
from_json_text: Optional[str] = None,
|
|
65
|
+
from_json_filename: Optional[str] = None) -> None:
|
|
66
|
+
"""Construct configuration for excel list transform."""
|
|
67
|
+
assert isinstance(colinfo.tinfo, type(tinfo))
|
|
68
|
+
self.column_ref: ColumnRef = col_ref
|
|
69
|
+
col2use = deepcopy(colinfo.col_to_use) # dont destroying caller's arg
|
|
70
|
+
self.max_column_read: int = 20
|
|
71
|
+
self.out_csv_dialect: CsvSpec = {'name': 'csv.excel',
|
|
72
|
+
'delimiter': ',', 'quoting': None,
|
|
73
|
+
'quotechar': '"',
|
|
74
|
+
'lineterminator': None,
|
|
75
|
+
'escapechar': None}
|
|
76
|
+
self.in_csv_dialect: CsvSpec = {'name': 'csv.excel',
|
|
77
|
+
'delimiter': ',', 'quoting': None,
|
|
78
|
+
'quotechar': '"',
|
|
79
|
+
'lineterminator': None,
|
|
80
|
+
'escapechar': None}
|
|
81
|
+
self.in_type: FileType = FileType.EXCEL
|
|
82
|
+
self.in_excel_library: ExcelLib = ExcelLib.PYLIGHTXL
|
|
83
|
+
self.out_excel_library: ExcelLib = ExcelLib.PYLIGHTXL
|
|
84
|
+
self.out_type: FileType = FileType.EXCEL
|
|
85
|
+
self.s1_split_columns: RuleSplit[Column] = colinfo.s1
|
|
86
|
+
self.s3_merge_columns: RuleMerge[Column] = \
|
|
87
|
+
[{'columns': [col2use.pop(0), col2use.pop(0)],
|
|
88
|
+
'separator': ' '}]
|
|
89
|
+
self.s5_rename_columns: Rule[Column] = \
|
|
90
|
+
[{'column': col2use.pop(0), 'name': 'First Name'},
|
|
91
|
+
{'column': col2use.pop(0), 'name': 'Last Name'}]
|
|
92
|
+
self.s6_insert_columns: Rule[Column] = colinfo.s6
|
|
93
|
+
self.s7_rewrite_columns: RuleRewrite[Column] = \
|
|
94
|
+
[{'column': col2use.pop(0),
|
|
95
|
+
'kind': RewriteKind.STRIP, 'chars': '',
|
|
96
|
+
'case': CaseSensitivity.IGNORE_CASE},
|
|
97
|
+
{'column': col2use.pop(0), 'kind': RewriteKind.REMOVECHARS,
|
|
98
|
+
'chars': [' ', '-'], 'case': CaseSensitivity.MATCH_CASE},
|
|
99
|
+
{'column': col2use.pop(0),
|
|
100
|
+
'kind': RewriteKind.REGEX_SUBSTITUTE,
|
|
101
|
+
'from': '^07', 'to': '+467',
|
|
102
|
+
'case': CaseSensitivity.MATCH_CASE},
|
|
103
|
+
{'column': col2use.pop(0),
|
|
104
|
+
'kind': RewriteKind.REGEX_SUBSTITUTE,
|
|
105
|
+
'from': '^+4607', 'to': '+467',
|
|
106
|
+
'case': CaseSensitivity.MATCH_CASE},
|
|
107
|
+
{'column': col2use.pop(0),
|
|
108
|
+
'kind': RewriteKind.REGEX_SUBSTITUTE,
|
|
109
|
+
'from': '^467', 'to': '+467',
|
|
110
|
+
'case': CaseSensitivity.MATCH_CASE},
|
|
111
|
+
{'column': col2use.pop(0), 'kind': RewriteKind.STR_SUBSTITUTE,
|
|
112
|
+
'from': 'donald', 'to': 'duck',
|
|
113
|
+
'case': CaseSensitivity.IGNORE_CASE}]
|
|
114
|
+
super().__init__(from_json_text, from_json_filename)
|
|
115
|
+
self.check_array_configs(split_last=colinfo.split_last,
|
|
116
|
+
insert_last=colinfo.insert_last)
|
|
117
|
+
self.sort_sx_hook()
|
|
118
|
+
self._check_no_duplicate_single(self.s1_split_columns,
|
|
119
|
+
's1_split_columns', colinfo.tinfo)
|
|
120
|
+
self._check_no_duplicate_single(self.s5_rename_columns,
|
|
121
|
+
's5_rename_columns', colinfo.tinfo)
|
|
122
|
+
self._check_no_duplicate_single(self.s6_insert_columns,
|
|
123
|
+
's6_insert_columns', colinfo.tinfo)
|
|
124
|
+
self.check_rewrite_configs(coltype=type(colinfo.tinfo))
|
|
125
|
+
|
|
126
|
+
def get_out_csv_dialect(self) -> type[Dialect]:
|
|
127
|
+
"""Get CSV dialect for output file."""
|
|
128
|
+
assert self.out_csv_dialect['name'] is not None
|
|
129
|
+
return self.get_csv_dialect(**self.out_csv_dialect)
|
|
130
|
+
|
|
131
|
+
def get_in_csv_dialect(self) -> type[Dialect]:
|
|
132
|
+
"""Get CSV dialect for input file from Portfolio."""
|
|
133
|
+
assert self.in_csv_dialect['name'] is not None
|
|
134
|
+
return self.get_csv_dialect(**self.in_csv_dialect)
|
|
135
|
+
|
|
136
|
+
def sort_sx_hook(self) -> None:
|
|
137
|
+
"""Sort s[0-9]_ as needed as needed (hook)."""
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _duplicates_not_allowed(expanded_data: list[Column],
|
|
141
|
+
param_name: str) -> None:
|
|
142
|
+
"""Error report duplicate data."""
|
|
143
|
+
dup = [str(k) for k, v in Counter(expanded_data).items() if v > 1]
|
|
144
|
+
if len(dup) == 0:
|
|
145
|
+
return
|
|
146
|
+
msg = f'Duplicates not allowed in {param_name}. Duplicate values: ' # noqa: E713, E501
|
|
147
|
+
msg += ','.join(dup)
|
|
148
|
+
print(msg, file=sys.stderr)
|
|
149
|
+
raise KeyError(msg)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def get_cols_single(rule: Rule[Column] | RuleSplit[Column] |
|
|
153
|
+
RuleRewrite[Column],
|
|
154
|
+
tinfo: Column) -> list[Column]:
|
|
155
|
+
"""Get list of columns for modification rule."""
|
|
156
|
+
cols: list[Column] = []
|
|
157
|
+
for row in rule:
|
|
158
|
+
rowcol = row['column']
|
|
159
|
+
assert isinstance(rowcol, type(tinfo))
|
|
160
|
+
cols.append(rowcol)
|
|
161
|
+
return cols
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def get_cols_multi(rule: RuleMerge[Column],
|
|
165
|
+
tinfo: Column) -> list[Column]:
|
|
166
|
+
"""Get list of columns for merge rule."""
|
|
167
|
+
cols: list[Column] = []
|
|
168
|
+
for row in rule:
|
|
169
|
+
rowcols = row['columns']
|
|
170
|
+
for singlecol in rowcols:
|
|
171
|
+
assert isinstance(singlecol, type(tinfo))
|
|
172
|
+
cols.append(singlecol)
|
|
173
|
+
return cols
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _check_no_duplicate_single(rule: Rule[Column] | RuleSplit[Column],
|
|
177
|
+
param_name: str, tinfo: Column) -> None:
|
|
178
|
+
"""Flag as error if column is refered to multiple times."""
|
|
179
|
+
cols: list[Column] = ConfigExcelListTransform.get_cols_single(rule,
|
|
180
|
+
tinfo)
|
|
181
|
+
ConfigExcelListTransform._duplicates_not_allowed(cols, param_name)
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _check_no_duplicate_multi(rule: RuleMerge[Column],
|
|
185
|
+
param_name: str, tinfo: Column) -> None:
|
|
186
|
+
"""Flag as error if column is refered to multiple times."""
|
|
187
|
+
cols: list[Column] = ConfigExcelListTransform.get_cols_multi(rule,
|
|
188
|
+
tinfo)
|
|
189
|
+
ConfigExcelListTransform._duplicates_not_allowed(cols, param_name)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _check_increasing_multi(rule: RuleMerge[Column], param_name: str,
|
|
193
|
+
tinfo: Column) -> None:
|
|
194
|
+
"""Flag as error if order is not increasing."""
|
|
195
|
+
cols: list[Column] = ConfigExcelListTransform.get_cols_multi(rule,
|
|
196
|
+
tinfo)
|
|
197
|
+
seen: Optional[Column] = None
|
|
198
|
+
for col in cols:
|
|
199
|
+
assert isinstance(col, (int, str))
|
|
200
|
+
if seen is not None and seen >= col:
|
|
201
|
+
msg: str = f'Increasing order needed in {param_name}'
|
|
202
|
+
print(msg, file=sys.stderr)
|
|
203
|
+
raise KeyError(msg)
|
|
204
|
+
seen = col
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def get_converter_dict(enum_type: type[Enum]) -> ParseConverter:
|
|
208
|
+
"""Get dict for converting to given enum_type."""
|
|
209
|
+
return ParseConverter(result_type=enum_type,
|
|
210
|
+
func=string_to_enum_best_match,
|
|
211
|
+
args={'num_type': enum_type})
|
|
212
|
+
|
|
213
|
+
def parse_converters(self) -> dict[str, ParseConverter]:
|
|
214
|
+
"""Get converters for use when parsing JSON.
|
|
215
|
+
|
|
216
|
+
Overriding in derived class.
|
|
217
|
+
Return None if no conversions.
|
|
218
|
+
Return dict of dict for use in json decoder hook.
|
|
219
|
+
Structure of return value shall be:
|
|
220
|
+
{key: {'result type': res_type, 'func': function,
|
|
221
|
+
'args': {arg_name: arg_value}}}.
|
|
222
|
+
"""
|
|
223
|
+
return {'in_type': self.get_converter_dict(FileType),
|
|
224
|
+
'out_type': self.get_converter_dict(FileType),
|
|
225
|
+
'in_excel_library': self.get_converter_dict(ExcelLib),
|
|
226
|
+
'out_excel_library': self.get_converter_dict(ExcelLib),
|
|
227
|
+
'where': self.get_converter_dict(SplitWhere),
|
|
228
|
+
'store_single': self.get_converter_dict(SplitWhere),
|
|
229
|
+
'kind': self.get_converter_dict(RewriteKind),
|
|
230
|
+
'case': self.get_converter_dict(CaseSensitivity),
|
|
231
|
+
'column_ref': self.get_converter_dict(ColumnRef)}
|
|
232
|
+
|
|
233
|
+
def check_array_configs(self, split_last: str,
|
|
234
|
+
insert_last: Optional[str]) -> None:
|
|
235
|
+
"""Check that keywords in configuration arrays are OK."""
|
|
236
|
+
split_col_keys = ['column', 'separator', 'where', split_last]
|
|
237
|
+
self.check_array_keys('s1_split_columns', self.s1_split_columns,
|
|
238
|
+
split_col_keys)
|
|
239
|
+
merge_col_keys = ['columns', 'separator']
|
|
240
|
+
self.check_array_keys('s3_merge_columns', self.s3_merge_columns,
|
|
241
|
+
merge_col_keys)
|
|
242
|
+
rename_col_keys = ['column', 'name']
|
|
243
|
+
self.check_array_keys('s5_rename_columns', self.s5_rename_columns,
|
|
244
|
+
rename_col_keys)
|
|
245
|
+
insert_col_keys = ['column', 'value']
|
|
246
|
+
if insert_last is not None:
|
|
247
|
+
assert insert_last is not None # keep mypy happy
|
|
248
|
+
insert_col_keys.append(insert_last)
|
|
249
|
+
self.check_array_keys('s6_insert_columns', self.s6_insert_columns,
|
|
250
|
+
insert_col_keys)
|
|
251
|
+
|
|
252
|
+
def check_rewrite_configs(self, coltype: type) -> None:
|
|
253
|
+
"""Check the rewrite column configuration."""
|
|
254
|
+
rewrite_col_mand_keys: list[str] = ['column', 'kind', 'case']
|
|
255
|
+
rewrite_col_opt_keys: list[str] = ['chars', 'from', 'to']
|
|
256
|
+
self.check_array_keys('s7_rewrite_columns', self.s7_rewrite_columns,
|
|
257
|
+
mandatory_keys=rewrite_col_mand_keys,
|
|
258
|
+
allowed_keys=rewrite_col_opt_keys)
|
|
259
|
+
template = {RewriteKind.STRIP: {'column': coltype,
|
|
260
|
+
'kind': RewriteKind,
|
|
261
|
+
'chars': str,
|
|
262
|
+
'case': CaseSensitivity},
|
|
263
|
+
RewriteKind.REMOVECHARS: {'column': coltype,
|
|
264
|
+
'kind': RewriteKind,
|
|
265
|
+
'chars': list,
|
|
266
|
+
'case': CaseSensitivity},
|
|
267
|
+
RewriteKind.REGEX_SUBSTITUTE: {'column': coltype,
|
|
268
|
+
'kind': RewriteKind,
|
|
269
|
+
'from': str, 'to': str,
|
|
270
|
+
'case': CaseSensitivity},
|
|
271
|
+
RewriteKind.STR_SUBSTITUTE: {'column': coltype,
|
|
272
|
+
'kind': RewriteKind,
|
|
273
|
+
'from': str, 'to': str,
|
|
274
|
+
'case': CaseSensitivity}}
|
|
275
|
+
self.check_array_dicts(name_of_cfg='s7_rewrite_columns',
|
|
276
|
+
array=self.s7_rewrite_columns,
|
|
277
|
+
kind_key='kind', kind_type=RewriteKind,
|
|
278
|
+
dict_of_templates=template)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Get correct configuration type."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import Optional, TypeAlias
|
|
9
|
+
import sys
|
|
10
|
+
from json import loads as json_loads
|
|
11
|
+
from json import JSONDecodeError
|
|
12
|
+
from excel_list_transform.config_enums import ColumnRef
|
|
13
|
+
from excel_list_transform.config_xls_list_refmt_name import \
|
|
14
|
+
ConfigXlsListRefmtName
|
|
15
|
+
from excel_list_transform.config_xls_list_refmt_num import \
|
|
16
|
+
ConfigXlsListRefmtNum
|
|
17
|
+
from excel_list_transform.str_to_enum import string_to_enum_best_match
|
|
18
|
+
from excel_list_transform.commontypes import JsonType
|
|
19
|
+
from excel_list_transform.file_must_exist import file_must_exist
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Configs: TypeAlias = \
|
|
23
|
+
dict[ColumnRef,
|
|
24
|
+
type[ConfigXlsListRefmtName] | type[ConfigXlsListRefmtNum]]
|
|
25
|
+
DerivedConfig: TypeAlias = ConfigXlsListRefmtName | ConfigXlsListRefmtNum
|
|
26
|
+
|
|
27
|
+
_CONFIGS: Configs = {ColumnRef.BY_NAME: ConfigXlsListRefmtName,
|
|
28
|
+
ColumnRef.BY_NUMBER: ConfigXlsListRefmtNum}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def config_factory_from_enum(numerator: ColumnRef) -> DerivedConfig:
|
|
32
|
+
"""Get correct configuration type for numerator value."""
|
|
33
|
+
return _CONFIGS[numerator]()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _config_factory_get_text(from_json_text: Optional[str] = None,
|
|
37
|
+
from_json_filename: Optional[str] = None) -> str:
|
|
38
|
+
"""Get JSON text to use."""
|
|
39
|
+
if from_json_filename is None and from_json_text is None:
|
|
40
|
+
msg = 'Either JSON text or JSON file needed. Both cannot be None.'
|
|
41
|
+
print(msg, file=sys.stderr)
|
|
42
|
+
raise RuntimeError(msg)
|
|
43
|
+
if from_json_filename is not None and from_json_text is not None:
|
|
44
|
+
msg = 'Either JSON text or JSON file needed. Both cannot be given.'
|
|
45
|
+
print(msg, file=sys.stderr)
|
|
46
|
+
raise RuntimeError(msg)
|
|
47
|
+
if from_json_text is not None:
|
|
48
|
+
return from_json_text
|
|
49
|
+
assert from_json_filename is not None
|
|
50
|
+
file_must_exist(filename=from_json_filename,
|
|
51
|
+
with_content_txt='configuration JSON input')
|
|
52
|
+
with open(from_json_filename, "r", encoding='UTF-8') as file:
|
|
53
|
+
text = file.read()
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _config_factory_exit(msg: str,
|
|
58
|
+
exc: Optional[JSONDecodeError] |
|
|
59
|
+
Optional[UnicodeDecodeError]) -> None:
|
|
60
|
+
"""Report config factory error and exit."""
|
|
61
|
+
msg2 = '\nDid you specify an incorrect configuration file?\n'
|
|
62
|
+
totmsg = msg + msg2 + ('' if exc is None else str(exc)) + '\n'
|
|
63
|
+
print(totmsg, file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def config_factory_from_json(from_json_text: Optional[str] = None,
|
|
68
|
+
from_json_filename: Optional[str] = None) \
|
|
69
|
+
-> DerivedConfig:
|
|
70
|
+
"""Get correct configuration type for JSON data."""
|
|
71
|
+
text = _config_factory_get_text(from_json_text=from_json_text,
|
|
72
|
+
from_json_filename=from_json_filename)
|
|
73
|
+
data: JsonType = None
|
|
74
|
+
try:
|
|
75
|
+
data = json_loads(text)
|
|
76
|
+
except JSONDecodeError as exc:
|
|
77
|
+
msg = 'Configuration JSON cannot be decoded.'
|
|
78
|
+
_config_factory_exit(msg, exc)
|
|
79
|
+
except UnicodeDecodeError as exc:
|
|
80
|
+
msg = 'Invalid UTF-8 in configuration data.\n'
|
|
81
|
+
_config_factory_exit(msg, exc)
|
|
82
|
+
if data is None or not isinstance(data, dict):
|
|
83
|
+
msg = 'JSON data is not valid configuration. Top level not dict'
|
|
84
|
+
_config_factory_exit(msg, None)
|
|
85
|
+
assert data is not None
|
|
86
|
+
assert isinstance(data, dict)
|
|
87
|
+
if 'column_ref' not in data:
|
|
88
|
+
msg = 'JSON data is not valid configuration. No key "column_ref".'
|
|
89
|
+
_config_factory_exit(msg, None)
|
|
90
|
+
refpar: JsonType = data['column_ref']
|
|
91
|
+
assert isinstance(refpar, str)
|
|
92
|
+
numerator = string_to_enum_best_match(inp=refpar,
|
|
93
|
+
num_type=ColumnRef)
|
|
94
|
+
return _CONFIGS[numerator](from_json_text=from_json_text,
|
|
95
|
+
from_json_filename=from_json_filename)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Read and write configuration of CSV splitting."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from excel_list_transform.config_enums import SplitWhere, ColumnRef
|
|
10
|
+
from excel_list_transform.config_excel_list_transform import \
|
|
11
|
+
ConfigExcelListTransform, RuleOrder, ColInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigXlsListRefmtName(ConfigExcelListTransform[str]): # pylint: disable=too-many-instance-attributes, line-too-long # noqa: E501
|
|
15
|
+
"""Class with configuration for excel list transform."""
|
|
16
|
+
|
|
17
|
+
def __init__(self,
|
|
18
|
+
from_json_text: Optional[str] = None,
|
|
19
|
+
from_json_filename: Optional[str] = None) -> None:
|
|
20
|
+
"""Construct configuration for excel list transform."""
|
|
21
|
+
col_to_use = ['street', 'street number', 'name', 'last name',
|
|
22
|
+
'Phone', 'Phone', 'Phone', 'Phone', 'Phone',
|
|
23
|
+
'Last Name']
|
|
24
|
+
colinfo = ColInfo[str](split_last='right_name', insert_last=None,
|
|
25
|
+
s1=[{'column': 'name',
|
|
26
|
+
'separator': ' ',
|
|
27
|
+
'where': SplitWhere.RIGHTMOST,
|
|
28
|
+
'right_name': 'last name'}],
|
|
29
|
+
s6=[{'column': 'Division', 'value': None},
|
|
30
|
+
{'column': 'Other', 'value': 'some text'}],
|
|
31
|
+
col_to_use=col_to_use, tinfo='a')
|
|
32
|
+
self.s8_column_order: RuleOrder = \
|
|
33
|
+
['Class', 'Division', 'Nationality', 'Sail Number', 'Boat Name',
|
|
34
|
+
'First Name', 'Last Name', 'Club Name', 'Email', 'Phone',
|
|
35
|
+
'WhatsApp']
|
|
36
|
+
super().__init__(col_ref=ColumnRef.BY_NAME,
|
|
37
|
+
colinfo=colinfo, tinfo='a',
|
|
38
|
+
from_json_text=from_json_text,
|
|
39
|
+
from_json_filename=from_json_filename)
|
|
40
|
+
self._duplicates_not_allowed(self.s8_column_order, 's8_column_order')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Read and write configuration of CSV splitting."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from excel_list_transform.config_enums import SplitWhere, ColumnRef
|
|
10
|
+
from excel_list_transform.config_excel_list_transform \
|
|
11
|
+
import ConfigExcelListTransform, RulePlace, RuleRemove, \
|
|
12
|
+
SingleRuleMerge, SingleRuleSplit, SingleRule, ColInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_column(rule: SingleRuleSplit[int] | SingleRule[int]) -> int:
|
|
16
|
+
"""Get column of rule."""
|
|
17
|
+
ret = rule['column']
|
|
18
|
+
assert isinstance(ret, int)
|
|
19
|
+
return ret
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_merge_first_column(rule: SingleRuleMerge[int]) -> int:
|
|
23
|
+
"""Get column of rule."""
|
|
24
|
+
cols = rule['columns']
|
|
25
|
+
ret = cols[0]
|
|
26
|
+
assert isinstance(ret, int)
|
|
27
|
+
return ret
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigXlsListRefmtNum(ConfigExcelListTransform[int]): # pylint: disable=too-many-instance-attributes, line-too-long # noqa: E501
|
|
31
|
+
"""Class with configuration for excel list transform."""
|
|
32
|
+
|
|
33
|
+
def __init__(self,
|
|
34
|
+
from_json_text: Optional[str] = None,
|
|
35
|
+
from_json_filename: Optional[str] = None) -> None:
|
|
36
|
+
"""Construct configuration for excel list transform."""
|
|
37
|
+
self.s2_remove_columns: RuleRemove = [1, 2, 3]
|
|
38
|
+
self.s4_place_columns_first: RulePlace = [7, 3, 6]
|
|
39
|
+
col_to_use = [15, 16, 1, 2, 5, 5, 5, 5, 5, 6]
|
|
40
|
+
colinfo: ColInfo[int] = \
|
|
41
|
+
ColInfo[int](split_last='store_single', insert_last='name',
|
|
42
|
+
s1=[{'column': 15, 'separator': ' ',
|
|
43
|
+
'where': SplitWhere.RIGHTMOST,
|
|
44
|
+
'store_single': SplitWhere.LEFTMOST}],
|
|
45
|
+
s6=[{'column': 1, 'name': 'Division',
|
|
46
|
+
'value': None},
|
|
47
|
+
{'column': 7, 'name': 'Other',
|
|
48
|
+
'value': 'some text'}],
|
|
49
|
+
col_to_use=col_to_use, tinfo=2)
|
|
50
|
+
super().__init__(col_ref=ColumnRef.BY_NUMBER,
|
|
51
|
+
colinfo=colinfo, tinfo=2,
|
|
52
|
+
from_json_text=from_json_text,
|
|
53
|
+
from_json_filename=from_json_filename)
|
|
54
|
+
self._duplicates_not_allowed(self.s2_remove_columns,
|
|
55
|
+
's2_remove_columns')
|
|
56
|
+
self._check_increasing_multi(self.s3_merge_columns,
|
|
57
|
+
's3_merge_columns', 2)
|
|
58
|
+
self._duplicates_not_allowed(self.s4_place_columns_first,
|
|
59
|
+
's4_place_columns_first')
|
|
60
|
+
|
|
61
|
+
def sort_sx_hook(self) -> None:
|
|
62
|
+
"""Sort s[0-9]_ as needed as needed (hook)."""
|
|
63
|
+
self.s1_split_columns = sorted(self.s1_split_columns, key=get_column)
|
|
64
|
+
self.s2_remove_columns = sorted(self.s2_remove_columns)
|
|
65
|
+
self.s3_merge_columns = sorted(self.s3_merge_columns,
|
|
66
|
+
key=get_merge_first_column)
|
|
67
|
+
self.s5_rename_columns = sorted(self.s5_rename_columns,
|
|
68
|
+
key=get_column)
|
|
69
|
+
self.s6_insert_columns = sorted(self.s6_insert_columns,
|
|
70
|
+
key=get_column)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Functions to add and remove file extensions."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from os import path
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fix_file_extension(filename: str, ext_to_add: str,
|
|
14
|
+
ext_to_remove: Optional[str] = None,
|
|
15
|
+
for_reading: bool = False) -> str:
|
|
16
|
+
"""Add and remove file extensions as needed."""
|
|
17
|
+
ret = deepcopy(filename)
|
|
18
|
+
low = ret.lower()
|
|
19
|
+
if for_reading and path.exists(path=ret):
|
|
20
|
+
return ret
|
|
21
|
+
if ext_to_remove is not None:
|
|
22
|
+
extlowrem = ext_to_remove.lower()
|
|
23
|
+
extlen = len(ext_to_remove)
|
|
24
|
+
if low[-extlen:] == extlowrem:
|
|
25
|
+
ret = ret[:-extlen]
|
|
26
|
+
extlowadd = ext_to_add.lower()
|
|
27
|
+
extlen = len(ext_to_add)
|
|
28
|
+
if low[-extlen:] != extlowadd:
|
|
29
|
+
ret = ret + ext_to_add
|
|
30
|
+
return ret
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#! /usr/local/bin/python3
|
|
2
|
+
"""Function for checking that file exists."""
|
|
3
|
+
|
|
4
|
+
# Copyright (c) 2024 Tom Björkholm
|
|
5
|
+
# MIT License
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from os import path
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def file_must_exist(filename: str,
|
|
14
|
+
with_content_txt: Optional[str] = None) -> None:
|
|
15
|
+
"""Check that input file exists. Exit if not."""
|
|
16
|
+
if not path.exists(filename):
|
|
17
|
+
msg = f'File {filename} '
|
|
18
|
+
if with_content_txt is not None:
|
|
19
|
+
msg += 'with ' + with_content_txt + ' '
|
|
20
|
+
msg += 'does not exist. Cannot proceed.'
|
|
21
|
+
print(msg, file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|