SankeyExcelParser 1.0.0b0__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.
Files changed (32) hide show
  1. SankeyExcelParser/__init__.py +0 -0
  2. SankeyExcelParser/io_excel.py +1867 -0
  3. SankeyExcelParser/io_excel_constants.py +811 -0
  4. SankeyExcelParser/sankey.py +3138 -0
  5. SankeyExcelParser/sankey_utils/__init__.py +0 -0
  6. SankeyExcelParser/sankey_utils/data.py +1118 -0
  7. SankeyExcelParser/sankey_utils/excel_source.py +31 -0
  8. SankeyExcelParser/sankey_utils/flux.py +344 -0
  9. SankeyExcelParser/sankey_utils/functions.py +278 -0
  10. SankeyExcelParser/sankey_utils/node.py +340 -0
  11. SankeyExcelParser/sankey_utils/protos/__init__.py +0 -0
  12. SankeyExcelParser/sankey_utils/protos/flux.py +84 -0
  13. SankeyExcelParser/sankey_utils/protos/node.py +386 -0
  14. SankeyExcelParser/sankey_utils/protos/sankey_object.py +135 -0
  15. SankeyExcelParser/sankey_utils/protos/tag_group.py +95 -0
  16. SankeyExcelParser/sankey_utils/sankey_object.py +165 -0
  17. SankeyExcelParser/sankey_utils/table_object.py +37 -0
  18. SankeyExcelParser/sankey_utils/tag.py +95 -0
  19. SankeyExcelParser/sankey_utils/tag_group.py +206 -0
  20. SankeyExcelParser/su_trace.py +239 -0
  21. SankeyExcelParser/tests/integration/__init__.py +0 -0
  22. SankeyExcelParser/tests/integration/test_base.py +356 -0
  23. SankeyExcelParser/tests/integration/test_run_check_input.py +100 -0
  24. SankeyExcelParser/tests/integration/test_run_conversions.py +96 -0
  25. SankeyExcelParser/tests/integration/test_run_load_input.py +94 -0
  26. SankeyExcelParser/tests/unit/__init__.py +0 -0
  27. SankeyExcelParser-1.0.0b0.data/scripts/run_parse_and_write_excel.py +155 -0
  28. SankeyExcelParser-1.0.0b0.data/scripts/run_parse_excel.py +115 -0
  29. SankeyExcelParser-1.0.0b0.dist-info/METADATA +113 -0
  30. SankeyExcelParser-1.0.0b0.dist-info/RECORD +32 -0
  31. SankeyExcelParser-1.0.0b0.dist-info/WHEEL +5 -0
  32. SankeyExcelParser-1.0.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,31 @@
