python-table-converter 0.2.8__tar.gz → 0.2.10__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 (17) hide show
  1. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/PKG-INFO +1 -1
  2. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/pyproject.toml +1 -1
  3. python_table_converter-0.2.10/table_converter/__init__.py +2 -0
  4. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/commands/convert_tables.py +12 -0
  5. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/config.py +19 -20
  6. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/convert.py +124 -54
  7. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/functions/assign_id.py +6 -2
  8. python_table_converter-0.2.10/table_converter/core/functions/flatten.py +24 -0
  9. python_table_converter-0.2.10/table_converter/core/functions/search_column_value.py +25 -0
  10. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/functions/set_field_value.py +1 -0
  11. python_table_converter-0.2.8/table_converter/__init__.py +0 -2
  12. python_table_converter-0.2.8/table_converter/core/functions/search_column_value.py +0 -30
  13. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/LICENSE +0 -0
  14. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/README.md +0 -0
  15. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/cli.py +0 -0
  16. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/constants.py +0 -0
  17. {python_table_converter-0.2.8 → python_table_converter-0.2.10}/table_converter/core/functions/get_field_value.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-table-converter
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Summary: A table data converter
5
5
  Home-page: https://github.com/akivajp/python-table-converter
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-table-converter"
3
- version = "0.2.8"
3
+ version = "0.2.10"
4
4
  description = "A table data converter"
5
5
  authors = ["Akiva Miura <akiva.miura@gmail.com>"]
6
6
  license = "MIT"
@@ -0,0 +1,2 @@
1
+ __version__ = "0.2.10"
2
+ __version_tuple__ = (0, 2, 10)
@@ -15,9 +15,11 @@ def run(
15
15
  config_path = args.config,
16
16
  assign_constants = args.assign_constants,
17
17
  assign_formats = args.assign_formats,
18
+ str_filters = args.filters,
18
19
  pickup_columns= args.pickup_columns,
19
20
  fields_to_split_by_newline = args.split_by_newline,
20
21
  fields_to_assign_ids = args.assign_ids,
22
+ str_omit_fields= args.omit_fields,
21
23
  output_debug = args.output_debug,
22
24
  )
23
25
 
@@ -66,6 +68,16 @@ def setup_parser(
66
68
  type=str,
67
69
  help='Field to assign formats',
68
70
  )
