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.
@@ -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)