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,365 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections import OrderedDict
4
+ import dataclasses
5
+ from typing import (
6
+ Any,
7
+ Literal,
8
+ Mapping,
9
+ )
10
+
11
+ from icecream import ic
12
+ import yaml
13
+
14
+ from . functions.flatten_row import (
15
+ FlatFieldMap,
16
+ flatten_row,
17
+ )
18
+
19
+ from . types import (
20
+ ActionConfig,
21
+ AssignFormatConfig,
22
+ AssignIdConfig,
23
+ AssignConstantConfig,
24
+ FilterConfig,
25
+ SplitConfig,
26
+ PickConfig,
27
+ )
28
+
29
+ @dataclasses.dataclass
30
+ class AssignArrayConfig:
31
+ field: str
32
+ optional: bool = True
33
+
34
+ @dataclasses.dataclass
35
+ class PushConfig:
36
+ target: str
37
+ source: str
38
+ condition: str | None = None
39
+
40
+ @dataclasses.dataclass
41
+ class ProcessConfig:
42
+ assign_array: Mapping[str, list[AssignArrayConfig]] = dataclasses.field(default_factory=OrderedDict)
43
+ assign_length: FlatFieldMap = dataclasses.field(default_factory=OrderedDict)
44
+ push: list[PushConfig] = dataclasses.field(default_factory=list)
45
+
46
+ def __setitem__(self, key, value):
47
+ setattr(self, key, value)
48
+
49
+ @dataclasses.dataclass
50
+ class Config:
51
+ actions: list[ActionConfig] = dataclasses.field(default_factory=list)
52
+
53
+ pick: list[PickConfig] = dataclasses.field(default_factory=list)
54
+ process: ProcessConfig = dataclasses.field(default_factory=ProcessConfig)
55
+
56
+ def setup_config(
57
+ config_path: str | None = None,
58
+ ):
59
+ config = Config()
60
+ if config_path:
61
+ if config_path.endswith('.yaml'):
62
+ yaml.add_constructor(
63
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
64
+ lambda loader, node: OrderedDict(loader.construct_pairs(node)),
65
+ )
66
+ with open(config_path, 'r') as f:
67
+ loaded = yaml.load(f, yaml.Loader)
68
+ else:
69
+ raise ValueError(
70
+ 'Only YAML configuration files are supported.'
71
+ )
72
+ ic(loaded)
73
+ if 'pick' in loaded:
74
+ if not isinstance(loaded['pick'], Mapping | list):
75
+ raise ValueError(
76
+ f'pick must be a dict or list, not {type(loaded["pick"])}'
77
+ )
78
+ if isinstance(loaded['pick'], Mapping):
79
+ for key, value in flatten_row(loaded['pick']).items():
80
+ config.pick.append(PickConfig(
81
+ target = key,
82
+ source = value,
83
+ ))
84
+ if isinstance(loaded['pick'], list):
85
+ for item in loaded['pick']:
86
+ if not isinstance(item, str):
87
+ raise ValueError(
88
+ 'Pickup list must contain strings.'
89
+ )
90
+ config.pick.append(PickConfig(
91
+ target = item,
92
+ source = item,
93
+ ))
94
+ setup_process_config(config, loaded)
95
+ return config
96
+
97
+ def setup_process_config(
98
+ config: Config,
99
+ loaded: Mapping,
100
+ ):
101
+ dict_process = loaded.get('process')
102
+ if isinstance(dict_process, Mapping):
103
+ for process_key in [
104
+ 'assign_length',
105
+ ]:
106
+ dict_subprocess = dict_process.get(process_key)
107
+ if isinstance(dict_subprocess, Mapping):
108
+ config.process[process_key] = flatten_row(loaded['process'][process_key])
109
+ for process_key in [
110
+ 'assign_constants',
111
+ 'assign_formats',
112
+ ]:
113
+ dict_subprocess = dict_process.get(process_key)
114
+ if isinstance(dict_subprocess, Mapping):
115
+ for key, value in dict_subprocess.items():
116
+ if process_key == 'assign_constants':
117
+ config.actions.append(AssignConstantConfig(
118
+ target = key,
119
+ value = value,
120
+ ))
121
+ if process_key == 'assign_formats':
122
+ config.actions.append(AssignFormatConfig(
123
+ target = key,
124
+ format = value,
125
+ ))
126
+ setup_process_assign_ids_config(config, dict_process)
127
+ setup_process_assign_array_config(config, dict_process)
128
+ setup_process_filter_config(config, dict_process)
129
+ setup_process_push_config(config, dict_process)
130
+ setup_process_split_config(config, dict_process)
131
+
132
+ def raise_error_for_unsupported_type(
133
+ value: Any,
134
+ should_be: str | None = None,
135
+ ):
136
+ ic.enable()
137
+ ic()
138
+ ic(value)
139
+ ic(type(value))
140
+ if should_be:
141
+ raise ValueError(
142
+ f'Unsupported value type: {type(value)}. Should be {should_be}.'
143
+ )
144
+ else:
145
+ raise ValueError(
146
+ f'Unsupported value type: {type(value)}'
147
+ )
148
+
149
+ def require_item(
150
+ mapping: Mapping,
151
+ key: str,
152
+ str_for: str
153
+ ):
154
+ if key not in mapping:
155
+ raise ValueError(
156
+ f'{key} is required for {str_for}.'
157
+ )
158
+ return mapping[key]
159
+
160
+ def setup_process_assign_ids_config(
161
+ config: Config,
162
+ dict_process: Mapping,
163
+ ):
164
+ dict_subprocess = dict_process.get('assign_ids')
165
+ str_for = 'assign_ids'
166
+ if isinstance(dict_subprocess, Mapping):
167
+ for key, value in dict_subprocess.items():
168
+ if isinstance(value, Mapping):
169
+ primary = require_item(value, 'primary', str_for)
170
+ context = value.get('context', None)
171
+ if isinstance(primary, str):
172
+ primary = [primary]
173
+ if isinstance(context, str):
174
+ context = [context]
175
+ config.actions.append(AssignIdConfig(
176
+ target = key,
177
+ primary = primary,
178
+ context = context,
179
+ ))
180
+ elif isinstance(value, list):
181
+ config.actions.append(AssignIdConfig(
182
+ target = key,
183
+ primary = value,
184
+ ))
185
+ elif isinstance(value, str):
186
+ config.actions.append(AssignIdConfig(
187
+ target = key,
188
+ primary = [value],
189
+ ))
190
+ else:
191
+ raise_error_for_unsupported_type(value, 'dict, list, or str')
192
+
193
+ def setup_process_assign_array_config(
194
+ config: Config,
195
+ dict_process: Mapping,
196
+ ):
197
+ dict_subprocess = dict_process.get('assign_array')
198
+ if dict_subprocess is None:
199
+ return
200
+ if not isinstance(dict_subprocess, Mapping):
201
+ raise ValueError(
202
+ 'Assign array must be a dictionary.'
203
+ )
204
+ for key, value in dict_subprocess.items():
205
+ if isinstance(value, list):
206
+ config.process.assign_array[key] = []
207
+ array = value
208
+ for item in array:
209
+ if isinstance(item, Mapping):
210
+ field = item.get('field')
211
+ optional = item.get('optional', False)
212
+ if field is None:
213
+ ic.enable()
214
+ ic(item)
215
+ ic(item.get('field'))
216
+ raise ValueError(
217
+ 'Field is required for assign_array.'
218
+ )
219
+ config.process.assign_array[key].append(AssignArrayConfig(
220
+ field = field,
221
+ optional = optional,
222
+ ))
223
+ elif isinstance(item, str):
224
+ config.process.assign_array[key].append(AssignArrayConfig(
225
+ field = item,
226
+ optional = True,
227
+ ))
228
+ else:
229
+ raise_error_for_unsupported_type(item, 'dict or str')
230
+ else:
231
+ raise_error_for_unsupported_type(value, 'list')
232
+
233
+ def setup_process_filter_config(
234
+ config: Config,
235
+ dict_process: Mapping,
236
+ ):
237
+ list_subprocess = dict_process.get('filter')
238
+ if list_subprocess is None:
239
+ return
240
+ if not isinstance(list_subprocess, list):
241
+ raise ValueError(
242
+ 'Filter must be a list.'
243
+ )
244
+ for item in list_subprocess:
245
+ if isinstance(item, Mapping):
246
+ field = item.get('field')
247
+ if not field:
248
+ ic.enable()
249
+ ic(item)
250
+ ic(item.get('field'))
251
+ raise ValueError(
252
+ 'Field is required for filter.'
253
+ )
254
+ operator = item.get('operator')
255
+ if not operator:
256
+ ic.enable()
257
+ ic(item)
258
+ ic(item.get('operator'))
259
+ raise ValueError(
260
+ 'Operator is required for filter.'
261
+ )
262
+ value = item.get('value')
263
+ if not value:
264
+ ic.enable()
265
+ ic(item)
266
+ ic(item.get('value'))
267
+ raise ValueError(
268
+ 'Value is required for filter.'
269
+ )
270
+ config.actions.append(FilterConfig(
271
+ field = field,
272
+ operator = operator,
273
+ value = value,
274
+ ))
275
+ else:
276
+ ic.enable()
277
+ ic(item)
278
+ ic(type(item))
279
+ raise ValueError(
280
+ f'Unsupported filter item type: {type(item)}'
281
+ )
282
+
283
+ def setup_process_push_config(
284
+ config: Config,
285
+ dict_process: Mapping,
286
+ ):
287
+ list_subprocess = dict_process.get('push')
288
+ if list_subprocess is None:
289
+ return
290
+ if not isinstance(list_subprocess, list):
291
+ raise ValueError(
292
+ 'push must be a list.'
293
+ )
294
+ str_for = 'push'
295
+ for item in list_subprocess:
296
+ if isinstance(item, Mapping):
297
+ target = require_item(item, 'target', str_for)
298
+ source = require_item(item, 'source', str_for)
299
+ condition = item.get('condition')
300
+ config.process.push.append(PushConfig(
301
+ target = target,
302
+ source = source,
303
+ condition = condition,
304
+ ))
305
+ else:
306
+ raise_error_for_unsupported_type(item, 'dict')
307
+
308
+ def setup_process_split_config(
309
+ config: Config,
310
+ dict_process: Mapping,
311
+ ):
312
+ dict_subprocess = dict_process.get('split')
313
+ if isinstance(dict_subprocess, Mapping):
314
+ for key, value in dict_subprocess.items():
315
+ if isinstance(value, Mapping):
316
+ field = value.get('field')
317
+ if not field:
318
+ ic.enable()
319
+ ic(value)
320
+ ic(value.get('field'))
321
+ raise ValueError(
322
+ 'Field is required for split.'
323
+ )
324
+ delimiter = value.get('delimiter')
325
+ if not delimiter:
326
+ ic.enable()
327
+ ic(value)
328
+ ic(value.get('delimiter'))
329
+ raise ValueError(
330
+ 'Delimiter is required for split.'
331
+ )
332
+ #config.process.split[key] = SplitConfig(
333
+ # field = field,
334
+ # delimiter = delimiter,
335
+ #)
336
+ config.actions.append(SplitConfig(
337
+ target = key,
338
+ source = field,
339
+ delimiter = delimiter,
340
+ ))
341
+ else:
342
+ ic.enable()
343
+ ic(value)
344
+ ic(type(value))
345
+ raise ValueError(
346
+ f'Unsupported assign_ids value type: {type(value)}'
347
+ )
348
+
349
+ def setup_pick_with_args(
350
+ config: Config,
351
+ list_fields: list[str],
352
+ ):
353
+ ic(list_fields)
354
+ for field in list_fields:
355
+ if '=' in field:
356
+ target, source = field.split('=')
357
+ config.pick.append(PickConfig(
358
+ target = target.strip(),
359
+ source = source.strip(),
360
+ ))
361
+ else:
362
+ config.pick.append(PickConfig(
363
+ target = field.strip(),
364
+ source = field.strip(),
365
+ ))
@@ -0,0 +1,5 @@
1
+ FILE_FIELD = '__file__'
2
+ ROW_INDEX_FIELD = '__row_index__'
3
+ FILE_ROW_INDEX_FIELD = '__file_row_index__'
4
+ INPUT_FIELD = '__input__'
5
+ STAGING_FIELD = '__staging__'