python-table-converter 0.2.3__tar.gz → 0.2.5__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.3
3
+ Version: 0.2.5
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.3"
3
+ version = "0.2.5"
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.5"
2
+ __version_tuple__ = (0, 2, 5)
@@ -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
+ )
@@ -14,6 +14,13 @@ import pandas as pd
14
14
  # local
15
15
 
16
16
  from . config import setup_config
17
+ from . functions.assign_id import (
18
+ assign_id,
19
+ create_id_context_map,
20
+ setup_assign_ids,
21
+ )
22
+ from . functions.get_field_value import get_field_value
23
+ from . functions.set_field_value import set_field_value
17
24
 
18
25
  dict_loaders: dict[str, callable] = {}
19
26
  def register_loader(
@@ -85,31 +92,6 @@ def save_jsonl(
85
92
  )
86
93
  f.write('\n')
87
94
 
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
95
  def search_column_value(
114
96
  row: OrderedDict,
115
97
  column: str,
@@ -159,7 +141,7 @@ def map_formats(
159
141
  #ic(e.args)
160
142
  #ic(e.args[0])
161
143
  key = e.args[0]
162
- params[key] = '__undefined__'
144
+ params[key] = f'__{key}__undefined__'
163
145
  except:
164
146
  raise
165
147
  set_field_value(new_row, f'__debug__.{column}', formatted)
@@ -196,46 +178,6 @@ def apply_fields_split_by_newline(
196
178
  set_field_value(new_row, f'__debug__.{column}', value)
197
179
  return new_row
198
180
 
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)
237
- return new_row
238
-
239
181
  def convert(
240
182
  input_files: list[str],
241
183
  output_file: str | None = None,
@@ -251,9 +193,7 @@ def convert(
251
193
  ic()
252
194
  ic(input_files)
253
195
  df_list = []
254
- dict_formats: OrderedDict | None = None
255
- dict_assign_ids= None
256
- root_id_stat = create_id_stat_node()
196
+ id_context_map = create_id_context_map()
257
197
  config = setup_config(config_path)
258
198
  ic(config)
259
199
  if assign_constants:
@@ -265,12 +205,11 @@ def convert(
265
205
  else:
266
206
  raise ValueError(f'Invalid constant assignment: {field}')
267
207
  if assign_formats:
268
- dict_formats = OrderedDict()
269
208
  fields = assign_formats.split(',')
270
209
  for field in fields:
271
210
  if '=' in field:
272
211
  dst, src = field.split('=')
273
- dict_formats[dst] = src
212
+ config.process.assign_formats[dst] = src
274
213
  else:
275
214
  raise ValueError(f'Invalid template assignment: {field}')
276
215
  if pickup_columns:
@@ -290,20 +229,14 @@ def convert(
290
229
  else:
291
230
  raise ValueError(f'Invalid split by newline: {field}')
292
231
  if fields_to_assign_ids:
293
- dict_assign_ids = OrderedDict()
294
- fields = fields_to_assign_ids.split(',')
295
- for field in fields:
296
- if '=' in field:
297
- dst, src = field.split('=')
298
- dict_assign_ids[dst] = src
299
- else:
300
- raise ValueError(f'Invalid id assignment: {field}')
232
+ setup_assign_ids(config, fields_to_assign_ids)
301
233
  if output_file:
302
234
  ext = os.path.splitext(output_file)[1]
303
235
  if ext not in dict_savers:
304
236
  raise ValueError(f'Unsupported file type: {ext}')
305
237
  saver = dict_savers[ext]
306
238
  ic(config)
239
+ #return # debug return
307
240
  for input_file in input_files:
308
241
  ic(input_file)
309
242
  if not os.path.exists(input_file):
@@ -331,10 +264,10 @@ def convert(
331
264
  new_row = remap_columns(new_row, config.map)
332
265
  if config.process.split_by_newline:
333
266
  new_row = apply_fields_split_by_newline(new_row, config.process.split_by_newline)
334
- if dict_assign_ids:
335
- new_row = assign_id(new_row, dict_assign_ids, root_id_stat)
336
- if dict_formats:
337
- new_row = map_formats(new_row, dict_formats)
267
+ if config.process.assign_ids:
268
+ new_row = assign_id(new_row, config.process.assign_ids, id_context_map)
269
+ if config.process.assign_formats:
270
+ new_row = map_formats(new_row, config.process.assign_formats)
338
271
  if config.map:
339
272
  new_row = remap_columns(new_row, config.map)
340
273
  if not output_debug:
@@ -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,28 @@
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 collections import OrderedDict
6
+
7
+ from . get_field_value import get_field_value
8
+ from . set_field_value import set_field_value
9
+
10
+ def search_column_value(
11
+ row: OrderedDict,
12
+ column: str,
13
+ ):
14
+ if '__debug__' in row:
15
+ value, found = get_field_value(row['__debug__'], column)
16
+ if found:
17
+ return value, True
18
+ value, found = get_field_value(row['__debug__'], column)
19
+ original, found = get_field_value(row, '__debug__.__original__')
20
+ if found:
21
+ value, found = get_field_value(original, column)
22
+ if found:
23
+ return value, True
24
+ value, found = get_field_value(row, column)
25
+ if found:
26
+ set_field_value(row, column, value)
27
+ return value, True
28
+ 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.3"
2
- __version_tuple__ = (0, 2, 3)
@@ -1,70 +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
- split_by_newline: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
17
-
18
- def __setitem__(self, key, value):
19
- setattr(self, key, value)
20
-
21
- @dataclasses.dataclass
22
- class Config:
23
- map: FieldMap = dataclasses.field(default_factory=OrderedDict)
24
- process: ProcessConfig = dataclasses.field(default_factory=ProcessConfig)
25
-
26
- def flatten(
27
- mapping: FieldMap,
28
- parent_key: str = '',
29
- new_mapping: FlatFieldMap | None = None,
30
- ) -> FlatFieldMap:
31
- if new_mapping is None:
32
- new_mapping = OrderedDict()
33
- for key, mapped in mapping.items():
34
- new_key = f'{parent_key}.{key}' if parent_key else key
35
- if isinstance(mapped, Mapping):
36
- flatten(mapped, new_key, new_mapping)
37
- else:
38
- new_mapping[new_key] = mapped
39
- return new_mapping
40
-
41
- def setup_config(
42
- config_path: str | None = None,
43
- ):
44
- config = Config()
45
- if config_path:
46
- if config_path.endswith('.yaml'):
47
- yaml.add_constructor(
48
- yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
49
- lambda loader, node: OrderedDict(loader.construct_pairs(node)),
50
- )
51
- with open(config_path, 'r') as f:
52
- loaded = yaml.load(f, yaml.Loader)
53
- else:
54
- raise ValueError(
55
- 'Only YAML configuration files are supported.'
56
- )
57
- ic(loaded)
58
- if 'map' in loaded:
59
- config.map = flatten(loaded['map'])
60
- #if 'process' in loaded:
61
- dict_process = loaded.get('process')
62
- if isinstance(dict_process, Mapping):
63
- for process_key in [
64
- 'assign_constants',
65
- 'split_by_newline',
66
- ]:
67
- dict_subprocess = dict_process.get(process_key)
68
- if isinstance(dict_subprocess, Mapping):
69
- config.process[process_key] = flatten(loaded['process'][process_key])
70
- return config