python-table-converter 0.2.13__tar.gz → 0.2.15__tar.gz

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.
Files changed (24) hide show
  1. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/PKG-INFO +2 -1
  2. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/pyproject.toml +2 -1
  3. python_table_converter-0.2.15/table_converter/__init__.py +2 -0
  4. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/table_converter/commands/convert_tables.py +16 -36
  5. python_table_converter-0.2.15/table_converter/core/actions.py +423 -0
  6. python_table_converter-0.2.15/table_converter/core/config.py +365 -0
  7. python_table_converter-0.2.15/table_converter/core/convert.py +331 -0
  8. python_table_converter-0.2.15/table_converter/core/functions/assign_id.py +75 -0
  9. python_table_converter-0.2.13/table_converter/core/functions/flatten.py → python_table_converter-0.2.15/table_converter/core/functions/flatten_row.py +2 -2
  10. python_table_converter-0.2.13/table_converter/core/functions/get_field_value.py → python_table_converter-0.2.15/table_converter/core/functions/get_nested_field_value.py +2 -2
  11. python_table_converter-0.2.15/table_converter/core/functions/nest_row.py +25 -0
  12. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/table_converter/core/functions/search_column_value.py +11 -10
  13. python_table_converter-0.2.15/table_converter/core/functions/set_flat_field_value.py +23 -0
  14. python_table_converter-0.2.13/table_converter/core/functions/set_field_value.py → python_table_converter-0.2.15/table_converter/core/functions/set_nested_field_value.py +3 -3
  15. python_table_converter-0.2.15/table_converter/core/functions/set_row_value.py +33 -0
  16. python_table_converter-0.2.15/table_converter/core/types.py +100 -0
  17. python_table_converter-0.2.13/table_converter/__init__.py +0 -2
  18. python_table_converter-0.2.13/table_converter/core/config.py +0 -215
  19. python_table_converter-0.2.13/table_converter/core/convert.py +0 -386
  20. python_table_converter-0.2.13/table_converter/core/functions/assign_id.py +0 -112
  21. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/LICENSE +0 -0
  22. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/README.md +0 -0
  23. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/table_converter/cli.py +0 -0
  24. {python_table_converter-0.2.13 → python_table_converter-0.2.15}/table_converter/core/constants.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-table-converter
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: A table data converter
5
5
  Home-page: https://github.com/akivajp/python-table-converter
6
6
  License: MIT
@@ -18,6 +18,7 @@ Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
18
18
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
19
19
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
20
20
  Requires-Dist: tqdm (>=4.67.0,<5.0.0)
21
+ Requires-Dist: xlsxwriter (>=3.2.0,<4.0.0)
21
22
  Project-URL: Repository, https://github.com/akivajp/python-table-converter
22
23
  Description-Content-Type: text/markdown
23
24
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-table-converter"
3
- version = "0.2.13"
3
+ version = "0.2.15"
4
4
  description = "A table data converter"
5
5
  authors = ["Akiva Miura <akiva.miura@gmail.com>"]
6
6
  license = "MIT"
@@ -26,6 +26,7 @@ tqdm = "^4.67.0"
26
26
  pandas = "^2.2.3"
27
27
  openpyxl = "^3.1.5"
28
28
  pyyaml = "^6.0.2"
29
+ xlsxwriter = "^3.2.0"
29
30
 
30
31
 
31
32
  [build-system]