71
+ parser.add_argument(
72
+ '--filters', '--filter', '-f',
73
+ type=str,
74
+ help='Expression list to filter records',
75
+ )
76
+ parser.add_argument(
77
+ '--omit-fields', '--omit',
78
+ type=str,
79
+ help='Field to omit',
80
+ )
69
81
  parser.add_argument(
70
82
  '--output-debug',
71
83
  action='store_true',
@@ -2,26 +2,39 @@
2
2
 
3
3
  from collections import OrderedDict
4
4
  import dataclasses
5
- from typing import Mapping
5
+ from typing import (
6
+ Literal,
7
+ Mapping,
8
+ )
6
9
 
7
10
  from icecream import ic
8
11
  import yaml
9
12
 
10
- type FlatFieldMap = Mapping[str, str]
11
- type FieldMap = Mapping[str, str|FieldMap]
13
+ from . functions.flatten import (
14
+ FieldMap,
15
+ FlatFieldMap,
16
+ flatten,
17
+ )
12
18
 
13
19
  @dataclasses.dataclass
14
20
  class AssignIdConfig:
15
21
  primary: list[str]
16
- #given: list[str] | None = None
17
22
  context: list[str] | None = None
18
23
 
24
+ @dataclasses.dataclass
25
+ class FilterConfig:
26
+ field: str
27
+ operator: Literal['==', '!=', '>', '>=', '<', '<=']
28
+ value: str
29
+
19
30
  @dataclasses.dataclass
20
31
  class ProcessConfig:
21
32
  assign_constants: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
22
33
  assign_formats: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
23
- #assign_ids: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
24
34
  assign_ids: Mapping[str, AssignIdConfig] = dataclasses.field(default_factory=OrderedDict)
35
+ #filter_eq: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
36
+ filter: list[FilterConfig] = dataclasses.field(default_factory=list)
37
+ omit_fields: list[str] = dataclasses.field(default_factory=list)
25
38
  split_by_newline: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
26
39
 
27
40
  def __setitem__(self, key, value):
@@ -32,21 +45,6 @@ class Config:
32
45
  map: FieldMap = dataclasses.field(default_factory=OrderedDict)
33
46
  process: ProcessConfig = dataclasses.field(default_factory=ProcessConfig)
34
47
 
35
- def flatten(
36
- mapping: FieldMap,
37
- parent_key: str = '',
38
- new_mapping: FlatFieldMap | None = None,
39
- ) -> FlatFieldMap:
40
- if new_mapping is None:
41
- new_mapping = OrderedDict()
42
- for key, mapped in mapping.items():
43
- new_key = f'{parent_key}.{key}' if parent_key else key
44
- if isinstance(mapped, Mapping):
45
- flatten(mapped, new_key, new_mapping)
46
- else:
47
- new_mapping[new_key] = mapped
48
- return new_mapping
49
-
50
48
  def setup_config(
51
49
  config_path: str | None = None,
52
50
  ):
@@ -78,6 +76,7 @@ def setup_process_config(
78
76
  for process_key in [
79
77
  'assign_constants',
80
78
  'assign_formats',
79
+ 'filter_eq',
81
80
  'split_by_newline',
82
81
  ]:
83
82
  dict_subprocess = dict_process.get(process_key)
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  import json
4
+ import math
4
5
  import os
5
6
 
6
7
  from collections import OrderedDict
@@ -13,7 +14,10 @@ import pandas as pd
13
14
 
14
15
  # local
15
16
 
16
- from . config import setup_config
17
+ from . config import (
18
+ FilterConfig,
19
+ setup_config,
20
+ )
17
21
  from . constants import (
18
22
  FILE_FIELD,
19
23
  INPUT_FIELD,
@@ -24,7 +28,11 @@ from . functions.assign_id import (
24
28
  create_id_context_map,
25
29
  setup_assign_ids,
26
30
  )
31
+ from . functions.flatten import (
32
+ flatten,
33
+ )
27
34
  from . functions.get_field_value import get_field_value
35
+ from . functions.search_column_value import search_column_value
28
36
  from . functions.set_field_value import set_field_value
29
37
 
30
38
  dict_loaders: dict[str, callable] = {}
@@ -58,9 +66,31 @@ def load_json(
58
66
  ):
59
67
  with open(input_file, 'r') as f:
60
68
  data = json.load(f)
61
- df = pd.DataFrame(data)
69
+ if not isinstance(data, list):
70
+ raise ValueError(f'Invalid JSON array data: {input_file}')
71
+ #ic(data[0])
72
+ rows = []
73
+ for row in data:
74
+ new_row = flatten(row)
75
+ rows.append(new_row)
76
+ df = pd.DataFrame(rows)
62
77
  return df
63
78
 
79
+ def nest(
80
+ row: OrderedDict,
81
+ remove_nan: bool = True,
82
+ ):
83
+ new_row = OrderedDict()
84
+ for key, value in row.items():
85
+ if isinstance(value, OrderedDict):
86
+ value = nest(value)
87
+ if isinstance(value, float):
88
+ if math.isnan(value):
89
+ if remove_nan:
90
+ continue
91
+ set_field_value(new_row, key, value)
92
+ return new_row
93
+
64
94
  @register_saver('.json')
65
95
  def save_json(
66
96
  df: pd.DataFrame,
@@ -76,6 +106,9 @@ def save_json(
76
106
  #)
77
107
  #ic(df.iloc[0])
78
108
  data = df.to_dict(orient='records')
109
+ #ic(data[0])
110
+ data = [nest(row) for row in data]
111
+ #ic(data[0])
79
112
  with open(output_file, 'w') as f:
80
113
  json.dump(
81
114
  data,
@@ -106,34 +139,13 @@ def save_jsonl(
106
139
  )
107
140
  f.write('\n')
108
141
 
109
- def search_column_value(
110
- row: OrderedDict,
111
- column: str,
112
- ):
113
- if STAGING_FIELD in row:
114
- value, found = get_field_value(row[STAGING_FIELD], column)
115
- if found:
116
- return value, True
117
- value, found = get_field_value(row[STAGING_FIELD], column)
118
- original, found = get_field_value(row, f'{STAGING_FIELD}.{INPUT_FIELD}')
119
- if found:
120
- value, found = get_field_value(original, column)
121
- if found:
122
- return value, True
123
- value, found = get_field_value(row, column)
124
- if found:
125
- set_field_value(row, column, value)
126
- return value, True
127
- return None, False
128
-
129
142
  def map_constants(
130
143
  row: OrderedDict,
131
144
  dict_constants: OrderedDict,
132
145
  ):
133
146
  new_row = OrderedDict(row)
134
147
  for column in dict_constants.keys():
135
- #set_field_value(new_row, column, dict_constants[column])
136
- set_field_value(new_row, f'{STAGING_FIELD}.{column}', dict_constants[column])
148
+ new_row[f'{STAGING_FIELD}.{column}'] = dict_constants[column]
137
149
  return new_row
138
150
 
139
151
  def map_formats(
@@ -144,7 +156,11 @@ def map_formats(
144
156
  for column in dict_formats.keys():
145
157
  template = dict_formats[column]
146
158
  params = {}
147
- params.update(row[STAGING_FIELD])
159
+ for key, value in row.items():
160
+ prefix = f'{STAGING_FIELD}.'
161
+ if key.startswith(prefix):
162
+ rest = key[len(prefix):]
163
+ params[rest] = value
148
164
  params.update(row)
149
165
  formatted = None
150
166
  while formatted is None:
@@ -158,7 +174,7 @@ def map_formats(
158
174
  params[key] = f'__{key}__undefined__'
159
175
  except:
160
176
  raise
161
- set_field_value(new_row, f'{STAGING_FIELD}.{column}', formatted)
177
+ new_row[f'{STAGING_FIELD}.{column}'] = formatted
162
178
  return new_row
163
179
 
164
180
  def remap_columns(
@@ -169,11 +185,12 @@ def remap_columns(
169
185
  for column in dict_remap.keys():
170
186
  value, found = search_column_value(row, dict_remap[column])
171
187
  if found:
172
- set_field_value(new_row, column, value)
188
+ new_row[column] = value
173
189
  for column in row.keys():
174
- if column == STAGING_FIELD:
175
- # NOTE: Ignore debug fields
176
- set_field_value(new_row, column, row[column])
190
+ prefix = f'{STAGING_FIELD}.'
191
+ if column.startswith(prefix):
192
+ # NOTE: Leave staging fields as is
193
+ new_row[column] = row[column]
177
194
  return new_row
178
195
 
179
196
  def apply_fields_split_by_newline(
@@ -183,21 +200,40 @@ def apply_fields_split_by_newline(
183
200
  new_row = OrderedDict(row)
184
201
  for column in dict_fields:
185
202
  value, found = search_column_value(row, dict_fields[column])
186
- #ic(value, found)
187
203
  if found:
188
204
  if isinstance(value, str):
189
205
  new_value = value.split('\n')
190
- set_field_value(new_row, f'{STAGING_FIELD}.{column}', new_value)
206
+ new_row[f'{STAGING_FIELD}.{column}'] = new_value
191
207
  else:
192
- set_field_value(new_row, f'{STAGING_FIELD}.{column}', value)
208
+ new_row[f'{STAGING_FIELD}.{column}'] = value
193
209
  return new_row
194
210
 
211
+ def filter_row(
212
+ row: OrderedDict,
213
+ list_filters: list[FilterConfig],
214
+ ):
215
+ for config in list_filters:
216
+ value, found = search_column_value(row, config.field)
217
+ if config.operator == '==':
218
+ if not found:
219
+ return False
220
+ if str(value) != str(config.value):
221
+ return False
222
+ elif config.operator == '!=':
223
+ if str(value) == str(config.value):
224
+ return False
225
+ else:
226
+ raise ValueError(f'Unsupported operator: {config.operator}')
227
+ return True
228
+
195
229
  def convert(
196
230
  input_files: list[str],
197
231
  output_file: str | None = None,
198
232
  config_path: str | None = None,
199
233
  assign_constants: str | None = None,
200
234
  assign_formats: str | None = None,
235
+ str_filters: str | None = None,
236
+ str_omit_fields: str | None = None,
201
237
  pickup_columns: str | None = None,
202
238
  fields_to_split_by_newline: str | None = None,
203
239
  fields_to_assign_ids: str | None = None,
@@ -242,6 +278,29 @@ def convert(
242
278
  config.process.split_by_newline[dst] = src
243
279
  else:
244
280
  raise ValueError(f'Invalid split by newline: {field}')
281
+ if str_filters:
282
+ fields = str_filters.split(',')
283
+ for field in fields:
284
+ if '==' in field:
285
+ column, value = field.split('==')
286
+ config.process.filter.append(FilterConfig(
287
+ field = column,
288
+ operator = '==',
289
+ value = value,
290
+ ))
291
+ elif '!=' in field:
292
+ column, value = field.split('!=')
293
+ config.process.filter.append(FilterConfig(
294
+ field = column,
295
+ operator = '!=',
296
+ value = value,
297
+ ))
298
+ else:
299
+ raise ValueError(f'Invalid filter eq: {field}')
300
+ if str_omit_fields:
301
+ fields = str_omit_fields.split(',')
302
+ for field in fields:
303
+ config.process.omit_fields.append(field)
245
304
  if fields_to_assign_ids:
246
305
  setup_assign_ids(config, fields_to_assign_ids)
247
306
  if output_file:
@@ -263,38 +322,49 @@ def convert(
263
322
  # NOTE: NaN を None に変換しておかないと厄介
264
323
  df = df.replace([np.nan], [None])
265
324
  #ic(df)
266
- ic(len(df))
267
- ic(df.columns)
268
- ic(df.iloc[0])
269
- new_rows = []
270
- for index, row in df.iterrows():
271
- orig = OrderedDict(row)
272
- new_row = OrderedDict(row)
273
- if STAGING_FIELD not in new_row:
274
- set_field_value(new_row, f'{STAGING_FIELD}.{INPUT_FIELD}', orig)
275
- set_field_value(new_row, f'{STAGING_FIELD}.{FILE_FIELD}', input_file)
325
+ #ic(len(df))
326
+ #ic(df.columns)
327
+ #ic(df.iloc[0])
328
+ #new_rows = []
329
+ new_flat_rows = []
330
+ for index, flat_row in df.iterrows():
331
+ orig = OrderedDict(flat_row)
332
+ new_flat_row = OrderedDict(flat_row)
333
+ new_nested_row = nest(new_flat_row)
334
+ if STAGING_FIELD not in new_nested_row:
335
+ new_flat_row[f'{STAGING_FIELD}.{FILE_FIELD}'] = input_file
336
+ for key, value in orig.items():
337
+ new_flat_row[f'{STAGING_FIELD}.{INPUT_FIELD}.{key}'] = value
276
338
  if config.process.assign_constants:
277
- new_row = map_constants(new_row, config.process.assign_constants)
339
+ new_flat_row = map_constants(new_flat_row, config.process.assign_constants)
278
340
  if config.map:
279
- new_row = remap_columns(new_row, config.map)
341
+ new_flat_row = remap_columns(new_flat_row, config.map)
280
342
  if config.process.split_by_newline:
281
- new_row = apply_fields_split_by_newline(new_row, config.process.split_by_newline)
343
+ new_flat_row = apply_fields_split_by_newline(new_flat_row, config.process.split_by_newline)
282
344
  if config.process.assign_ids:
283
- new_row = assign_id(new_row, config.process.assign_ids, id_context_map)
345
+ new_flat_row = assign_id(new_flat_row, config.process.assign_ids, id_context_map)
284
346
  if config.process.assign_formats:
285
- new_row = map_formats(new_row, config.process.assign_formats)
347
+ new_flat_row = map_formats(new_flat_row, config.process.assign_formats)
286
348
  if config.map:
287
- new_row = remap_columns(new_row, config.map)
349
+ new_flat_row = remap_columns(new_flat_row, config.map)
350
+ if config.process.filter:
351
+ if not filter_row(new_flat_row, config.process.filter):
352
+ continue
353
+ if config.process.omit_fields:
354
+ for field in config.process.omit_fields:
355
+ new_flat_row.pop(field, None)
288
356
  if not output_debug:
289
- new_row.pop(STAGING_FIELD, None)
290
- new_rows.append(new_row)
291
- new_df = pd.DataFrame(new_rows)
357
+ for key in list(new_flat_row.keys()):
358
+ if key.startswith(STAGING_FIELD):
359
+ new_flat_row.pop(key)
360
+ new_flat_rows.append(new_flat_row)
361
+ new_df = pd.DataFrame(new_flat_rows)
292
362
  df_list.append(new_df)
293
363
  all_df = pd.concat(df_list)
294
364
  #ic(all_df)
295
365
  ic(len(all_df))
296
- ic(all_df.columns)
297
- ic(all_df.iloc[0])
366
+ #ic(all_df.columns)
367
+ #ic(all_df.iloc[0])
298
368
  if output_file:
299
369
  ic('Saing to: ', output_file)
300
370
  saver(all_df, output_file)
@@ -16,7 +16,11 @@ import pandas as pd
16
16
 
17
17
  # local
18
18
 
19
- from ..config import (
19
+ from .. constants import (
20
+ STAGING_FIELD,
21
+ )
22
+
23
+ from .. config import (
20
24
  AssignIdConfig,
21
25
  Config,
22
26
  )
@@ -87,7 +91,7 @@ def assign_id(
87
91
  id_map.dict_id_to_value[field_id] = primary_value
88
92
  else:
89
93
  field_id = id_map.dict_value_to_id[primary_value]
90
- set_field_value(new_row, f'__debug__.{column}', field_id)
94
+ new_row[f'{STAGING_FIELD}.{column}'] = field_id
91
95
  return new_row
92
96
 
93
97
  def setup_assign_ids(
@@ -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(
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(mapped, new_key, new_mapping)
22
+ else:
23
+ new_mapping[new_key] = mapped
24
+ return new_mapping
@@ -0,0 +1,25 @@
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
+ def search_column_value(
13
+ row: OrderedDict,
14
+ column: str,
15
+ ):
16
+ if f'{STAGING_FIELD}.{column}' in row:
17
+ value = row[f'{STAGING_FIELD}.{column}']
18
+ return value, True
19
+ if f'{STAGING_FIELD}.{INPUT_FIELD}.{column}' in row:
20
+ value = row[f'{STAGING_FIELD}.{INPUT_FIELD}.{column}']
21
+ return value, True
22
+ if column in row:
23
+ value = row[column]
24
+ return value, True
25
+ return None, False
@@ -3,6 +3,7 @@ Set the value of a field in a nested dictionary.
3
3
  '''
4
4
 
5
5
  from collections import OrderedDict
6
+ from icecream import ic
6
7
 
7
8
  def set_field_value(
8
9
  data: OrderedDict,
@@ -1,2 +0,0 @@
1
- __version__ = "0.2.8"
2
- __version_tuple__ = (0, 2, 8)
@@ -1,30 +0,0 @@
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 STAGING_FIELD
6
-
7
- from collections import OrderedDict
8
-
9
- from . get_field_value import get_field_value
10
- from . set_field_value import set_field_value
11
-
12
- def search_column_value(
13
- row: OrderedDict,
14
- column: str,
15
- ):
16
- if STAGING_FIELD in row:
17
- value, found = get_field_value(row[STAGING_FIELD], column)
18
- if found:
19
- return value, True
20
- value, found = get_field_value(row[STAGING_FIELD], column)
21
- original, found = get_field_value(row, f'{STAGING_FIELD}.__original__')
22
- if found:
23
- value, found = get_field_value(original, column)
24
- if found:
25
- return value, True
26
- value, found = get_field_value(row, column)
27
- if found:
28
- set_field_value(row, column, value)
29
- return value, True
30
- return None, False