extract-list 0.2__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 @@
1
+ """Extract a list of columns from JSON or XML and save to excel, CSV, etc."""
@@ -0,0 +1,11 @@
1
+ #! /usr/local/bin/python3
2
+ """Extract a list of columns from JSON or XML and save to excel, CSV, etc."""
3
+
4
+ # Copyright (c) 2024 - 2025 Tom Björkholm
5
+ # MIT License
6
+
7
+
8
+ from extract_list.extract_cmd import extract_cmd # pragma: no cover # noqa: E501
9
+
10
+ if __name__ == '__main__': # pragma: no cover
11
+ extract_cmd()
@@ -0,0 +1,24 @@
1
+ #! /usr/local/bin/python3
2
+ """Define types that are common for several files."""
3
+
4
+ # Copyright (c) 2024 - 2025 Tom Björkholm
5
+ # MIT License
6
+
7
+ from typing import Optional, TypeAlias
8
+ from datetime import datetime
9
+ from enum import Enum, auto
10
+
11
+ Value: TypeAlias = Optional[str | int | bool | float | datetime]
12
+ Row: TypeAlias = dict[str, Value]
13
+ Data: TypeAlias = list[Row]
14
+
15
+
16
+ class CfgTypes(Enum):
17
+ """Types of example configurations."""
18
+
19
+ SW_JSON_TO_RRS = auto()
20
+ SW_XML_TO_RRS = auto()
21
+ EXAMPLE_JSON = auto()
22
+ EXAMPLE_XML = auto()
23
+ EXAMPLE2_JSON = auto()
24
+ EXAMPLE2_XML = auto()
@@ -0,0 +1,32 @@
1
+ #! /usr/local/bin/python3
2
+ """Enumerations used in configuration."""
3
+
4
+ # Copyright (c) 2024 - 2025 Tom Björkholm
5
+ # MIT License
6
+
7
+ from enum import Enum, auto
8
+ from excel_list_transform.config_enums import FileType as ExcFileType
9
+
10
+
11
+ class InFileType(Enum):
12
+ """Input file type.""" # Code duplication due to mypy limitation
13
+
14
+ JSON = len(ExcFileType) + 1
15
+ XML = auto()
16
+
17
+
18
+ class OutFileType(Enum):
19
+ """Output file type.""" # Code duplication due to mypy limitation
20
+
21
+ EXCEL = ExcFileType.EXCEL.value
22
+ CSV = ExcFileType.CSV.value
23
+ JSON = InFileType.JSON.value
24
+ XML = InFileType.XML.value
25
+ TXT = auto()
26
+
27
+
28
+ class MissingInputForColumn(Enum):
29
+ """What to do if path for column does not exist."""
30
+
31
+ ERROR = auto()
32
+ EMPTY = auto()
@@ -0,0 +1,144 @@
1
+ #! /usr/local/bin/python3
2
+ """Extract a list of columns from JSON or XML and save to excel, CSV, etc."""
3
+
4
+ # Copyright (c) 2024 - 2025 Tom Björkholm
5
+ # MIT License
6
+
7
+ from sys import argv as sys_argv
8
+ from copy import deepcopy
9
+ from typing import Optional, TypeAlias
10
+ import argparse
11
+ from extract_list.generate_cfg import generate_example_cfg, \
12
+ get_types_of_cfg, get_out_file_types
13
+ from extract_list.extract_func import extract_func
14
+
15
+
16
+ def gen_cfg_cmd(args: argparse.Namespace) -> int:
17
+ """Generate example cfg file."""
18
+ outfilename: str = args.output[0]
19
+ cfgtype: str = args.kind[0]
20
+ outfiletype: str = args.typeofoutput[0]
21
+ return generate_example_cfg(filename=outfilename,
22
+ cfgtype=cfgtype,
23
+ out_file_type=outfiletype)
24
+
25
+
26
+ def do_extract_cmd(args: argparse.Namespace) -> int:
27
+ """Do extraction of list."""
28
+ outfilename = args.output[0]
29
+ infilename = args.input[0]
30
+ cfgfilename = args.cfg[0]
31
+ return extract_func(in_file_name=infilename,
32
+ out_file_name=outfilename,
33
+ cfg_file_name=cfgfilename)
34
+
35
+
36
+ USAGE_ORDER = '''
37
+ The normal way to use this command is:
38
+ (1) Using the "cfg-example" sub-command a few example configuration (.cfg)
39
+ files with description (.txt) files are generated.
40
+ (2) Read the example configuration (.cfg) files and the accompanying
41
+ description (.txt) files.
42
+ (3) Find an example that is close to what you want to achieve.
43
+ (4) Modify that configuration file to achieve what you want to achieve.
44
+ (5) Use the "extract" sub-command to extract data from input (JSON or XML)
45
+ and output it as a list according to your modified configuration
46
+ file.
47
+ (6) Read the produced output. If necessary go back to step 4 and adjust
48
+ how the data is transformed.
49
+ '''
50
+
51
+ TXT_DESCRIPTION = '''
52
+ When generating an example configuration file a text file describing
53
+ the configuration file syntax is also generated, with the same name
54
+ as the configuration file but with extension .txt instead of .cfg.
55
+ '''
56
+
57
+ GENERAL_DESCRIPTION = '''
58
+ Extract data from an input file in JSON or XML format, and output
59
+ it as a list of columns in Excel, CSV, text, JSON or XML format.
60
+ How data is extracted is described in a configuration file.
61
+ Name of input file, output file and configuration file is given
62
+ as command line arguments.
63
+ The command can also generate a few example configuration files.
64
+ When generating an example configuration file the output file name
65
+ switch gives the name of the generated configuration file.
66
+ '''
67
+
68
+ SEE_MAIN_HELP = '''
69
+ See also help text for main command without sub-commands.
70
+ '''
71
+
72
+ SubParseAct: TypeAlias = 'argparse._SubParsersAction[argparse.ArgumentParser]'
73
+
74
+
75
+ def gen_cfg_args(subparsers: SubParseAct) -> None:
76
+ """Add arguments for generate example config sub-command."""
77
+ cfg_help = 'Generate example configuration file (example .cfg file). '
78
+ cfg_help += 'Arguments select the kind of configuration file that '
79
+ cfg_help += 'is generated.'
80
+ cfg_parser = subparsers.add_parser('cfg-example', help=cfg_help,
81
+ epilog=USAGE_ORDER,
82
+ description=cfg_help +
83
+ TXT_DESCRIPTION + SEE_MAIN_HELP)
84
+ cfg_parser.set_defaults(func=gen_cfg_cmd)
85
+ examplekinds = get_types_of_cfg()
86
+ kind_help = 'Kind of example to generate configuration file for.'
87
+ kind_help += 'Possible kinds are (' + ', '.join(examplekinds) + ').'
88
+ cfg_parser.add_argument('-k', '--kind', nargs=1, required=True,
89
+ help=kind_help, choices=examplekinds)
90
+ outtypes = get_out_file_types()
91
+ out_help = 'What output file format should configuration file '
92
+ out_help += 'specify. '
93
+ out_help += 'Possible values are (' + ', '.join(outtypes) + '). '
94
+ cfg_parser.add_argument('-t', '--typeofoutput', nargs=1, required=True,
95
+ help=out_help, choices=outtypes)
96
+ cfg_output_help = 'Name of configuration (output) file to create.'
97
+ cfg_parser.add_argument('-o', '--output', nargs=1,
98
+ help=cfg_output_help, required=True)
99
+
100
+
101
+ def extract_args(subparsers: SubParseAct) -> None:
102
+ """Add arguments for extract sub-command."""
103
+ extract_help = 'Extract list of columns of data from JSON or XML input. '
104
+ extract_help += 'How data is extracted '
105
+ extract_help += 'is described in a configuration file. Name of input '
106
+ extract_help += 'file, output file and configuration file is given as '
107
+ extract_help += 'command line arguments.'
108
+ extract_parser = subparsers.add_parser('extract', help=extract_help,
109
+ epilog=USAGE_ORDER,
110
+ description=extract_help +
111
+ SEE_MAIN_HELP)
112
+ extract_parser.set_defaults(func=do_extract_cmd)
113
+ extract_parser.add_argument('-c', '--cfg', nargs=1, required=True,
114
+ help='Configuation file name to use.')
115
+ extract_parser.add_argument('-i', '--input', nargs=1,
116
+ help='Name of input file.', required=True)
117
+ extract_parser.add_argument('-o', '--output', nargs=1,
118
+ help='Name of output file to create.',
119
+ required=True)
120
+
121
+
122
+ def extract_cmd(arguments: Optional[list[str]] = None) -> int:
123
+ """Extract a list of columns from JSON or XML and save to excel, etc."""
124
+ epimain = 'More detailed help is available for each sub-command.'
125
+ if arguments is None: # pragma: no cover
126
+ arguments = sys_argv
127
+ fixed_args = deepcopy(arguments)
128
+ if len(fixed_args) > 2 and 'python' in fixed_args[0]:
129
+ del fixed_args[0]
130
+ if len(fixed_args) > 2 and '-m' == fixed_args[0]:
131
+ del fixed_args[0]
132
+ while len(fixed_args) >= 1 and fixed_args[0][-3:] == '.py':
133
+ del fixed_args[0]
134
+ desc = GENERAL_DESCRIPTION + \
135
+ USAGE_ORDER
136
+ parser = argparse.ArgumentParser(prog='extract_list', description=desc,
137
+ epilog=epimain)
138
+ subparsers = parser.add_subparsers(dest='subparser_name', required=True)
139
+ gen_cfg_args(subparsers)
140
+ extract_args(subparsers)
141
+ args = parser.parse_args(args=fixed_args)
142
+ ret = args.func(args)
143
+ assert isinstance(ret, int)
144
+ return ret
@@ -0,0 +1,415 @@
1
+ #! /usr/local/bin/python3
2
+ """Configuration of extract a list of columns from JSON or XML."""
3
+
4
+ # Copyright (c) 2024 - 2025 Tom Björkholm
5
+ # MIT License
6
+
7
+ from typing import Optional, TypeAlias, TypeVar, TypedDict, cast
8
+ from enum import Enum
9
+ from csv import Dialect
10
+ import sys
11
+ from string import whitespace
12
+ from copy import deepcopy
13
+ from collections import Counter
14
+ from excel_list_transform.config import Config, ParseConverter
15
+ from excel_list_transform.config_enums import ExcelLib
16
+ from excel_list_transform.str_to_enum import string_to_enum_best_match
17
+ from extract_list.config_enums import InFileType, OutFileType, \
18
+ MissingInputForColumn
19
+
20
+ CsvSpec: TypeAlias = dict[str, Optional[str]]
21
+
22
+
23
+ MLineDict = TypedDict('MLineDict', {'line': list[str],
24
+ 'columns': dict[str, list[str]],
25
+ 'expand_at': list[list[str]]})
26
+ LLineDict = TypedDict('LLineDict', {'line': list[str],
27
+ 'columns': dict[str, list[str]],
28
+ 'linked_main_column': list[str],
29
+ 'linked_column': list[str],
30
+ 'expand_at': list[list[str]]})
31
+
32
+
33
+ class MainLineSpec: # pylint: disable=too-few-public-methods
34
+ """Some spec."""
35
+
36
+ def __init__(self, data: Optional[MLineDict] = None):
37
+ """Construct mainline spec."""
38
+ self.line: list[str] = []
39
+ self.columns: dict[str, list[str]] = {}
40
+ self.expand_at: list[list[str]] = []
41
+ if data is not None:
42
+ self.line = data['line']
43
+ self.columns = data['columns']
44
+ self.expand_at = data['expand_at']
45
+
46
+ def __str__(self) -> str:
47
+ """Get string representation."""
48
+ return 'MainLineSpec(' + str(self.__dict__) + ')'
49
+
50
+
51
+ class LinkedLineSpec: # pylint: disable=too-few-public-methods
52
+ """other spec."""
53
+
54
+ def __init__(self, data: Optional[LLineDict] = None):
55
+ """Construct linked line spec."""
56
+ self.line: list[str] = []
57
+ self.columns: dict[str, list[str]] = {}
58
+ self.linked_main_column: list[str] = []
59
+ self.linked_column: list[str] = []
60
+ self.expand_at: list[list[str]] = []
61
+ if data is not None:
62
+ self.line = data['line']
63
+ self.columns = data['columns']
64
+ self.linked_main_column = data['linked_main_column']
65
+ self.linked_column = data['linked_column']
66
+ self.expand_at = data['expand_at']
67
+
68
+ def __str__(self) -> str:
69
+ """Get string representation."""
70
+ return 'LinkedLineSpec(' + str(self.__dict__) + ')'
71
+
72
+
73
+ class LinkedLineList(list[LinkedLineSpec]):
74
+ """Type trick for JSON parser."""
75
+
76
+
77
+ SomeNamedTuple = TypeVar('SomeNamedTuple', MainLineSpec, LinkedLineSpec)
78
+ SomeCfgTyp = TypeVar('SomeCfgTyp')
79
+
80
+
81
+ def _mline_spec_from_dict(data: MLineDict) -> MainLineSpec:
82
+ """Get named tuple converted from dict."""
83
+ return MainLineSpec(data=data)
84
+
85
+
86
+ def _linked_line_from_json_array(data: list[LLineDict]) -> LinkedLineList:
87
+ """Get list of LinkedLineSpec from list of dict."""
88
+ assert isinstance(data, list)
89
+ ret = []
90
+ for elem in data:
91
+ ret.append(LinkedLineSpec(data=elem))
92
+ return LinkedLineList(ret)
93
+
94
+
95
+ class ExtractConfig(Config): # pylint: disable=too-many-instance-attributes
96
+ """Configuration of extract a list of columns from JSON or XML."""
97
+
98
+ @staticmethod
99
+ def example_main_line() -> MainLineSpec:
100
+ """Get example spec for main line."""
101
+ main_col = {'What': ['items', 'item'],
102
+ 'How many': ['items', 'quantity']}
103
+ data: MLineDict = {'line': ['orders'], 'columns': main_col,
104
+ 'expand_at': [['items']]}
105
+ return MainLineSpec(data=data)
106
+
107
+ @staticmethod
108
+ def example_linked_line() -> LinkedLineSpec:
109
+ """Get example spec for linked line."""
110
+ columns = {'Customer name': ['name'],
111
+ 'Street': ['address', 'street'],
112
+ 'Street number': ['address', 'number']}
113
+ data: LLineDict = {'line': ['customers'], 'columns': columns,
114
+ 'linked_main_column': ['customer'],
115
+ 'linked_column': ['customer_number'],
116
+ 'expand_at': []}
117
+ return LinkedLineSpec(data=data)
118
+
119
+ def __init__(self, from_json_data_text: Optional[str] = None,
120
+ from_json_filename: Optional[str] = None) -> None:
121
+ """Construct extract configuration object."""
122
+ self.infile_type: InFileType = InFileType.JSON
123
+ self.infile_encoding: str = 'utf-8'
124
+ self.in_xml_strip_at: bool = False
125
+ self.include_key: bool = True
126
+ self.column_name_for_key: str = 'key col'
127
+ self.missing_input_for_column: MissingInputForColumn = \
128
+ MissingInputForColumn.EMPTY
129
+ self.main_line: MainLineSpec = self.example_main_line()
130
+ self.linked_lines: list[LinkedLineSpec] = [self.example_linked_line()]
131
+ self.one_output_line_per_main_line: bool = True
132
+ self.outfile_type: OutFileType = OutFileType.EXCEL
133
+ self.outfile_encoding: str = 'utf-8'
134
+ self.outfile_excel_library: ExcelLib = ExcelLib.PYLIGHTXL
135
+ self.column_order: list[str] = ['What', 'How many', 'Customer name',
136
+ 'Street', 'Street number', 'key col']
137
+ self.order_rows_by: list[str] = []
138
+ self.out_xml_attributes = ['What']
139
+ self.out_csv_dialect: CsvSpec = {'name': 'csv.excel',
140
+ 'delimiter': ',', 'quoting': None,
141
+ 'quotechar': '"',
142
+ 'lineterminator': None,
143
+ 'escapechar': None}
144
+ super().__init__(from_json_data_text=from_json_data_text,
145
+ from_json_filename=from_json_filename)
146
+ self._check_self()
147
+
148
+ def get_out_csv_dialect(self) -> type[Dialect]:
149
+ """Get CSV dialect for outpyt file."""
150
+ assert self.out_csv_dialect['name'] is not None
151
+ return self.get_csv_dialect(**self.out_csv_dialect)
152
+
153
+ def _check_self(self) -> None:
154
+ """Check that configuration is OK after reading from file or str."""
155
+ self._check_filetype(self.infile_type, InFileType)
156
+ self.check_char_encoding(self.infile_encoding)
157
+ self._check_filetype(self.outfile_type, OutFileType)
158
+ self.check_char_encoding(self.outfile_encoding)
159
+ self._check_type(self.in_xml_strip_at, bool, 'in_xml_strip_at')
160
+ self._check_type(self.include_key, bool, 'include_key')
161
+ self._check_type(self.column_name_for_key, str, 'column_name_for_key')
162
+ self._check_enum(self.missing_input_for_column, MissingInputForColumn,
163
+ 'missing_input_for_column')
164
+ self._check_type(self.main_line, MainLineSpec, 'main_line')
165
+ self._check_mainline_part(var=self.main_line, spectype=MainLineSpec,
166
+ varname='main_line')
167
+ self._check_type(self.linked_lines, list, 'linked_lines')
168
+ self._check_linkedline(self.linked_lines, 'linked_lines')
169
+ self._check_type(self.one_output_line_per_main_line, bool,
170
+ 'one_output_line_per_main_line')
171
+ self._check_enum(self.outfile_excel_library, ExcelLib,
172
+ 'outfile_excel_library')
173
+ self._check_type(self.column_order, list, 'column_order')
174
+ self._check_list_str(self.column_order, 'column_order')
175
+ self._check_type(self.order_rows_by, list, 'order_rows_by')
176
+ self._check_list_str(self.order_rows_by, 'order_rows_by')
177
+ self.check_no_duplicates(self.column_order, 'column_order')
178
+ self._check_type(self.out_xml_attributes, list, 'out_xml_attributes')
179
+ self._check_list_str(self.out_xml_attributes, 'out_xml_attributes')
180
+ self.check_csv()
181
+ self.check_extract_unique_colnames()
182
+ self.cross_check_columns()
183
+ self.cross_check_attrs()
184
+ self.check_valid_xml_colnames()
185
+
186
+ def _extracted_columns(self) -> list[str]:
187
+ """Get list names of all extracted columns."""
188
+ extracted_cols: list[str] = []
189
+ for link in self.linked_lines:
190
+ extracted_cols += link.columns.keys()
191
+ extracted_cols += self.main_line.columns.keys()
192
+ if self.include_key:
193
+ extracted_cols.append(self.column_name_for_key)
194
+ return extracted_cols
195
+
196
+ def get_order_rows_by(self) -> list[str]:
197
+ """Get list of columns to use for sorting rows."""
198
+ if self.order_rows_by:
199
+ return self.order_rows_by
200
+ return self.column_order
201
+
202
+ def cross_check_attrs(self) -> None:
203
+ """Check that out_xml_attributes refer to existing 'columns'."""
204
+ extracted_cols = self._extracted_columns()
205
+ for att in self.out_xml_attributes:
206
+ if att not in extracted_cols:
207
+ print(f'Attribute name "{att}" in out_xml_attributes\n' +
208
+ 'but no column with that name extracted',
209
+ file=sys.stderr)
210
+ sys.exit(1)
211
+
212
+ def cross_check_columns(self) -> None:
213
+ """Do cross-check column order to extracted columns."""
214
+ extracted_cols = self._extracted_columns()
215
+ for col in self.column_order:
216
+ if col not in extracted_cols:
217
+ print(f'column order includes column "{col}"\n' +
218
+ 'but that column is not extracted', file=sys.stderr)
219
+ sys.exit(1)
220
+ for col in extracted_cols:
221
+ if col not in self.column_order:
222
+ print(f'Extracted column "{col}" is missing in column_order',
223
+ file=sys.stderr)
224
+ sys.exit(1)
225
+ for col in self.order_rows_by:
226
+ if col not in extracted_cols:
227
+ print(f'order rows by includes column "{col}"\n' +
228
+ 'but that column is not extracted', file=sys.stderr)
229
+ sys.exit(1)
230
+
231
+ def check_extract_unique_colnames(self) -> None:
232
+ """Check that not several extracted columns have same name."""
233
+ col_names = self._extracted_columns()
234
+ repeated = [k for k, v in Counter(col_names).items() if v > 1]
235
+ if repeated:
236
+ print('Column names of extracted data must be unique.',
237
+ file=sys.stderr)
238
+ print('Repeated column name(s): ' + ' ,'.join(repeated),
239
+ file=sys.stderr)
240
+ sys.exit(1)
241
+
242
+ def check_valid_xml_colnames(self) -> None:
243
+ """Check and warn for column names that are not valid XML."""
244
+ if self.outfile_type != OutFileType.XML:
245
+ return
246
+ for colname in self.column_order:
247
+ if True in [c in colname for c in whitespace]:
248
+ msg = f'Warning: Column name "{colname}" is not a valid ' +\
249
+ f'column name in XML,\nas "{colname}" contains white' +\
250
+ ' space.'
251
+ print(msg, file=sys.stderr)
252
+
253
+ def check_csv(self) -> None:
254
+ """Check if CSV configuration is OK."""
255
+ try:
256
+ _ = self.get_out_csv_dialect()
257
+ except Exception as exc: # pylint: disable=broad-exception-caught
258
+ print('Configured out_csv_dialect is not valid', file=sys.stderr)
259
+ print(str(exc), file=sys.stderr)
260
+ sys.exit(1)
261
+
262
+ @staticmethod
263
+ def _check_mainline_part(var: MainLineSpec | LinkedLineSpec,
264
+ spectype:
265
+ type[MainLineSpec] | type[LinkedLineSpec],
266
+ varname: str) -> None:
267
+ """Check MainLineSpec or MainLineSpec part of LinkedLineSpec."""
268
+ if not isinstance(var, spectype):
269
+ print(f'Expected {spectype.__name__} for {varname}, but found: \n'
270
+ f'{var}\nof type {type(var).__name__}',
271
+ file=sys.stderr)
272
+ sys.exit(1)
273
+ ExtractConfig._check_list_str(var.line, 'line in ' + varname)
274
+ ExtractConfig._check_dict_str_lst_str(var.columns,
275
+ 'columns in ' + varname)
276
+
277
+ @staticmethod
278
+ def _check_linkedline(var: LinkedLineList | list[LinkedLineSpec],
279
+ varname: str) -> None:
280
+ """Check that we have correct LinkedLineSpec list."""
281
+ if not isinstance(var, list):
282
+ print(f'Expected a list of LinkedLineSpec in {varname}\n' +
283
+ f'but found: {var}\nof type {type(var).__name__}',
284
+ file=sys.stderr)
285
+ sys.exit(1)
286
+ for elem in var:
287
+ vname = 'element in ' + varname
288
+ ExtractConfig._check_mainline_part(var=elem,
289
+ spectype=LinkedLineSpec,
290
+ varname=vname)
291
+ ExtractConfig._check_list_str(elem.linked_main_column,
292
+ 'linked_main_column in ' + vname)
293
+ ExtractConfig._check_list_str(elem.linked_column,
294
+ 'linked_column in ' + vname)
295
+
296
+ @staticmethod
297
+ def _check_dict_str_lst_str(var: dict[str, list[str]],
298
+ varname: str) -> None:
299
+ """Check that var is dict[str, list[str]]."""
300
+ if not isinstance(var, dict):
301
+ print(f'Expected a dict of strings to lists in {varname}\n' +
302
+ f'but found: {var}\nof type {type(var).__name__}',
303
+ file=sys.stderr)
304
+ sys.exit(1)
305
+ for key, value in var.items():
306
+ if not isinstance(key, str):
307
+ print(f'Expected a dict of strings to lists in {varname}\n' +
308
+ f'but found key: {key}\nof type {type(key).__name__}',
309
+ file=sys.stderr)
310
+ sys.exit(1)
311
+ ExtractConfig._check_list_str(value, key + ' in ' + varname)
312
+
313
+ @staticmethod
314
+ def _check_list_str(var: list[str], varname: str) -> None:
315
+ """Check that variable is list of str."""
316
+ if not isinstance(var, list):
317
+ print(f'Expected a list of strings in {varname}\n' +
318
+ f'but found: {var}\nof type {type(var).__name__}',
319
+ file=sys.stderr)
320
+ sys.exit(1)
321
+ for elem in var:
322
+ if not isinstance(elem, str):
323
+ print(f'Expected a list of strings in {varname}\n' +
324
+ f'but found element: {elem}\n' +
325
+ f'of type {type(elem).__name__}',
326
+ file=sys.stderr)
327
+ sys.exit(1)
328
+
329
+ @staticmethod
330
+ def _check_enum(var: Enum, enum_type: type[Enum], varname: str) -> None:
331
+ """Check that config variable is correct enum type."""
332
+ ExtractConfig._check_type(var=var, oftype=enum_type, varname=varname)
333
+ if var not in enum_type: # pragma: no cover
334
+ allowed = ' ,'.join(list(enum_type))
335
+ print(f'{varname} value {var} is not one of allowed: {allowed}',
336
+ file=sys.stderr)
337
+ sys.exit(1)
338
+
339
+ @staticmethod
340
+ def _check_type(var: SomeCfgTyp, oftype: type[SomeCfgTyp],
341
+ varname: str) -> None:
342
+ """Check that config variable is of type."""
343
+ if not isinstance(var, oftype):
344
+ print(f'Configuration parameter "{varname}" has wrong type. ',
345
+ file=sys.stderr)
346
+ print(f'Type is "{type(var).__name__}", ' +
347
+ f'but expected type "{oftype.__name__}".', file=sys.stderr)
348
+ sys.exit(1)
349
+
350
+ @staticmethod
351
+ def _check_filetype(ftype: InFileType | OutFileType,
352
+ enum_type:
353
+ type[InFileType] | type[OutFileType]) -> None:
354
+ """Check that file types are OK."""
355
+ if not isinstance(ftype, enum_type):
356
+ print(f'File type {ftype} is not of type {enum_type.__name__}',
357
+ file=sys.stderr)
358
+ sys.exit(1)
359
+ if ftype not in enum_type: # pragma: no cover
360
+ allowed = ' ,'.join(list(enum_type))
361
+ print(f'File type {ftype} is not one of allowed types: {allowed}',
362
+ file=sys.stderr)
363
+ sys.exit(1)
364
+
365
+ @staticmethod
366
+ def get_converter_dict(enum_type: type[Enum]) -> ParseConverter:
367
+ """Get dict for converting to given enum_type."""
368
+ return ParseConverter(result_type=enum_type,
369
+ func=string_to_enum_best_match,
370
+ args={'num_type': enum_type})
371
+
372
+ @staticmethod
373
+ def get_converter_mainline(nttype: type[MainLineSpec]) -> ParseConverter:
374
+ """Get dict for converting to given namedtuple type."""
375
+ return ParseConverter(result_type=nttype,
376
+ func=_mline_spec_from_dict,
377
+ args={})
378
+
379
+ @staticmethod
380
+ def get_converter_linkedline() -> ParseConverter:
381
+ """Get dict for converting to linked_lines."""
382
+ return ParseConverter(result_type=LinkedLineList,
383
+ func=_linked_line_from_json_array,
384
+ args={})
385
+
386
+ def parse_converters(self) -> dict[str, ParseConverter]:
387
+ """Get converters for use when parsing JSON.
388
+
389
+ Overriding in derived class.
390
+ Return None if no conversions.
391
+ Return dict of dict for use in json decoder hook.
392
+ Structure of return value shall be:
393
+ {key: {'result type': res_type, 'func': function,
394
+ 'args': {arg_name: arg_value}}}.
395
+ """
396
+ return {'infile_type': self.get_converter_dict(InFileType),
397
+ 'outfile_type': self.get_converter_dict(OutFileType),
398
+ 'outfile_excel_library': self.get_converter_dict(ExcelLib),
399
+ 'missing_input_for_column':
400
+ self.get_converter_dict(MissingInputForColumn),
401
+ 'main_line': self.get_converter_mainline(MainLineSpec),
402
+ 'linked_lines': self.get_converter_linkedline()}
403
+
404
+ def as_json_string(self) -> str:
405
+ """Get JSON string representing this object."""
406
+ if isinstance(self.main_line, dict):
407
+ return super().as_json_string()
408
+ adjusted = deepcopy(self)
409
+ # intentionally violating typing to get wanted JSON
410
+ adjusted.main_line = cast(MainLineSpec, self.main_line.__dict__)
411
+ adjusted.linked_lines = []
412
+ for i in self.linked_lines:
413
+ # intentionally violating typing to get wanted JSON
414
+ adjusted.linked_lines.append(cast(LinkedLineSpec, i.__dict__))
415
+ return adjusted.as_json_string()