python-table-processor 0.2.29__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,402 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import json
4
+ import math
5
+ import os
6
+
7
+ from collections import OrderedDict
8
+
9
+ from typing import (
10
+ Mapping,
11
+ )
12
+
13
+ # 3-rd party modules
14
+
15
+ from icecream import ic
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ # local
20
+
21
+ from . config import (
22
+ AssignArrayConfig,
23
+ PushConfig,
24
+ setup_config,
25
+ setup_pick_with_args,
26
+ )
27
+ from . constants import (
28
+ FILE_FIELD,
29
+ ROW_INDEX_FIELD,
30
+ FILE_ROW_INDEX_FIELD,
31
+ INPUT_FIELD,
32
+ STAGING_FIELD,
33
+ )
34
+ from . functions.flatten_row import flatten_row
35
+ from . functions.get_nested_field_value import get_nested_field_value
36
+ from . functions.get_nested_field_value import get_nested_field_value
37
+ from . functions.nest_row import nest_row as nest
38
+ from . functions.search_column_value import search_column_value
39
+ from . functions.set_nested_field_value import set_nested_field_value
40
+ from . functions.set_row_value import (
41
+ set_row_value,
42
+ set_row_staging_value,
43
+ )
44
+
45
+ from . actions import (
46
+ do_actions,
47
+ pop_row_staging,
48
+ prepare_row,
49
+ remap_columns,
50
+ setup_actions_with_args,
51
+ )
52
+
53
+ from . types import (
54
+ GlobalStatus,
55
+ )
56
+
57
+ dict_loaders: dict[str, callable] = {}
58
+ def register_loader(
59
+ ext: str,
60
+ ):
61
+ def decorator(loader):
62
+ dict_loaders[ext] = loader
63
+ return loader
64
+ return decorator
65
+
66
+ dict_savers: dict[str, callable] = {}
67
+ def register_saver(
68
+ ext: str,
69
+ ):
70
+ def decorator(saver):
71
+ dict_savers[ext] = saver
72
+ return saver
73
+ return decorator
74
+
75
+ @register_loader('.csv')
76
+ def load_csv(
77
+ input_file: str,
78
+ ):
79
+ # utf-8
80
+ #df = pd.read_csv(input_file)
81
+ # UTF-8 with BOM
82
+ df = pd.read_csv(input_file, encoding='utf-8-sig')
83
+ return df
84
+
85
+ @register_loader('.xlsx')
86
+ def load_excel(
87
+ input_file: str,
88
+ ):
89
+ #df = pd.read_excel(input_file)
90
+ # NOTE: Excelで勝手に日時データなどに変換されてしまうことを防ぐため
91
+ df = pd.read_excel(input_file, dtype=str)
92
+ # NOTE: 列番号でもアクセスできるようフィールドを追加する
93
+ df_with_column_number = pd.read_excel(
94
+ input_file, dtype=str, header=None, skiprows=1
95
+ )
96
+ new_column_names = [f'__values__.{i}' for i in df_with_column_number.columns]
97
+ df2 = df_with_column_number.rename(columns=dict(
98
+ zip(df_with_column_number.columns, new_column_names)
99
+ ))
100
+ df = pd.concat([df, df2], axis=1)
101
+ df = df.dropna(axis=0, how='all')
102
+ df = df.dropna(axis=1, how='all')
103
+ return df
104
+
105
+ @register_loader('.json')
106
+ def load_json(
107
+ input_file: str,
108
+ ):
109
+ with open(input_file, 'r') as f:
110
+ data = json.load(f)
111
+ if not isinstance(data, list):
112
+ raise ValueError(f'Invalid JSON array data: {input_file}')
113
+ #ic(data[0])
114
+ rows = []
115
+ for row in data:
116
+ new_row = flatten_row(row)
117
+ rows.append(new_row)
118
+ df = pd.DataFrame(rows)
119
+ return df
120
+
121
+ @register_saver('.json')
122
+ def save_json(
123
+ df: pd.DataFrame,
124
+ output_file: str,
125
+ ):
126
+ # NOTE: この方法だとスラッシュがすべてエスケープされてしまった
127
+ #df.to_json(
128
+ # output_file,
129
+ # orient='records',
130
+ # force_ascii=False,
131
+ # indent=2,
132
+ # escape_forward_slashes=False,
133
+ #)
134
+ #ic(df.iloc[0])
135
+ data = df.to_dict(orient='records')
136
+ #ic(data[0])
137
+ data = [nest(row) for row in data]
138
+ #ic(data[0])
139
+ with open(output_file, 'w') as f:
140
+ json.dump(
141
+ data,
142
+ f,
143
+ indent=2,
144
+ ensure_ascii=False,
145
+ )
146
+
147
+ @register_loader('.jsonl')
148
+ def load_jsonl(
149
+ input_file: str,
150
+ ):
151
+ rows = []
152
+ with open(input_file, 'r') as f:
153
+ for line in f:
154
+ row = json.loads(line)
155
+ rows.append(row)
156
+ df = pd.DataFrame(rows)
157
+ return df
158
+
159
+ @register_saver('.jsonl')
160
+ def save_jsonl(
161
+ df: pd.DataFrame,
162
+ output_file: str,
163
+ ):
164
+ # NOTE: この方法だとスラッシュがすべてエスケープされてしまった
165
+ #df.to_json(
166
+ # output_file,
167
+ # orient='records',
168
+ # lines=True,
169
+ # force_ascii=False,
170
+ #)
171
+ with open(output_file, 'w') as f:
172
+ for index, row in df.iterrows():
173
+ data = row.to_dict()
174
+ json.dump(
175
+ data,
176
+ f,
177
+ ensure_ascii=False,
178
+ )
179
+ f.write('\n')
180
+
181
+ @register_saver('.csv')
182
+ def save_csv(
183
+ df: pd.DataFrame,
184
+ output_file: str,
185
+ ):
186
+ # utf-8
187
+ #df.to_csv(output_file, index=False)
188
+ # UTF-8 with BOM
189
+ df.to_csv(output_file, index=False, encoding='utf-8-sig')
190
+
191
+ @register_saver('.xlsx')
192
+ def save_excel(
193
+ df: pd.DataFrame,
194
+ output_file: str,
195
+ ):
196
+ # openpyxl
197
+ df.to_excel(output_file, index=False)
198
+ # xlsxwriter
199
+ #writer = pd.ExcelWriter(
200
+ # output_file,
201
+ # engine='xlsxwriter',
202
+ # engine_kwargs={
203
+ # 'options': {
204
+ # 'strings_to_urls': False,
205
+ # },
206
+ # }
207
+ #)
208
+ #df.to_excel(writer, index=False)
209
+ #writer.close()
210
+
211
+ def assign_array(
212
+ row: OrderedDict,
213
+ dict_config: Mapping[str, list[AssignArrayConfig]],
214
+ ):
215
+ new_row = OrderedDict(row)
216
+ #ic(dict_config)
217
+ for key, config in dict_config.items():
218
+ array = []
219
+ for item in config:
220
+ value, found = search_column_value(row, item.field)
221
+ if found and value is not None:
222
+ array.append(value)
223
+ elif not item.optional:
224
+ array.append(None)
225
+ new_row[f'{STAGING_FIELD}.{key}'] = array
226
+ return new_row
227
+
228
+ def search_column_value_from_nested(
229
+ nested_row: OrderedDict,
230
+ column: str,
231
+ ):
232
+ if STAGING_FIELD in nested_row:
233
+ value, found = get_nested_field_value(nested_row[STAGING_FIELD], column)
234
+ if found:
235
+ return value, True
236
+ value, found = get_nested_field_value(nested_row[STAGING_FIELD], column)
237
+ original, found = get_nested_field_value(nested_row, f'{STAGING_FIELD}.{INPUT_FIELD}')
238
+ if found:
239
+ value, found = get_nested_field_value(original, column)
240
+ if found:
241
+ return value, True
242
+ value, found = get_nested_field_value(nested_row, column)
243
+ if found:
244
+ return value, True
245
+ return None, False
246
+
247
+
248
+ def push_fields(
249
+ row: OrderedDict,
250
+ list_config: list[PushConfig],
251
+ ):
252
+ nested_row = nest(row)
253
+ for config in list_config:
254
+ target_value, found = search_column_value_from_nested(nested_row, config.target)
255
+ if found:
256
+ array = target_value
257
+ else:
258
+ array = []
259
+ #set_field_value(nested_row, f'{STAGING_FIELD}.{config.target}', array)
260
+ set_row_staging_value(nested_row, config.target, array)
261
+ source_value, found = search_column_value_from_nested(nested_row, config.source)
262
+ if config.condition is None:
263
+ array.append(source_value)
264
+ continue
265
+ condition_value, found = search_column_value_from_nested(nested_row, config.condition)
266
+ if condition_value:
267
+ array.append(source_value)
268
+ return flatten_row(nested_row)
269
+
270
+ def assign_length(
271
+ row: OrderedDict,
272
+ dict_fields: OrderedDict,
273
+ ):
274
+ new_row = OrderedDict(row)
275
+ for key, field in dict_fields.items():
276
+ value, found = search_column_value(row, field)
277
+ if found:
278
+ new_row[f'{STAGING_FIELD}.{key}'] = len(value)
279
+ return new_row
280
+
281
+ def convert(
282
+ input_files: list[str],
283
+ output_file: str | None = None,
284
+ output_file_filtered_out: str | None = None,
285
+ config_path: str | None = None,
286
+ output_debug: bool = False,
287
+ list_actions: list[str] | None = None,
288
+ list_pick_columns: list[str] | None = None,
289
+ action_delimiter: str = ':',
290
+ verbose: bool = False,
291
+ ignore_file_rows: list[str] | None = None,
292
+ ):
293
+ ic.enable()
294
+ ic()
295
+ ic(input_files)
296
+ df_list = []
297
+ row_list_filtered_out = []
298
+ set_ignore_file_rows = set()
299
+ global_status = GlobalStatus()
300
+ config = setup_config(config_path)
301
+ ic(config)
302
+ if ignore_file_rows:
303
+ set_ignore_file_rows = set(ignore_file_rows)
304
+ if list_pick_columns:
305
+ setup_pick_with_args(config, list_pick_columns)
306
+ if list_actions:
307
+ setup_actions_with_args(
308
+ config,
309
+ list_actions,
310
+ action_delimiter=action_delimiter
311
+ )
312
+ if output_file:
313
+ ext = os.path.splitext(output_file)[1]
314
+ if ext not in dict_savers:
315
+ raise ValueError(f'Unsupported file type: {ext}')
316
+ saver = dict_savers[ext]
317
+ ic(config)
318
+ #return # debug return
319
+ for input_file in input_files:
320
+ ic(input_file)
321
+ if not os.path.exists(input_file):
322
+ raise FileNotFoundError(f'File not found: {input_file}')
323
+ base_name = os.path.basename(input_file)
324
+ ext = os.path.splitext(input_file)[1]
325
+ ic(ext)
326
+ if ext not in dict_loaders:
327
+ raise ValueError(f'Unsupported file type: {ext}')
328
+ df = dict_loaders[ext](input_file)
329
+ # NOTE: NaN を None に変換しておかないと厄介
330
+ df = df.replace([np.nan], [None])
331
+ #ic(df)
332
+ #ic(len(df))
333
+ #ic(df.columns)
334
+ #ic(df.iloc[0])
335
+ #new_rows = []
336
+ new_flat_rows = []
337
+ for index, flat_row in df.iterrows():
338
+ file_row_index = f'{input_file}:{index}'
339
+ if file_row_index in set_ignore_file_rows:
340
+ continue
341
+ short_file_row_index = f'{base_name}:{index}'
342
+ if short_file_row_index in set_ignore_file_rows:
343
+ continue
344
+ #if flat_row.empty:
345
+ # continue
346
+ orig_row = prepare_row(flat_row)
347
+ row = prepare_row(flat_row)
348
+ if STAGING_FIELD not in orig_row.nested:
349
+ set_row_staging_value(row, FILE_FIELD, input_file)
350
+ set_row_staging_value(row, FILE_ROW_INDEX_FIELD, file_row_index)
351
+ set_row_staging_value(row, ROW_INDEX_FIELD, index)
352
+ set_row_staging_value(row, INPUT_FIELD, orig_row.nested)
353
+ if config.process.assign_array:
354
+ row.flat= assign_array(row.flat, config.process.assign_array)
355
+ if config.process.push:
356
+ row.flat = push_fields(row.flat, config.process.push)
357
+ if config.process.assign_length:
358
+ row.flat = assign_length(row.flat, config.process.assign_length)
359
+ if config.actions:
360
+ try:
361
+ new_row = do_actions(global_status, row, config.actions)
362
+ if new_row is None:
363
+ if not output_debug:
364
+ pop_row_staging(row)
365
+ if verbose:
366
+ ic('Filtered out: ', row.flat)
367
+ if output_file_filtered_out:
368
+ row_list_filtered_out.append(row.flat)
369
+ continue
370
+ row = new_row
371
+ except Exception as e:
372
+ if verbose:
373
+ ic(index)
374
+ ic(flat_row)
375
+ ic(row.flat)
376
+ raise e
377
+ if config.pick:
378
+ remap_columns(row, config.pick)
379
+ if not output_debug:
380
+ pop_row_staging(row)
381
+ new_flat_rows.append(row.flat)
382
+ new_df = pd.DataFrame(new_flat_rows)
383
+ df_list.append(new_df)
384
+ # NOTE: concatの仕様が変わり、all-NAの列を含むdfを連結しようとすると警告が出るようになった
385
+ #if ic(new_df.dropna(axis=1, how='all').empty):
386
+ # ic(new_df.dropna(axis=1, how='all'))
387
+ # raise ValueError('No rows to output.')
388
+ #df_list.append(new_df.dropna(axis=1, how='all'))
389
+ all_df = pd.concat(df_list)
390
+ #ic(all_df)
391
+ ic(len(all_df))
392
+ #ic(all_df.columns)
393
+ #ic(all_df.iloc[0])
394
+ if output_file:
395
+ ic('Saing to: ', output_file)
396
+ saver(all_df, output_file)
397
+ else:
398
+ ic(all_df)
399
+ if row_list_filtered_out:
400
+ df_filtered_out = pd.DataFrame(row_list_filtered_out)
401
+ ic('Saving filtered out to: ', output_file_filtered_out)
402
+ saver(df_filtered_out, output_file_filtered_out)
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import dataclasses
4
+ import json
5
+ import os
6
+
7
+ from collections import OrderedDict
8
+ from collections import defaultdict
9
+ from typing import Mapping
10
+
11
+ # 3-rd party modules
12
+
13
+ from icecream import ic
14
+ import numpy as np
15
+ import pandas as pd
16
+
17
+ # local
18
+
19
+ from .. constants import (
20
+ STAGING_FIELD,
21
+ )
22
+
23
+ from .. config import (
24
+ AssignIdConfig,
25
+ Config,
26
+ )
27
+
28
+ from . search_column_value import search_column_value
29
+ from . set_nested_field_value import set_nested_field_value
30
+ from . set_row_value import set_row_staging_value
31
+
32
+ from .. types import (
33
+ AssignIdConfig,
34
+ IdContextMap,
35
+ Row,
36
+ )
37
+
38
+ def assign_id(
39
+ id_context_map: IdContextMap,
40
+ row: Row,
41
+ config: Config,
42
+ ):
43
+ context_columns = []
44
+ context_values = []
45
+ if config.context:
46
+ for context_column in config.context:
47
+ value, found = search_column_value(row.nested, context_column)
48
+ if not found:
49
+ raise KeyError(f'Column not found: {context_column}, existing columns: {row.flat.keys()}')
50
+ context_columns.append(context_column)
51
+ context_values.append(value)
52
+ primary_columns = []
53
+ primary_values = []
54
+ for primary_column in config.primary:
55
+ value, found = search_column_value(row.nested, primary_column)
56
+ if not found:
57
+ raise KeyError(f'Column not found: {primary_column}, existing columns: {row.flat.keys()}')
58
+ primary_columns.append(primary_column)
59
+ primary_values.append(value)
60
+ context_key = (
61
+ tuple(context_columns),
62
+ tuple(context_values),
63
+ tuple(primary_columns),
64
+ )
65
+ primary_value = tuple(primary_values)
66
+ id_map = id_context_map[context_key]
67
+ if primary_value not in id_map.dict_value_to_id:
68
+ field_id = id_map.max_id + 1
69
+ id_map.max_id = field_id
70
+ id_map.dict_value_to_id[primary_value] = field_id
71
+ id_map.dict_id_to_value[field_id] = primary_value
72
+ else:
73
+ field_id = id_map.dict_value_to_id[primary_value]
74
+ set_row_staging_value(row, config.target, field_id)
75
+ return row
@@ -0,0 +1,24 @@
1
+ '''
2
+ This module contains the function to flatten a nested dictionary.
3
+ '''
4
+
5
+ from collections import OrderedDict
6
+ from typing import Mapping
7
+
8
+ type FlatFieldMap = Mapping[str]
9
+ type FieldMap = Mapping[str, str|FieldMap]
10
+
11
+ def flatten_row(
12
+ mapping: FieldMap,
13
+ parent_key: str = '',
14
+ new_mapping: FlatFieldMap | None = None,
15
+ ) -> FlatFieldMap:
16
+ if new_mapping is None:
17
+ new_mapping = OrderedDict()
18
+ for key, mapped in mapping.items():
19
+ new_key = f'{parent_key}.{key}' if parent_key else key
20
+ if isinstance(mapped, Mapping):
21
+ flatten_row(mapped, new_key, new_mapping)
22
+ else:
23
+ new_mapping[new_key] = mapped
24
+ return new_mapping
@@ -0,0 +1,22 @@
1
+ # Description: Get the value of a field in a dictionary.
2
+
3
+ from collections import OrderedDict
4
+
5
+ def get_nested_field_value(
6
+ data: OrderedDict | list,
7
+ field: str,
8
+ ):
9
+ if isinstance(data, list):
10
+ if field.isdigit():
11
+ index = int(field)
12
+ if index < len(data):
13
+ return data[index], True
14
+ return None, False
15
+ if isinstance(data, dict):
16
+ if field in data:
17
+ return data[field], True
18
+ if '.' in field:
19
+ field, rest = field.split('.', 1)
20
+ if field in data:
21
+ return get_nested_field_value(data[field], rest)
22
+ return None, False
@@ -0,0 +1,25 @@
1
+ '''
2
+ This function is used to nest a row. It is used to nest a row that has been unnested.
3
+ '''
4
+
5
+ import math
6
+
7
+ from collections import OrderedDict
8
+ from typing import Mapping
9
+
10
+ from .set_nested_field_value import set_nested_field_value
11
+
12
+ def nest_row(
13
+ row: Mapping,
14
+ remove_nan: bool = True,
15
+ ):
16
+ new_row = OrderedDict()
17
+ for key, value in row.items():
18
+ if isinstance(value, OrderedDict):
19
+ value = nest_row(value)
20
+ if isinstance(value, float):
21
+ if math.isnan(value):
22
+ if remove_nan:
23
+ continue
24
+ set_nested_field_value(new_row, key, value)
25
+ return new_row
@@ -0,0 +1,26 @@
1
+ '''
2
+ This function is used to search for a column value in a row. It will first search in the '__debug__' field, then in the '__debug__.__original__' field, and finally in the row itself. If the value is found, it will be set in the row and returned.
3
+ '''
4
+
5
+ from .. constants import (
6
+ INPUT_FIELD,
7
+ STAGING_FIELD,
8
+ )
9
+
10
+ from collections import OrderedDict
11
+
12
+ from . get_nested_field_value import get_nested_field_value
13
+
14
+ def search_column_value(
15
+ row: OrderedDict,
16
+ column: str,
17
+ ):
18
+ for key in [
19
+ f'{STAGING_FIELD}.{column}',
20
+ column,
21
+ f'{STAGING_FIELD}.{INPUT_FIELD}.{column}',
22
+ ]:
23
+ value, found = get_nested_field_value(row, key)
24
+ if found:
25
+ return value, key
26
+ return None, None
@@ -0,0 +1,23 @@
1
+ '''
2
+ Set a value in a flat row dictionary with a nested key.
3
+ '''
4
+
5
+ from collections import OrderedDict
6
+ from typing import Any
7
+
8
+ def set_flat_field_value(
9
+ flat_row: OrderedDict,
10
+ target: str,
11
+ value: Any,
12
+ depth: int = 0,
13
+ ):
14
+ if depth > 10:
15
+ raise ValueError(
16
+ 'Depth too high'
17
+ )
18
+ if isinstance(value, dict):
19
+ for key in value.keys():
20
+ set_flat_field_value(flat_row, f'{target}.{key}', value[key], depth + 1)
21
+ else:
22
+ flat_row[target] = value
23
+ return flat_row
@@ -0,0 +1,24 @@
1
+ '''
2
+ Set the value of a field in a nested dictionary.
3
+ '''
4
+
5
+ from collections import OrderedDict
6
+ from icecream import ic
7
+
8
+ def set_nested_field_value(
9
+ data: OrderedDict,
10
+ field: str,
11
+ value: any,
12
+ ):
13
+ if isinstance(field, str) and '.' in field:
14
+ field, rest = field.split('.', 1)
15
+ sub_data = data.get(field)
16
+ if not isinstance(sub_data, dict):
17
+ data[field] = OrderedDict()
18
+ set_nested_field_value(data[field], rest, value)
19
+ else:
20
+ try:
21
+ data[field] = value
22
+ except:
23
+ ic(data, field, value)
24
+ raise
@@ -0,0 +1,33 @@
1
+ '''
2
+ Set a value in a row, both in the flat and nested representations.
3
+ '''
4
+
5
+ from typing import Any
6
+
7
+ from .. constants import (
8
+ STAGING_FIELD,
9
+ )
10
+
11
+ from . set_flat_field_value import set_flat_field_value
12
+ from . set_nested_field_value import set_nested_field_value
13
+
14
+ from .. types import (
15
+ Row,
16
+ )
17
+
18
+ def set_row_value(
19
+ row: Row,
20
+ target: str,
21
+ value: Any,
22
+ ):
23
+ set_flat_field_value(row.flat, target, value)
24
+ set_nested_field_value(row.nested, target, value)
25
+ return row
26
+
27
+ def set_row_staging_value(
28
+ row: Row,
29
+ target: str,
30
+ value: Any,
31
+ ):
32
+ set_row_value(row, f'{STAGING_FIELD}.{target}', value)
33
+ return row