1
+ """
2
+ Author : Vincent LE DOZE
3
+ Date : 31/05/23
4
+
5
+ This file contains description for ExcelSource class
6
+
7
+ """
8
+
9
+ # External libs ----------------------------------------------------------------
10
+ import pandas as pd
11
+
12
+
13
+ # CLASS ----------------------------------------------------------------------------
14
+ class ExcelSource(object):
15
+ user_sheet_name: str
16
+ sheet_type: str
17
+ _table: pd.DataFrame
18
+
19
+ def __init__(
20
+ self,
21
+ user_sheet_name: str,
22
+ sheet_type: str,
23
+ ):
24
+ self.user_sheet_name = user_sheet_name
25
+ self.sheet_type = sheet_type
26
+
27
+ def save_origin_table(
28
+ self,
29
+ table: pd.DataFrame
30
+ ):
31
+ self.table = table
@@ -0,0 +1,344 @@
1
+ """
2
+ Author : Vincent LE DOZE
3
+ Date : 31/05/23
4
+
5
+ This file contains descriptions for Flux class
6
+
7
+ """
8
+
9
+ # Local modules -----------------------------------------------------
10
+ from SankeyExcelParser.sankey_utils.protos.flux import _ProtoFlux
11
+ from SankeyExcelParser.sankey_utils.data import Data
12
+ from SankeyExcelParser.sankey_utils.data import DataConstraint
13
+ from SankeyExcelParser.sankey_utils.data import DataMinMax
14
+ from SankeyExcelParser.sankey_utils.data import MCData
15
+ from SankeyExcelParser.sankey_utils.node import Node
16
+
17
+
18
+ # CLASS ----------------------------------------------------------------------------
19
+ class Flux(_ProtoFlux):
20
+ """
21
+ Define a flux.
22
+ Inherits from `_ProtoFlux`
23
+
24
+ Parameters
25
+ ----------
26
+ :param orig: Flux starting node
27
+ :type orig: Node
28
+
29
+ :param dest: Flux ending node
30
+ :type dest: Node
31
+
32
+ :param datas: All datas for the flux
33
+ :type datas: list [Data, ...]
34
+
35
+ :param results: All results for the flux
36
+ :type results: list [Data, ...]
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ orig: Node,
42
+ dest: Node,
43
+ **kwargs
44
+ ):
45
+ # Init super constructor
46
+ _ProtoFlux.__init__(self, orig, dest)
47
+ self._orig.add_output_flux(self)
48
+ self._dest.add_input_flux(self)
49
+ # Datas
50
+ self._datatags_combinations = []
51
+ self._datas = []
52
+ self._results = []
53
+ self._monte_carlo = None
54
+ # Init contraints values for all datas
55
+ self._min_max = DataMinMax(self)
56
+ self._max = None
57
+ self._constraints = {}
58
+ # Update values
59
+ self.update(**kwargs)
60
+
61
+ def update(
62
+ self,
63
+ **kwargs
64
+ ):
65
+ for key, value in kwargs.items():
66
+ setattr(self, key, value)
67
+
68
+ @property
69
+ def datas(self):
70
+ return self._datas
71
+
72
+ def add_data(self, _):
73
+ if type(_) is float:
74
+ data = Data(value=_)
75
+ self._datas.append(data)
76
+ data.flux = self
77
+ return
78
+ if type(_) is Data:
79
+ self._datas.append(_)
80
+ _.flux = self
81
+ return
82
+
83
+ def has_data(self):
84
+ return len(self._datas) > 0
85
+
86
+ def instanciate_all_datas(
87
+ self,
88
+ data_taggs={}
89
+ ):
90
+ """
91
+ Create data according to data tags presents in a table line.
92
+ Data can only have one tag for every given data tag group.
93
+ If multiple tag are given, then we must create one data for each one and link them
94
+ to the reference flux.
95
+
96
+ Parameters
97
+ ----------
98
+ :param data_taggs: List of data tags
99
+ :type data_taggs: dict as {key=TagGroup.name: value=TagGroup, ...} . (default={})
100
+
101
+ Returns
102
+ -------
103
+ :return: List of created datas
104
+ :rtype: list as [Data, ...]
105
+ """
106
+ # If no data tag -> Create only one data
107
+ if len(data_taggs) == 0:
108
+ # Add only one data
109
+ self.add_data(Data())
110
+ # Add empty datatags combinations
111
+ self._datatags_combinations.append([])
112
+ return
113
+ # Otherwise create data recursivly
114
+ self._recursive_instanciate_all_datas(
115
+ [list(_.tags.values()) for _ in data_taggs.values()])
116
+
117
+ def _recursive_instanciate_all_datas(
118
+ self,
119
+ data_tags_per_tagg,
120
+ data_tags_per_datas=[[]]
121
+ ):
122
+ """
123
+ Create data recursivly according to data tags presents in a table line.
124
+ Data can only have one tag for every given data tag group.
125
+ If multiple tag are given, then we must create one data for each one and link them
126
+ to the reference flux.
127
+
128
+ Parameters
129
+ ----------
130
+ :param data_tags_per_tagg: List of data tags regrouped per data tag groups.
131
+ :type data_tags_per_tagg: list as [list as [Tag, ...], ...]
132
+
133
+ :param data_tags_per_datas: Must not be touch. Used for recursivity
134
+ :type data_tags_per_datas: list (default=[[]])
135
+
136
+ Returns
137
+ -------
138
+ :return: List of created datas
139
+ :rtype: list as [Data, ...]
140
+ """
141
+ # Check if we arrived at the end of recursivity on data tags
142
+ if len(data_tags_per_tagg) == 0:
143
+ # Create unique data for each data tag per data tag groups
144
+ for data_tags in data_tags_per_datas:
145
+ # Create Data
146
+ data = Data()
147
+ # Add tags
148
+ for data_tag in data_tags:
149
+ data.add_tag(data_tag)
150
+ # Add data to flux
151
+ self.add_data(data)
152
+ # Save data_tags_per_datas as keys
153
+ self._datatags_combinations.append(data_tags.copy())
154
+ # Otherwise we continue to recurse
155
+ else:
156
+ # Get data tags related to datas to create and to a unique data tag group
157
+ data_tags = data_tags_per_tagg.pop()
158
+ # Create the list of unique data tags for each data
159
+ all_data_tags_per_datas = []
160
+ for data_tag in data_tags:
161
+ # data_tags_per_datas = [[tag1_1, tag2_1], [tag1_1, tag2_2], [tag1_2, tag2_1], ...]
162
+ # with data tag group 1 = (tag1_1, tag1_2)
163
+ # data tag group 2 = (tag2_1, tag2_2)
164
+ # So, if we have data tag group 3 = (tag3_1, tag3_2, ...)
165
+ # we must copy each list from data_tags_per_datas and append tag3_1
166
+ # then make anothers copies to append tag3_2, etc.
167
+ new_data_tags_per_datas = []
168
+ for data_tags_per_data in data_tags_per_datas:
169
+ new_data_tags_per_data = data_tags_per_data.copy()
170
+ new_data_tags_per_data.append(data_tag)
171
+ new_data_tags_per_datas.append(new_data_tags_per_data)
172
+ all_data_tags_per_datas += new_data_tags_per_datas
173
+ # We recurse to deal with data tag groups
174
+ self._recursive_instanciate_all_datas(
175
+ data_tags_per_tagg,
176
+ data_tags_per_datas=all_data_tags_per_datas)
177
+
178
+ def get_corresponding_datas_from_tags(
179
+ self,
180
+ datatags_to_match,
181
+ fluxtags_to_match=[]
182
+ ):
183
+ """
184
+ Get a list of data that correspond to the input list of tags
185
+
186
+ Parameters
187
+ ----------
188
+ :param tags: list of tags to check
189
+ :type tags: list[Tag, ...]
190
+
191
+ Returns
192
+ -------
193
+ :return: List of corresponding datas
194
+ :rtype: list[Data, ...]
195
+ """
196
+ # Init list of matched datas
197
+ matched_datas = []
198
+ # Match // datatags
199
+ for datatags, data in zip(self._datatags_combinations, self._datas):
200
+ ok_match_datatags = False
201
+ # If all current datatags are contained in datatags to match list
202
+ if set(datatags_to_match).issuperset(set(datatags)):
203
+ ok_match_datatags = True
204
+ # If all datatags to match are contained in current datatags list
205
+ if set(datatags).issuperset(set(datatags_to_match)):
206
+ ok_match_datatags = True
207
+ # Check flux tags also
208
+ if ok_match_datatags:
209
+ # If flux tags are related to curren data
210
+ if set(data.tags).issuperset(set(fluxtags_to_match)):
211
+ matched_datas.append(data)
212
+ # Output
213
+ return matched_datas
214
+
215
+ @property
216
+ def results(self):
217
+ return self._results
218
+
219
+ def add_result(self, _):
220
+ if type(_) is float:
221
+ result = Data(value=_)
222
+ self._results.append(result)
223
+ result.flux = self
224
+ return
225
+ if type(_) is Data:
226
+ self._results.append(_)
227
+ _.flux = self
228
+ return
229
+
230
+ def has_result(self):
231
+ return len(self._results) > 0
232
+
233
+ def reset_results(self):
234
+ # remove altergo links with datas
235
+ for result in self._results:
236
+ result.alterego = None
237
+ # Empty list
238
+ self._results = []
239
+
240
+ def get_corresponding_results_from_tags(self, tags):
241
+ """
242
+ Get a list of data that correspond to the input list of tags
243
+
244
+ Parameters
245
+ ----------
246
+ :param tag: tag
247
+ :type tag: str | Tag
248
+
249
+ Returns
250
+ -------
251
+ :return: Corresponding data if it exist, else None
252
+ :rtype: Data | None
253
+ """
254
+ results = set(self._results)
255
+ for tag in tags:
256
+ results &= set(tag.references)
257
+ return list(results)
258
+
259
+ @property
260
+ def monte_carlo(self):
261
+ return self._monte_carlo
262
+
263
+ def add_monte_carlo(
264
+ self,
265
+ starting_mean_value,
266
+ starting_sigma,
267
+ result_mean_value,
268
+ result_sigma,
269
+ result_min,
270
+ result_max
271
+ ):
272
+ self._monte_carlo = MCData(flux=self)
273
+ self._monte_carlo.starting_data = Data(
274
+ value=starting_mean_value,
275
+ sigma=starting_sigma)
276
+ self._monte_carlo.result_data = Data(
277
+ value=result_mean_value,
278
+ sigma=result_sigma)
279
+ self._monte_carlo.min = result_min
280
+ self._monte_carlo.max = result_max
281
+
282
+ @property
283
+ def min_max(self):
284
+ return self._min_max
285
+
286
+ @property
287
+ def min(self):
288
+ return self._min_max.min_val
289
+
290
+ @min.setter
291
+ def min(self, _):
292
+ self._min_max.min = _
293
+
294
+ @property
295
+ def max(self):
296
+ return self._min_max.max_val
297
+
298
+ @max.setter
299
+ def max(self, _):
300
+ self._min_max.max = _
301
+
302
+ @property
303
+ def constraints(self):
304
+ return self._constraints
305
+
306
+ def add_constraint(self, id_constraint, **kwargs):
307
+ # Create a new piece of constraint
308
+ constraint = DataConstraint(id_constraint, self, **kwargs)
309
+ # Update constraint for given id
310
+ if id_constraint in self._constraints.keys():
311
+ self._constraints[id_constraint].append(constraint)
312
+ else:
313
+ self._constraints[id_constraint] = [constraint]
314
+ # Return constraint
315
+ return constraint
316
+
317
+ def get_as_dict(self):
318
+ # Init output
319
+ output = {}
320
+ # Get values
321
+ output['orig'] = self.orig.name
322
+ output['dest'] = self.dest.name
323
+ output['datas'] = []
324
+ for data in self.datas:
325
+ output['datas'].append(data.get_as_dict())
326
+ return output
327
+
328
+ def __repr__(self):
329
+ if self.has_data():
330
+ if len(self.datas) > 1:
331
+ return '{0} --- [{2}, ...] ---> {1}'.format(
332
+ self._orig.name,
333
+ self._dest.name,
334
+ self.datas[0])
335
+ else:
336
+ return '{0} --- {2} ---> {1}'.format(
337
+ self._orig.name,
338
+ self._dest.name,
339
+ self.datas[0])
340
+ else:
341
+ return '{0} --- {2} ---> {1}'.format(
342
+ self._orig.name,
343
+ self._dest.name,
344
+ 'No data')
@@ -0,0 +1,278 @@
1
+ """
2
+ Author : Vincent LE DOZE
3
+ Date : 31/05/23
4
+
5
+ This file contains functions used in sankey_utils modules
6
+
7
+ """
8
+
9
+ # External libs ----------------------------------------------------------------
10
+ import pandas as pd
11
+ import re
12
+ import webcolors
13
+
14
+ # External modules ------------------------------------------------------------
15
+ from unidecode import unidecode
16
+
17
+
18
+ # FUNCTIONS -------------------------------------------------------------------
19
+ def _stdStr(s):
20
+ """
21
+ Returns standardize format for input string.
22
+
23
+ Parameters
24
+ ----------
25
+ :param s: input.
26
+ :type s: str | set | list
27
+
28
+ Returns
29
+ -------
30
+ :return: formatted string(s).
31
+ :rtype: same type as input
32
+ """
33
+ if type(s) is str:
34
+ return unidecode(s).strip().lower().replace('.0', '')
35
+ if type(s) is set:
36
+ new_s = set()
37
+ for _ in s:
38
+ new_s.add(_stdStr(_))
39
+ return s
40
+ if type(s) is list:
41
+ return [_stdStr(_) for _ in s]
42
+
43
+
44
+ def _getValueIfPresent(
45
+ line: pd.Series,
46
+ index: int,
47
+ default_value
48
+ ):
49
+ """
50
+ Extract the value from a table line for given column name.
51
+ If nothing is found, returns a default value.
52
+
53
+ Parameters
54
+ ----------
55
+ :param line: Line where the info should be find.
56
+ :type line: pd.Series
57
+
58
+ :param index: Column name under which we should find a value
59
+ :type index: int
60
+
61
+ :param default_value: If no value is found under the given column, this is the default output.
62
+
63
+ Returns
64
+ -------
65
+ :return: Found value or default value if nothing is found.
66
+ """
67
+ value = line[index] if index in line.index else default_value
68
+ return value
69
+
70
+
71
+ def _extractFluxFromMatrix(
72
+ table: pd.DataFrame,
73
+ origins: list,
74
+ destinations: list,
75
+ values: list
76
+ ):
77
+ """
78
+ Extract flux from a matrix as following:
79
+
80
+ +------+------+------+------+
81
+ | - | C4 | C5 | C6 |
82
+ +======+======+======+======+
83
+ | R3 | None | x | x |
84
+ +------+------+------+------+
85
+ | R4 | x | None | None |
86
+ +------+------+------+------+
87
+
88
+ Parameters
89
+ ----------
90
+ :param table: Matrix table (rows = Origins, cols = Destinations)
91
+ :type table: pd.DataFrame
92
+
93
+ :param origins: List of all origins
94
+ :type origins: list, modified
95
+
96
+ :param destinations: List of all destinations
97
+ :type destinations: list, modified
98
+
99
+ :param values: List of all values for flux
100
+ :type values: list, modified
101
+
102
+ """
103
+ for orig in table.index:
104
+ for dest in table.columns:
105
+ if orig == dest:
106
+ continue
107
+ v = table.loc[orig][dest]
108
+ ok = (v is not None) and (v != 0) and (v != "0")
109
+ if ok:
110
+ origins.append(orig)
111
+ destinations.append(dest)
112
+ if re.fullmatch('[xX]', str(v)) is not None:
113
+ values.append(None)
114
+ else:
115
+ values.append(float(v))
116
+
117
+
118
+ def _createMatrixFromFlux(
119
+ origins: list,
120
+ destinations: list,
121
+ transpose=False
122
+ ):
123
+ """
124
+ Create a matrix table (rows = Origins, cols = Destinations)
125
+
126
+ Parameters
127
+ ----------
128
+ :param origins: List of all origin nodes
129
+ :type origins: list[Node]
130
+
131
+ :param destinations: List of all destination nodes
132
+ :type destinations: list[Node]
133
+
134
+ :param transpose: Return matrix as transpose version
135
+ :type transpose: bool
136
+
137
+ Returns
138
+ -------
139
+ :return: matrix table (rows = Origins, cols = Destinations).
140
+ If "x" we have a flux from origin to destination
141
+ If None we dont have a flux
142
+ :rtype: list[list]
143
+ """
144
+ table = []
145
+ if transpose:
146
+ for destination in destinations:
147
+ # Init new row
148
+ row = []
149
+ # Get all nodes that are registred as a flux destination from origin
150
+ registered_origins = \
151
+ [flux.orig for flux in destination.input_flux]
152
+ # If we have such nodes
153
+ if len(registered_origins) > 0:
154
+ for origin in origins:
155
+ # 'x' if we have a flux origin -> destination
156
+ # None otherwise
157
+ if origin in registered_origins:
158
+ row.append('x')
159
+ else:
160
+ row.append(None)
161
+ else:
162
+ row += [None]*len(origins)
163
+ # Add row to table
164
+ table.append(row)
165
+ else:
166
+ for origin in origins:
167
+ # Init new row
168
+ row = []
169
+ # Get all nodes that are registred as a flux destination from origin
170
+ registered_destinations = \
171
+ [flux.dest for flux in origin.output_flux]
172
+ # If we have such nodes
173
+ if len(registered_destinations) > 0:
174
+ for destination in destinations:
175
+ # 'x' if we have a flux origin -> destination
176
+ # None otherwise
177
+ if destination in registered_destinations:
178
+ row.append('x')
179
+ else:
180
+ row.append(None)
181
+ else:
182
+ row += [None]*len(destinations)
183
+ # Add row to table
184
+ table.append(row)
185
+ # Create and return panda table
186
+ return table
187
+
188
+
189
+ def is_hex(s):
190
+ return re.fullmatch(r"^\# ?[0-9a-fA-F]+$", s or "") is not None
191
+
192
+
193
+ def _convertColorToHex(color, default_color=''):
194
+ """
195
+ Convert a color str to hex value.
196
+
197
+ Parameters
198
+ ----------
199
+ :param color: color to convert
200
+ :type color: str
201
+ """
202
+ if type(color) is str:
203
+ # is the color as hexa ?
204
+ if (re.fullmatch(r"^\# ?[0-9a-fA-F]+$", color) is not None) or (color == ""):
205
+ return color
206
+ else:
207
+ return webcolors.name_to_hex(color)
208
+ return default_color
209
+
210
+
211
+ def _reorderTable(
212
+ table: pd.DataFrame,
213
+ cols: list,
214
+ contents: list
215
+ ):
216
+ """
217
+ Reorder table lines accordlying to values presents in list of cols
218
+
219
+ Example :
220
+ -------
221
+ Col 1 | Col 2 | Col 3
222
+ ---------------------
223
+ a | 1 | 2
224
+ b | 2 | 2
225
+ c | 2 | 1
226
+ d | 1 | 2
227
+
228
+ Reordering as [Col2, Col3] gives
229
+ Col 1 | Col 2 | Col 3
230
+ ---------------------
231
+ a | 1 | 2
232
+ d | 1 | 2
233
+ c | 2 | 1
234
+ b | 2 | 2
235
+
236
+ Parameters
237
+ ----------
238
+ :param table: Table to reorder
239
+ :type table: panda.DataFrame
240
+
241
+ :param cols: List of ref columns for reordering
242
+ :type cols: list as [str, ...]
243
+
244
+ :param contents: List possible content per col. Must be ordered following this.
245
+ If content not in this list, then will be processed at the end.
246
+ :type contents: list as [list as [str, ...], ...]
247
+
248
+ Returns
249
+ -------
250
+ :return: Reordered table
251
+ :rtype: panda.DataFrame
252
+
253
+ """
254
+ # No more ordering cols -> return table
255
+ if len(cols) == 0:
256
+ return table
257
+ # Create new table to get block of sorted subtables
258
+ new_table = pd.DataFrame(columns=table.columns)
259
+ sorting_col = cols.pop()
260
+ sorting_contents = contents.pop()
261
+ present_contents = table[sorting_col].unique()
262
+ # Update sorting content with unexpected content
263
+ prio_sorting_contents = [_ for _ in sorting_contents if _ in present_contents]
264
+ other_sorting_contents = [_ for _ in present_contents if _ not in sorting_contents]
265
+ sorting_contents = prio_sorting_contents + other_sorting_contents
266
+ # Sort
267
+ for value in sorting_contents:
268
+ # Get subtable
269
+ if value is None:
270
+ sub_table = table.loc[table[sorting_col].isnull()]
271
+ else:
272
+ sub_table = table.loc[table[sorting_col] == value]
273
+ # Recursive filtering of sub_table
274
+ if len(cols) > 0:
275
+ sub_table = _reorderTable(sub_table, cols.copy(), contents.copy())
276
+ # Appending results
277
+ new_table = new_table._append(sub_table, ignore_index=True)
278
+ return new_table