python-table-converter 0.2.4__tar.gz → 0.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-table-converter
3
- Version: 0.2.4
3
+ Version: 0.2.6
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.4"
3
+ version = "0.2.6"
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.6"
2
+ __version_tuple__ = (0, 2, 6)
@@ -0,0 +1,123 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections import OrderedDict
4
+ import dataclasses
5
+ from typing import Mapping
6
+
7
+ from icecream import ic
8
+ import yaml
9
+
10
+ type FlatFieldMap = Mapping[str, str]
11
+ type FieldMap = Mapping[str, str|FieldMap]
12
+
13
+ @dataclasses.dataclass
14
+ class AssignIdConfig:
15
+ primary: list[str]
16
+ #given: list[str] | None = None
17
+ context: list[str] | None = None
18
+
19
+ @dataclasses.dataclass
20
+ class ProcessConfig:
21
+ assign_constants: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
22
+ assign_formats: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
23
+ #assign_ids: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
24
+ assign_ids: Mapping[str, AssignIdConfig] = dataclasses.field(default_factory=OrderedDict)
25
+ split_by_newline: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
26
+
27
+ def __setitem__(self, key, value):
28
+ setattr(self, key, value)
29
+
30
+ @dataclasses.dataclass
31
+ class Config:
32
+ map: FieldMap = dataclasses.field(default_factory=OrderedDict)
33
+ process: ProcessConfig = dataclasses.field(default_factory=ProcessConfig)
34
+
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
+ def setup_config(
51
+ config_path: str | None = None,
52
+ ):
53
+ config = Config()
54
+ if config_path:
55
+ if config_path.endswith('.yaml'):
56
+ yaml.add_constructor(
57
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
58
+ lambda loader, node: OrderedDict(loader.construct_pairs(node)),
59
+ )
60
+ with open(config_path, 'r') as f:
61
+ loaded = yaml.load(f, yaml.Loader)
62
+ else:
63
+ raise ValueError(
64
+ 'Only YAML configuration files are supported.'
65
+ )
66
+ ic(loaded)
67
+ if 'map' in loaded:
68
+ config.map = flatten(loaded['map'])
69
+ setup_process_config(config, loaded)
70
+ return config
71
+
72
+ def setup_process_config(
73
+ config: Config,
74
+ loaded: Mapping,
75
+ ):
76
+ dict_process = loaded.get('process')
77
+ if isinstance(dict_process, Mapping):
78
+ for process_key in [
79
+ 'assign_constants',
80
+ 'assign_formats',
81
+ 'split_by_newline',
82
+ ]:
83
+ dict_subprocess = dict_process.get(process_key)
84
+ if isinstance(dict_subprocess, Mapping):
85
+ config.process[process_key] = flatten(loaded['process'][process_key])
86
+ setup_process_assign_ids_config(config, dict_process)
87
+
88
+ def setup_process_assign_ids_config(
89
+ config: Config,
90
+ dict_process: Mapping,
91
+ ):
92
+ dict_subprocess = dict_process.get('assign_ids')
93
+ if isinstance(dict_subprocess, Mapping):
94
+ for key, value in dict_subprocess.items():
95
+ if isinstance(value, Mapping):
96
+ primary = value.get('primary')
97
+ if not primary:
98
+ ic.enable()
99
+ ic(value)
100
+ ic(value.get('primary'))
101
+ raise ValueError(
102
+ 'Primary field is required for assign_ids.'
103
+ )
104
+ config.process.assign_ids[key] = AssignIdConfig(
105
+ primary = value.get('primary', []),
106
+ #given = value.get('given', None),
107
+ context = value.get('context', None),
108
+ )
109
+ elif isinstance(value, list):
110
+ config.process.assign_ids[key] = AssignIdConfig(
111
+ primary = value,
112
+ )
113
+ elif isinstance(value, str):
114
+ config.process.assign_ids[key] = AssignIdConfig(
115
+ primary = [value],
116
+ )
117
+ else:
118
+ ic.enable()
119
+ ic(value)
120
+ ic(type(value))
121
+ raise ValueError(
122
+ f'Unsupported assign_ids value type: {type(value)}'
123
+ )
@@ -0,0 +1,3 @@
1
+ FILE_FIELD = '__file__'
2
+ INPUT_FIELD = '__input__'
3
+ STAGING_FIELD = '__staging__'
@@ -14,6 +14,18 @@ import pandas as pd
14
14
  # local