@@ -0,0 +1,2 @@
1
+ __version__ = "0.2.15"
2
+ __version_tuple__ = (0, 2, 15)
@@ -12,15 +12,12 @@ def run(
12
12
  convert(
13
13
  input_files = args.input_files,
14
14
  output_file = args.output_file,
15
+ output_file_filtered_out = args.output_file_filtered_out,
15
16
  config_path = args.config,
16
- assign_constants = args.assign_constants,
17
- assign_formats = args.assign_formats,
18
- str_filters = args.filters,
19
- pickup_columns= args.pickup_columns,
20
- fields_to_split_by_newline = args.split_by_newline,
21
- fields_to_assign_ids = args.assign_ids,
22
- str_omit_fields= args.omit_fields,
17
+ list_actions = args.do_actions,
18
+ list_pick_columns = args.pick_columns,
23
19
  output_debug = args.output_debug,
20
+ verbose = args.verbose,
24
21
  )
25
22
 
26
23
  def setup_parser(
@@ -39,44 +36,27 @@ def setup_parser(
39
36
  help='Path to the output file.'
40
37
  )
41
38
  parser.add_argument(
42
- '--config', '-c',
43
- type=str,
44
- help='Path to the configuration file.',
45
- )
46
- parser.add_argument(
47
- '--pickup-columns',
48
- type=str,
49
- help='Pickup column map',
39
+ '--output-file-filtered-out', '--output-filtered-out', '-f',
40
+ metavar='OUTPUT_FILE_FILTERED_OUT',
41
+ required=False,
42
+ help='Path to the output file for filtered out rows.',
50
43
  )
51
44
  parser.add_argument(
52
- '--split-by-newline',
53
- type=str,
54
- help='Fields to split by newline',
55
- )
56
- parser.add_argument(
57
- '--assign-ids',
58
- type=str,
59
- help='Field to assign ids',
60
- )
61
- parser.add_argument(
62
- '--assign-constants',
63
- type=str,
64
- help='Field to assign constants',
65
- )
66
- parser.add_argument(
67
- '--assign-formats',
45
+ '--config', '-c',
68
46
  type=str,
69
- help='Field to assign formats',
47
+ help='Path to the configuration file.',
70
48
  )
71
49
  parser.add_argument(
72
- '--filters', '--filter', '-f',
50
+ '--pick-columns', '--pick',
73
51
  type=str,
74
- help='Expression list to filter records',
52
+ nargs='+',
53
+ help='Pick column map',
75
54
  )
76
55
  parser.add_argument(
77
- '--omit-fields', '--omit',
56
+ '--do-actions', '--actions', '--do',
57
+ nargs='+',
78
58
  type=str,
79
- help='Field to omit',
59
+ help='Actions to do',
80
60
  )
81
61
  parser.add_argument(
82
62
  '--output-debug',
@@ -0,0 +1,423 @@
1
+ '''
2
+ Actions are used to transform the data in the table.
3
+ '''
4
+
5
+ from collections import OrderedDict
6
+ from dataclasses import dataclass
7
+ from typing import (
8
+ Any,
9
+ )
10
+
11
+ from icecream import ic
12
+
13
+ from . config import (
14
+ Config,
15
+ )
16
+
17
+ from . constants import (
18
+ INPUT_FIELD,
19
+ STAGING_FIELD,
20
+ )
21
+
22
+ from . types import (
23
+ AssignConstantConfig,
24
+ AssignFormatConfig,
25
+ AssignIdConfig,
26
+ FilterConfig,
27
+ GlobalStatus,
28
+ JoinConfig,
29
+ OmitConfig,
30
+ PickConfig,
31
+ SplitConfig,
32
+ Row,
33
+ )
34
+
35
+ from . functions.assign_id import assign_id
36
+ from . functions.flatten_row import flatten_row
37
+ from . functions.nest_row import nest_row
38
+ from . functions.search_column_value import search_column_value
39
+ from . functions.set_flat_field_value import set_flat_field_value
40
+ from . functions.set_row_value import (
41
+ set_row_staging_value,
42
+ )
43
+ from . functions.set_nested_field_value import set_nested_field_value
44
+
45
+ def setup_actions_with_args(
46
+ config: Config,
47
+ list_actions: list[str],
48
+ ):
49
+ ic(list_actions)
50
+ for str_action in list_actions:
51
+ fields = str_action.split(':')
52
+ if len(fields) >= 1:
53
+ action_name = fields[0].strip()
54
+ if action_name == 'assign-format':
55
+ setup_assign_format_action(config, str_action)
56
+ continue
57
+ if action_name == 'filter':
58
+ setup_filter_action(config, str_action)
59
+ continue
60
+ if len(fields) not in [2,3]:
61
+ raise ValueError(
62
+ f'Action must have 2 or 3 colon-separated fields: {str_action}'
63
+ )
64
+ str_fields = fields[1].strip()
65
+ if len(fields) == 3:
66
+ str_options = fields[2].strip()
67
+ else:
68
+ str_options = ''
69
+ options = OrderedDict()
70
+ if str_options:
71
+ for str_option in str_options.split(','):
72
+ if '=' in str_option:
73
+ key, value = str_option.split('=')
74
+ options[key.strip()] = value.strip()
75
+ else:
76
+ options[str_option.strip()] = True
77
+ fields = str_fields.split(',')
78
+ for field in fields:
79
+ if '=' in field:
80
+ target, source = field.split('=')
81
+ target = target.strip()
82
+ source = source.strip()
83
+ else:
84
+ target = field.strip()
85
+ source = field.strip()
86
+ if action_name == 'assign-constant':
87
+ str_type = options.get('type', 'str')
88
+ if str_type in ['str', 'string']:
89
+ value = source
90
+ elif str_type in ['int', 'integer']:
91
+ value = int(source)
92
+ elif str_type == 'float':
93
+ value = float(source)
94
+ elif str_type in ['bool', 'boolean']:
95
+ value = bool(source)
96
+ else:
97
+ raise ValueError(
98
+ f'Unsupported type: {str_type}'
99
+ )
100
+ config.actions.append(AssignConstantConfig(
101
+ target = target,
102
+ value = value,
103
+ ))
104
+ continue
105
+ if action_name == 'assign-id':
106
+ context = options.get('context', None)
107
+ if context:
108
+ context = context.split(',')
109
+ config.actions.append(AssignIdConfig(
110
+ target = target,
111
+ primary = [source],
112
+ context = context,
113
+ ))
114
+ continue
115
+ if action_name == 'join':
116
+ delimiter = options.get('delimiter', None)
117
+ config.actions.append(JoinConfig(
118
+ target = target,
119
+ source = source,
120
+ delimiter = delimiter,
121
+ ))
122
+ continue
123
+ if action_name == 'omit':
124
+ config.actions.append(OmitConfig(
125
+ field = target,
126
+ ))
127
+ continue
128
+ if action_name == 'split':
129
+ delimiter = options.get('delimiter', None)
130
+ config.actions.append(SplitConfig(
131
+ target = target,
132
+ source = source,
133
+ delimiter = delimiter,
134
+ ))
135
+ continue
136
+ raise ValueError(
137
+ f'Unsupported action: {action_name}'
138
+ )
139
+ return config
140
+
141
+ def setup_assign_format_action(
142
+ config: Config,
143
+ str_action: str,
144
+ ):
145
+ action_fields = str_action.split(':', 1)
146
+ if len(action_fields) != 2:
147
+ raise ValueError(
148
+ f'Expected 2 fields separated by ":": {str_action}'
149
+ )
150
+ action_name = action_fields[0].strip()
151
+ assert action_name == 'assign-format'
152
+ assignment_fields = action_fields[1].split('=')
153
+ if len(assignment_fields) != 2:
154
+ raise ValueError(
155
+ f'Expected 2 fields separated by "=": {action_fields[1]}'
156
+ )
157
+ target = assignment_fields[0].strip()
158
+ format = assignment_fields[1].strip()
159
+ config.actions.append(AssignFormatConfig(
160
+ target = target,
161
+ format = format,
162
+ ))
163
+ return config
164
+
165
+ def setup_filter_action(
166
+ config: Config,
167
+ str_action: str,
168
+ ):
169
+ action_fields = str_action.split(':', 1)
170
+ if len(action_fields) != 2:
171
+ raise ValueError(
172
+ f'Expected 2 fields separated by ":": {str_action}'
173
+ )
174
+ action_name = action_fields[0].strip()
175
+ assert action_name == 'filter'
176
+ str_filter = action_fields[1].strip()
177
+ if '==' in str_filter:
178
+ field, value = str_filter.split('==')
179
+ config.actions.append(FilterConfig(
180
+ field = field.strip(),
181
+ operator = '==',
182
+ value = value.strip(),
183
+ ))
184
+ return config
185
+ if '!=' in str_filter:
186
+ field, value = str_filter.split('!=')
187
+ config.actions.append(FilterConfig(
188
+ field = field.strip(),
189
+ operator = '!=',
190
+ value = value.strip(),
191
+ ))
192
+ return config
193
+ raise ValueError(
194
+ f'Unsupported filter: {str_filter}'
195
+ )
196
+
197
+ def do_actions(
198
+ status: GlobalStatus,
199
+ row: Row,
200
+ actions: list[AssignConstantConfig],
201
+ ):
202
+ for action in actions:
203
+ row = do_action(status, row, action)
204
+ if row is None:
205
+ return None
206
+ return row
207
+
208
+ def do_action(
209
+ status: GlobalStatus,
210
+ row: Row,
211
+ action: AssignConstantConfig,
212
+ ):
213
+ if isinstance(action, AssignConstantConfig):
214
+ return assign_constant(row, action)
215
+ if isinstance(action, AssignFormatConfig):
216
+ return assign_format(row, action)
217
+ if isinstance(action, AssignIdConfig):
218
+ return assign_id(status.id_context_map, row, action)
219
+ if isinstance(action, FilterConfig):
220
+ if filter_row(row, action):
221
+ return row
222
+ return None
223
+ if isinstance(action, JoinConfig):
224
+ return join_field(row, action)
225
+ if isinstance(action, OmitConfig):
226
+ return omit_field(row, action)
227
+ if isinstance(action, SplitConfig):
228
+ return split_field(row, action)
229
+ raise ValueError(
230
+ f'Unsupported action: {action}'
231
+ )
232
+
233
+ def prepare_row(
234
+ flat_row: OrderedDict | None = None,
235
+ ):
236
+ if flat_row is None:
237
+ flat_row = OrderedDict()
238
+ nested_row = nest_row(flat_row)
239
+ return Row(
240
+ flat = OrderedDict(flat_row),
241
+ nested = nested_row,
242
+ )
243
+
244
+ def delete_flat_row_value(
245
+ flat_row: OrderedDict,
246
+ target: str,
247
+ ):
248
+ prefix = f'{target}.'
249
+ for key in list(flat_row.keys()):
250
+ if key == target or key.startswith(prefix):
251
+ del flat_row[key]
252
+
253
+ def pop_nested_row_value(
254
+ nested_row: OrderedDict,
255
+ key: str,
256
+ default: Any = None,
257
+ ):
258
+ keys = key.split('.')
259
+ for key in keys[:-1]:
260
+ if key not in nested_row:
261
+ return default, False
262
+ nested_row = nested_row[key]
263
+ return nested_row.pop(keys[-1], default), True
264
+
265
+ def pop_row_value(
266
+ row: Row,
267
+ key: str,
268
+ default: Any = None,
269
+ ):
270
+ delete_flat_row_value(row.flat, key)
271
+ return pop_nested_row_value(row.nested, key, default)
272
+
273
+ def pop_row_staging(
274
+ row: Row,
275
+ default: Any = None,
276
+ ):
277
+ return pop_row_value(row, STAGING_FIELD, default)
278
+
279
+ def assign_constant(
280
+ row: Row,
281
+ config: AssignConstantConfig,
282
+ ):
283
+ set_row_staging_value(row, config.target, config.value)
284
+ return row
285
+
286
+ def split_field(
287
+ row: Row,
288
+ config: SplitConfig,
289
+ ):
290
+ value, found = search_column_value(row.flat, config.source)
291
+ if found:
292
+ if isinstance(value, str):
293
+ new_value = value.split(config.delimiter)
294
+ new_value = list(filter(None, new_value))
295
+ value = new_value
296
+ set_row_staging_value(row, config.target, value)
297
+ return row
298
+
299
+ def remap_columns(
300
+ row: Row,
301
+ list_config: list[PickConfig],
302
+ ):
303
+ if not list_config:
304
+ list_config = []
305
+ for key in row.nested[STAGING_FIELD][INPUT_FIELD].keys():
306
+ list_config.append(PickConfig(
307
+ source = key,
308
+ target = key,
309
+ ))
310
+ new_flat_row = OrderedDict()
311
+ picked = []
312
+ for config in list_config:
313
+ value, key = search_column_value(row.nested, config.source)
314
+ if key:
315
+ set_flat_field_value(new_flat_row, config.target, value)
316
+ picked.append(key)
317
+ for key in row.flat.keys():
318
+ if key in picked:
319
+ if not key.startswith(f'{STAGING_FIELD}.{INPUT_FIELD}.'):
320
+ continue
321
+ if key in new_flat_row:
322
+ continue
323
+ if key.startswith(f'{STAGING_FIELD}.'):
324
+ # NOTE: Skip staging fields
325
+ new_flat_row[key] = row.flat[key]
326
+ else:
327
+ input_key = f'{STAGING_FIELD}.{INPUT_FIELD}.{key}'
328
+ if input_key in row.flat:
329
+ value = row.flat[key]
330
+ input_value = row.flat[input_key]
331
+ if value == input_value:
332
+ # NOTE: Skip if the same value in the input field
333
+ continue
334
+ # NOTE: Set the unused value to the staging field
335
+ new_flat_row[f'{STAGING_FIELD}.{key}'] = row.flat[key]
336
+ row.flat = new_flat_row
337
+ row.nested = nest_row(new_flat_row)
338
+ return row
339
+
340
+ def assign_format(
341
+ row: Row,
342
+ config: AssignFormatConfig,
343
+ ):
344
+ template = config.format
345
+ params = {}
346
+ for key, value in row.flat.items():
347
+ for prefix in [
348
+ f'{STAGING_FIELD}.{INPUT_FIELD}.',
349
+ f'{STAGING_FIELD}.',
350
+ ]:
351
+ if key.startswith(prefix):
352
+ rest = key[len(prefix):]
353
+ params[rest] = value
354
+ params.update(row.flat)
355
+ formatted = None
356
+ while formatted is None:
357
+ try:
358
+ formatted = template.format(**params)
359
+ except KeyError as e:
360
+ #ic(e)
361
+ #ic(e.args)
362
+ #ic(e.args[0])
363
+ key = e.args[0]
364
+ params[key] = f'__{key}__undefined__'
365
+ except:
366
+ #ic(params)
367
+ ic(params.keys())
368
+ raise
369
+ set_row_staging_value(row, config.target, formatted)
370
+ return row
371
+
372
+ def filter_row(
373
+ row: Row,
374
+ config: list[FilterConfig],
375
+ ):
376
+ value, found = search_column_value(row.nested, config.field)
377
+ #ic(config, value, found)
378
+ if config.operator == '==':
379
+ if not found:
380
+ return False
381
+ if value != config.value and str(value) != str(config.value):
382
+ return False
383
+ elif config.operator == '!=':
384
+ if str(value) == str(config.value) or value == config.value:
385
+ return False
386
+ elif config.operator == 'not-in':
387
+ if isinstance(config.value, list):
388
+ if value in config.value:
389
+ return False
390
+ if str(value) in config.value:
391
+ return False
392
+ else:
393
+ raise ValueError(f'Unsupported filter value type: type{config.value}')
394
+ else:
395
+ raise ValueError(f'Unsupported operator: {config.operator}')
396
+ return True
397
+
398
+ def omit_field(
399
+ row: Row,
400
+ config: OmitConfig,
401
+ ):
402
+ value, found = pop_row_value(row, config.field)
403
+ if not found:
404
+ return row
405
+ if f'{STAGING_FIELD}.{config.field}' not in row.flat:
406
+ set_row_staging_value(row, config.field, value)
407
+ return row
408
+
409
+ def join_field(
410
+ row: Row,
411
+ config: JoinConfig,
412
+ ):
413
+ value, found = search_column_value(row.nested, config.source)
414
+ if found:
415
+ delimiter = config.delimiter
416
+ if delimiter is None:
417
+ delimiter = ';'
418
+ if delimiter == '\\n':
419
+ delimiter = '\n'
420
+ if isinstance(value, list):
421
+ value = delimiter.join(value)
422
+ set_row_staging_value(row, config.target, value)
423
+ return row