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.
- python_table_processor-0.2.29.dist-info/LICENSE +21 -0
- python_table_processor-0.2.29.dist-info/METADATA +28 -0
- python_table_processor-0.2.29.dist-info/RECORD +21 -0
- python_table_processor-0.2.29.dist-info/WHEEL +4 -0
- python_table_processor-0.2.29.dist-info/entry_points.txt +4 -0
- table_processor/__init__.py +2 -0
- table_processor/cli.py +66 -0
- table_processor/commands/convert_tables.py +80 -0
- table_processor/core/actions.py +601 -0
- table_processor/core/config.py +365 -0
- table_processor/core/constants.py +5 -0
- table_processor/core/convert.py +402 -0
- table_processor/core/functions/assign_id.py +75 -0
- table_processor/core/functions/flatten_row.py +24 -0
- table_processor/core/functions/get_nested_field_value.py +22 -0
- table_processor/core/functions/nest_row.py +25 -0
- table_processor/core/functions/search_column_value.py +26 -0
- table_processor/core/functions/set_flat_field_value.py +23 -0
- table_processor/core/functions/set_nested_field_value.py +24 -0
- table_processor/core/functions/set_row_value.py +33 -0
- table_processor/core/types.py +118 -0
|
@@ -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
|