15
15
 
16
16
  from . config import setup_config
17
+ from . constants import (
18
+ FILE_FIELD,
19
+ INPUT_FIELD,
20
+ STAGING_FIELD,
21
+ )
22
+ from . functions.assign_id import (
23
+ assign_id,
24
+ create_id_context_map,
25
+ setup_assign_ids,
26
+ )
27
+ from . functions.get_field_value import get_field_value
28
+ from . functions.set_field_value import set_field_value
17
29
 
18
30
  dict_loaders: dict[str, callable] = {}
19
31
  def register_loader(
@@ -85,41 +97,16 @@ def save_jsonl(
85
97
  )
86
98
  f.write('\n')
87
99
 
88
- def set_field_value(
89
- data: OrderedDict,
90
- field: str,
91
- value: any,
92
- ):
93
- if '.' in field:
94
- field, rest = field.split('.', 1)
95
- if field not in data:
96
- data[field] = OrderedDict()
97
- set_field_value(data[field], rest, value)
98
- else:
99
- data[field] = value
100
-
101
- def get_field_value(
102
- data: OrderedDict,
103
- field: str,
104
- ):
105
- if field in data:
106
- return data[field], True
107
- if '.' in field:
108
- field, rest = field.split('.', 1)
109
- if field in data:
110
- return get_field_value(data[field], rest)
111
- return None, False
112
-
113
100
  def search_column_value(
114
101
  row: OrderedDict,
115
102
  column: str,
116
103
  ):
117
- if '__debug__' in row:
118
- value, found = get_field_value(row['__debug__'], column)
104
+ if STAGING_FIELD in row:
105
+ value, found = get_field_value(row[STAGING_FIELD], column)
119
106
  if found:
120
107
  return value, True
121
- value, found = get_field_value(row['__debug__'], column)
122
- original, found = get_field_value(row, '__debug__.__original__')
108
+ value, found = get_field_value(row[STAGING_FIELD], column)
109
+ original, found = get_field_value(row, f'{STAGING_FIELD}.{INPUT_FIELD}')
123
110
  if found:
124
111
  value, found = get_field_value(original, column)
125
112
  if found:
@@ -137,7 +124,7 @@ def map_constants(
137
124
  new_row = OrderedDict(row)
138
125
  for column in dict_constants.keys():
139
126
  #set_field_value(new_row, column, dict_constants[column])
140
- set_field_value(new_row, f'__debug__.{column}', dict_constants[column])
127
+ set_field_value(new_row, f'{STAGING_FIELD}.{column}', dict_constants[column])
141
128
  return new_row
142
129
 
143
130
  def map_formats(
@@ -148,7 +135,7 @@ def map_formats(
148
135
  for column in dict_formats.keys():
149
136
  template = dict_formats[column]
150
137
  params = {}
151
- params.update(row['__debug__'])
138
+ params.update(row[STAGING_FIELD])
152
139
  params.update(row)
153
140
  formatted = None
154
141
  while formatted is None:
@@ -159,10 +146,10 @@ def map_formats(
159
146
  #ic(e.args)
160
147
  #ic(e.args[0])
161
148
  key = e.args[0]
162
- params[key] = '__undefined__'
149
+ params[key] = f'__{key}__undefined__'
163
150
  except:
164
151
  raise
165
- set_field_value(new_row, f'__debug__.{column}', formatted)
152
+ set_field_value(new_row, f'{STAGING_FIELD}.{column}', formatted)
166
153
  return new_row
167
154
 
168
155
  def remap_columns(
@@ -175,7 +162,7 @@ def remap_columns(
175
162
  if found:
176
163
  set_field_value(new_row, column, value)
177
164
  for column in row.keys():
178
- if column == '__debug__':
165
+ if column == STAGING_FIELD:
179
166
  # NOTE: Ignore debug fields
180
167
  set_field_value(new_row, column, row[column])
181
168
  return new_row
@@ -191,49 +178,9 @@ def apply_fields_split_by_newline(
191
178
  if found:
192
179
  if isinstance(value, str):
193
180
  new_value = value.split('\n')
194
- set_field_value(new_row, f'__debug__.{column}', new_value)
181
+ set_field_value(new_row, f'{STAGING_FIELD}.{column}', new_value)
195
182
  else:
196
- set_field_value(new_row, f'__debug__.{column}', value)
197
- return new_row
198
-
199
- def create_id_stat_node():
200
- return {
201
- 'max_id': 0,
202
- 'dict_value_to_id': {},
203
- 'dict_id_to_node': {},
204
- }
205
-
206
- def assign_id_in_node(
207
- row: OrderedDict,
208
- column: str,
209
- dict_assignment: OrderedDict,
210
- id_stat_node: dict,
211
- ):
212
- value, found = search_column_value(row, dict_assignment[column])
213
- if not found:
214
- raise KeyError(f'Column not found: {column}, existing columns: {row.keys()}')
215
- if value not in id_stat_node['dict_value_to_id']:
216
- field_id = id_stat_node['max_id'] + 1
217
- id_stat_node['max_id'] = field_id
218
- id_stat_node['dict_value_to_id'][value] = field_id
219
- node = create_id_stat_node()
220
- id_stat_node['dict_id_to_node'][field_id] = node
221
- else:
222
- field_id = id_stat_node['dict_value_to_id'][value]
223
- node = id_stat_node['dict_id_to_node'][field_id]
224
- set_field_value(row, f'__debug__.{column}', field_id)
225
- set_field_value(row, f'__debug__.__ids__.{column}', field_id)
226
- return node
227
-
228
- def assign_id(
229
- row: OrderedDict,
230
- dict_assignment: OrderedDict,
231
- root_id_stat_node: dict,
232
- ):
233
- new_row = OrderedDict(row)
234
- node = root_id_stat_node
235
- for column in dict_assignment:
236
- node = assign_id_in_node(new_row, column, dict_assignment, node)
183
+ set_field_value(new_row, f'{STAGING_FIELD}.{column}', value)
237
184
  return new_row
238
185
 
239
186
  def convert(
@@ -251,8 +198,7 @@ def convert(
251
198
  ic()
252
199
  ic(input_files)
253
200
  df_list = []
254
- dict_assign_ids= None
255
- root_id_stat = create_id_stat_node()
201
+ id_context_map = create_id_context_map()
256
202
  config = setup_config(config_path)
257
203
  ic(config)
258
204
  if assign_constants:
@@ -288,14 +234,7 @@ def convert(
288
234
  else:
289
235
  raise ValueError(f'Invalid split by newline: {field}')
290
236
  if fields_to_assign_ids:
291
- dict_assign_ids = OrderedDict()
292
- fields = fields_to_assign_ids.split(',')
293
- for field in fields:
294
- if '=' in field:
295
- dst, src = field.split('=')
296
- dict_assign_ids[dst] = src
297
- else:
298
- raise ValueError(f'Invalid id assignment: {field}')
237
+ setup_assign_ids(config, fields_to_assign_ids)
299
238
  if output_file:
300
239
  ext = os.path.splitext(output_file)[1]
301
240
  if ext not in dict_savers:
@@ -322,22 +261,22 @@ def convert(
322
261
  for index, row in df.iterrows():
323
262
  orig = OrderedDict(row)
324
263
  new_row = OrderedDict(row)
325
- set_field_value(new_row, '__debug__.__original__', orig)
326
- set_field_value(new_row, '__debug__.__file__', input_file)
264
+ set_field_value(new_row, f'{STAGING_FIELD}.{INPUT_FIELD}', orig)
265
+ set_field_value(new_row, f'{STAGING_FIELD}.{FILE_FIELD}', input_file)
327
266
  if config.process.assign_constants:
328
267
  new_row = map_constants(new_row, config.process.assign_constants)
329
268
  if config.map:
330
269
  new_row = remap_columns(new_row, config.map)
331
270
  if config.process.split_by_newline:
332
271
  new_row = apply_fields_split_by_newline(new_row, config.process.split_by_newline)
333
- if dict_assign_ids:
334
- new_row = assign_id(new_row, dict_assign_ids, root_id_stat)
272
+ if config.process.assign_ids:
273
+ new_row = assign_id(new_row, config.process.assign_ids, id_context_map)
335
274
  if config.process.assign_formats:
336
275
  new_row = map_formats(new_row, config.process.assign_formats)
337
276
  if config.map:
338
277
  new_row = remap_columns(new_row, config.map)
339
278
  if not output_debug:
340
- new_row.pop('__debug__', None)
279
+ new_row.pop(STAGING_FIELD, None)
341
280
  new_rows.append(new_row)
342
281
  new_df = pd.DataFrame(new_rows)
343
282
  df_list.append(new_df)
@@ -0,0 +1,108 @@
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 ..config import (
20
+ AssignIdConfig,
21
+ Config,
22
+ )
23
+
24
+ from . search_column_value import search_column_value
25
+ from . set_field_value import set_field_value
26
+
27
+ type ContextColumnTuple = tuple[str]
28
+ type ContextValueTuple = tuple
29
+ type PrimaryColumnTuple = tuple[str]
30
+ type PrimaryValueTuple = tuple
31
+
32
+ @dataclasses.dataclass
33
+ class IdMap:
34
+ max_id: int = 0
35
+ dict_value_to_id: Mapping[PrimaryValueTuple, int] = \
36
+ dataclasses.field(default_factory=defaultdict)
37
+ dict_id_to_value: Mapping[int, PrimaryValueTuple] = \
38
+ dataclasses.field(default_factory=defaultdict)
39
+
40
+ type IdContextMap = Mapping[
41
+ (
42
+ ContextColumnTuple,
43
+ ContextValueTuple,
44
+ PrimaryColumnTuple,
45
+ ),
46
+ IdMap
47
+ ]
48
+
49
+ def create_id_context_map() -> IdContextMap:
50
+ return defaultdict(IdMap)
51
+
52
+ def assign_id(
53
+ row: OrderedDict,
54
+ dict_assignment: Mapping[str, AssignIdConfig],
55
+ id_context_map: IdContextMap,
56
+ ):
57
+ new_row = OrderedDict(row)
58
+ for column, config in dict_assignment.items():
59
+ context_columns = []
60
+ context_values = []
61
+ if config.context:
62
+ for context_column in config.context:
63
+ value, found = search_column_value(new_row, context_column)
64
+ if not found:
65
+ raise KeyError(f'Column not found: {context_column}, existing columns: {new_row.keys()}')
66
+ context_columns.append(context_column)
67
+ context_values.append(value)
68
+ primary_columns = []
69
+ primary_values = []
70
+ for primary_column in config.primary:
71
+ value, found = search_column_value(new_row, primary_column)
72
+ if not found:
73
+ raise KeyError(f'Column not found: {primary_column}, existing columns: {new_row.keys()}')
74
+ primary_columns.append(primary_column)
75
+ primary_values.append(value)
76
+ context_key = (
77
+ tuple(context_columns),
78
+ tuple(context_values),
79
+ tuple(primary_columns),
80
+ )
81
+ primary_value = tuple(primary_values)
82
+ id_map = id_context_map[context_key]
83
+ if primary_value not in id_map.dict_value_to_id:
84
+ field_id = id_map.max_id + 1
85
+ id_map.max_id = field_id
86
+ id_map.dict_value_to_id[primary_value] = field_id
87
+ id_map.dict_id_to_value[field_id] = primary_value
88
+ else:
89
+ field_id = id_map.dict_value_to_id[primary_value]
90
+ set_field_value(new_row, f'__debug__.{column}', field_id)
91
+ return new_row
92
+
93
+ def setup_assign_ids(
94
+ config: Config,
95
+ fields_to_assign_ids: str,
96
+ ):
97
+ if fields_to_assign_ids:
98
+ fields = fields_to_assign_ids.split(',')
99
+ context = []
100
+ for field in fields:
101
+ if '=' in field:
102
+ dst, src = field.split('=')
103
+ config.process.assign_ids[dst] = AssignIdConfig(
104
+ primary = [src],
105
+ context = context,
106
+ )
107
+ else:
108
+ raise ValueError(f'Invalid id assignment: {field}')
@@ -0,0 +1,15 @@
1
+ # Description: Get the value of a field in a dictionary.
2
+
3
+ from collections import OrderedDict
4
+
5
+ def get_field_value(
6
+ data: OrderedDict,
7
+ field: str,
8
+ ):
9
+ if field in data:
10
+ return data[field], True
11
+ if '.' in field:
12
+ field, rest = field.split('.', 1)
13
+ if field in data:
14
+ return get_field_value(data[field], rest)
15
+ return None, False
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,18 @@
1
+ '''
2
+ Set the value of a field in a nested dictionary.
3
+ '''
4
+
5
+ from collections import OrderedDict
6
+
7
+ def set_field_value(
8
+ data: OrderedDict,
9
+ field: str,
10
+ value: any,
11
+ ):
12
+ if '.' in field:
13
+ field, rest = field.split('.', 1)
14
+ if field not in data:
15
+ data[field] = OrderedDict()
16
+ set_field_value(data[field], rest, value)
17
+ else:
18
+ data[field] = value
@@ -1,2 +0,0 @@
1
- __version__ = "0.2.4"
2
- __version_tuple__ = (0, 2, 4)
@@ -1,71 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- from collections import OrderedDict
4
- import dataclasses
5
- from typing import Mapping
6
-
7
- from icecream import ic
8
- import yaml
9
-
10
- type FlatFieldMap = Mapping[str, str]
11
- type FieldMap = Mapping[str, str|FieldMap]
12
-
13
- @dataclasses.dataclass
14
- class ProcessConfig:
15
- assign_constants: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
16
- assign_formats: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
17
- split_by_newline: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
18
-
19
- def __setitem__(self, key, value):
20
- setattr(self, key, value)
21
-
22
- @dataclasses.dataclass
23
- class Config:
24
- map: FieldMap = dataclasses.field(default_factory=OrderedDict)
25
- process: ProcessConfig = dataclasses.field(default_factory=ProcessConfig)
26
-
27
- def flatten(
28
- mapping: FieldMap,
29
- parent_key: str = '',
30
- new_mapping: FlatFieldMap | None = None,
31
- ) -> FlatFieldMap:
32
- if new_mapping is None:
33
- new_mapping = OrderedDict()
34
- for key, mapped in mapping.items():
35
- new_key = f'{parent_key}.{key}' if parent_key else key
36
- if isinstance(mapped, Mapping):
37
- flatten(mapped, new_key, new_mapping)
38
- else:
39
- new_mapping[new_key] = mapped
40
- return new_mapping
41
-
42
- def setup_config(
43
- config_path: str | None = None,
44
- ):
45
- config = Config()
46
- if config_path:
47
- if config_path.endswith('.yaml'):
48
- yaml.add_constructor(
49
- yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
50
- lambda loader, node: OrderedDict(loader.construct_pairs(node)),
51
- )
52
- with open(config_path, 'r') as f:
53
- loaded = yaml.load(f, yaml.Loader)
54
- else:
55
- raise ValueError(
56
- 'Only YAML configuration files are supported.'
57
- )
58
- ic(loaded)
59
- if 'map' in loaded:
60
- config.map = flatten(loaded['map'])
61
- dict_process = loaded.get('process')
62
- if isinstance(dict_process, Mapping):
63
- for process_key in [
64
- 'assign_constants',
65
- 'assign_formats',
66
- 'split_by_newline',
67
- ]:
68
- dict_subprocess = dict_process.get(process_key)
69
- if isinstance(dict_subprocess, Mapping):
70
- config.process[process_key] = flatten(loaded['process'][process_key])
71
- return config