pyflutter-app 0.1.1__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 (45) hide show
  1. converter/__init__.py +0 -0
  2. converter/__main__.py +33 -0
  3. converter/assets/forms_config.json +57 -0
  4. converter/excel/__init__.py +0 -0
  5. converter/excel/_page_edit.py +199 -0
  6. converter/excel/controller.py +297 -0
  7. converter/excel/model.py +14 -0
  8. converter/excel/view.py +278 -0
  9. converter/lib_toi.py +797 -0
  10. converter/page_forms/__init__.py +195 -0
  11. converter/page_forms/common.py +81 -0
  12. converter/page_forms/config.py +129 -0
  13. converter/page_forms/controller_forms.py +186 -0
  14. converter/page_forms/model_forms.py +52 -0
  15. converter/page_forms/service.py +207 -0
  16. converter/table_widget.py +138 -0
  17. converter/whatsapp/__init__.py +0 -0
  18. converter/whatsapp/model.py +25 -0
  19. pyflutter_app-0.1.1.dist-info/METADATA +104 -0
  20. pyflutter_app-0.1.1.dist-info/RECORD +45 -0
  21. pyflutter_app-0.1.1.dist-info/WHEEL +4 -0
  22. pyflutter_ui/__init__.py +25 -0
  23. pyflutter_ui/app.py +305 -0
  24. pyflutter_ui/core.py +791 -0
  25. pyflutter_ui/file_picker.py +268 -0
  26. pyflutter_ui/navigator.py +95 -0
  27. pyflutter_ui/theme.py +1059 -0
  28. pyflutter_ui/widgets.py +1874 -0
  29. table_stream/__init__.py +12 -0
  30. table_stream/erros.py +102 -0
  31. table_stream/hash_map.py +307 -0
  32. table_stream/notifier.py +170 -0
  33. table_stream/pattner/__init__.py +7 -0
  34. table_stream/pattner/adapter.py +40 -0
  35. table_stream/pattner/observer.py +279 -0
  36. table_stream/sheet/__init__.py +10 -0
  37. table_stream/sheet/check_mod.py +9 -0
  38. table_stream/sheet/load.py +322 -0
  39. table_stream/sheet/options.py +72 -0
  40. table_stream/sheet/parse.py +207 -0
  41. table_stream/sheet/save.py +146 -0
  42. table_stream/state.py +284 -0
  43. table_stream/sysfiles.py +530 -0
  44. table_stream/thread.py +171 -0
  45. table_stream/workbook.py +19 -0
converter/__init__.py ADDED
File without changes
converter/__main__.py ADDED
@@ -0,0 +1,33 @@
1
+ from pyflutter_ui import (
2
+ Provider, TkMaterialApp, run_app
3
+ )
4
+ from converter.excel.view import HomePage
5
+ from converter.excel.controller import ControllerHomePage
6
+ from converter.excel.model import ExcelModel
7
+
8
+ from converter.page_forms import ControllerForms
9
+ from converter.page_forms.model_forms import ModelForms
10
+ import pandas as pd
11
+
12
+
13
+ def main() -> None:
14
+ model = ExcelModel()
15
+ model_forms = ModelForms()
16
+
17
+ controller_excel = ControllerHomePage(model)
18
+ controller_forms = ControllerForms(model_forms)
19
+
20
+ app = TkMaterialApp(home=HomePage())
21
+ #app.register_notifier(controller)
22
+ Provider.add_state(controller_excel)
23
+ Provider.add_state(controller_excel.data_columns)
24
+ Provider.add_state(controller_forms)
25
+
26
+ app.set_width(760)
27
+ app.set_height(480)
28
+ run_app(app)
29
+
30
+
31
+ if __name__ == '__main__':
32
+ main()
33
+
@@ -0,0 +1,57 @@
1
+ {
2
+ "form_url": "https://forms.office.com/pages/responsepage.aspx?id=hoDG-Fy5y0mdOO03lAq-9Q3te9L0aHxBnNMUUIYjxH9UMVFGS0ozVEVKVU84RlBDNTJXVlQ1UUZZRy4u",
3
+ "success_text": "Sua resposta foi enviada",
4
+ "wait_after_submit_ms": 2500,
5
+ "fields": [
6
+ {
7
+ "name": "uc",
8
+ "question_text": "uc",
9
+ "field_type": "text",
10
+ "column": null,
11
+ "options": [],
12
+ "multiple_files": false,
13
+ "date_format": "%d/%m/%Y",
14
+ "required": true
15
+ },
16
+ {
17
+ "name": "toi",
18
+ "question_text": "toi",
19
+ "field_type": "text",
20
+ "column": null,
21
+ "options": [],
22
+ "multiple_files": false,
23
+ "date_format": "%d/%m/%Y",
24
+ "required": true
25
+ },
26
+ {
27
+ "name": "local",
28
+ "question_text": "local de entrega",
29
+ "field_type": "radio",
30
+ "column": null,
31
+ "options": ["em mãos", "caixa de correios", "outro"],
32
+ "multiple_files": false,
33
+ "date_format": "%d/%m/%Y",
34
+ "required": true
35
+ },
36
+ {
37
+ "name": "tipo",
38
+ "question_text": "tipo de documento",
39
+ "field_type": "dropdown",
40
+ "column": null,
41
+ "options": ["carta calculo fatura", "via toi", "cumprimento sentença"],
42
+ "multiple_files": false,
43
+ "date_format": "%d/%m/%Y",
44
+ "required": true
45
+ },
46
+ {
47
+ "name": "data",
48
+ "question_text": "data",
49
+ "field_type": "date",
50
+ "column": null,
51
+ "options": [],
52
+ "multiple_files": false,
53
+ "date_format": "%d/%m/%Y",
54
+ "required": true
55
+ }
56
+ ]
57
+ }
File without changes
@@ -0,0 +1,199 @@
1
+ # converter/excel/page_edit.py
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+ from pyflutter_ui import (
6
+ BuildContext, FlutterWidget, StatelessWidget,
7
+ Consumer, Column, Row, Expanded, Divider,
8
+ ElevatedButton, Text, SizedBox, Scaffold, AppBar,
9
+ MainAxisAlignment, CrossAxisAlignment,
10
+ SnackBar, SnackBarType, TableView, ValueNotifier,
11
+ ValueListenableBuilder, Provider, DropdownButton
12
+ )
13
+ from pyflutter_ui.widgets import ElevatedButton
14
+
15
+ from converter.excel.controller import ControllerHomePage, DataNotifer, CommandEditColumns
16
+ from converter.table_widget import TableOrProgress
17
+ from table_stream import ArrayList
18
+
19
+
20
+ class PageEditExcel(StatelessWidget):
21
+ """
22
+ Página de edição de dados — acessada via Navigator.push().
23
+ Exibe o DataFrame carregado e permite operações básicas.
24
+ """
25
+
26
+ def __init__(self, key: str | None = None) -> None:
27
+ super().__init__(key)
28
+
29
+ def build(self, context: BuildContext) -> FlutterWidget:
30
+ return _PageEditState()
31
+
32
+
33
+ class _PageEditState(StatelessWidget):
34
+
35
+ def __init__(self) -> None:
36
+ super().__init__()
37
+
38
+ @property
39
+ def controller(self) -> ControllerHomePage:
40
+ return Provider.read(ControllerHomePage)
41
+
42
+ def add_work_col(self):
43
+ """
44
+ Ação do botão que adiciona uma coluna a lista de trabalho
45
+ """
46
+
47
+ _selected: str = self.controller.data_notifier.selectedColumnSheet
48
+ self.controller.data_columns.add_column_edit(_selected)
49
+
50
+ print(f'{__class__.__name__} [DEBUG] coluna adicionada ... {_selected}')
51
+
52
+ def action_back_page(self, context: BuildContext) -> None:
53
+ print(context.name)
54
+ context.navigator.pop()
55
+
56
+ def action_drop_column(self, context: BuildContext, columns: list[str]) -> None:
57
+ """
58
+ Ação para apagar uma lista de colunas.
59
+ """
60
+ self.controller.data_columns.delete_columns(ArrayList(columns))
61
+
62
+ def action_load_data(self, context: BuildContext) -> None:
63
+
64
+ if self.controller.get_file_sheet() is None:
65
+ SnackBar.of(context).show(
66
+ "Importe um arquivo na página anterior para prosseguir!",
67
+ type=SnackBarType.INFO,
68
+ )
69
+ return
70
+
71
+ self.controller.command_load_data()
72
+
73
+ def action_change_selected_column(self, col: str):
74
+ self.controller.data_columns.selectedColumn = col
75
+
76
+ def build_selected_columns(self, context: BuildContext, controller: ControllerHomePage):
77
+ """
78
+ Colunas para edição apagar/concatenar etc.
79
+ """
80
+ columns: ArrayList[str] = controller.data_columns.value
81
+ current = columns.get_first() if columns.size() > 0 else "-"
82
+
83
+ return DropdownButton(
84
+ columns,
85
+ current,
86
+ on_changed=self.action_change_selected_column
87
+ )
88
+
89
+ def build_value_selected_columns(self, context: BuildContext, columns: ArrayList[str]):
90
+ """
91
+ Colunas para edição apagar/concatenar etc.
92
+ """
93
+ current = columns.get_first() if columns.size() > 0 else "-"
94
+
95
+ return DropdownButton(
96
+ columns,
97
+ current,
98
+ on_changed=self.action_change_selected_column
99
+ )
100
+
101
+ def build_edit_options(self, context: BuildContext, controller_columns: CommandEditColumns) -> FlutterWidget:
102
+ """
103
+ Botões para editar o DataFrame()
104
+ """
105
+
106
+ df: pd.DataFrame = controller_columns.controller.data_notifier.value
107
+ if df.empty:
108
+ return Text("Nenhum dado carregado")
109
+
110
+ cols: ArrayList[str] = ArrayList(df.columns.tolist())
111
+
112
+ return Row(
113
+ children=[
114
+ ElevatedButton("Adicionar coluna", on_pressed=self.add_work_col),
115
+ SizedBox(width=8),
116
+ DropdownButton(
117
+ cols,
118
+ on_changed=lambda x: controller_columns.controller.data_notifier.set_selected_column_sheet(x),
119
+ ),
120
+ Divider(),
121
+ #Consumer(ExcelController, self.build_selected_columns,),
122
+ ValueListenableBuilder[ArrayList[str]](
123
+ controller_columns, self.build_value_selected_columns
124
+ ),
125
+ ],
126
+ main_axis_alignment=MainAxisAlignment.START,
127
+ )
128
+
129
+ def build_table(self, context: BuildContext, ctrl: ControllerHomePage) -> FlutterWidget:
130
+
131
+ df = ctrl.data_notifier.value
132
+ if df.empty:
133
+ df = pd.DataFrame({"A": ["Sem dados"]})
134
+ return TableOrProgress(
135
+ running=ctrl.data_notifier.running,
136
+ df=df,
137
+ )
138
+
139
+ def build_info_sheet(self, context: BuildContext, controller: ControllerHomePage) -> FlutterWidget:
140
+ print(f'_build_info: {context.name}')
141
+
142
+ df = controller.data_notifier.value
143
+ if df.empty:
144
+ return Text("Nenhum dado disponível")
145
+
146
+ linhas, colunas = df.shape
147
+ arquivo = controller.get_file_sheet()
148
+ nome = arquivo.basename() if arquivo else "—"
149
+
150
+ return Row(
151
+ children=[
152
+ Text(f"Arquivo: {nome}"),
153
+ Divider(orientation="horizontal"),
154
+ Text(f"{linhas} linhas"),
155
+ Divider(orientation="horizontal"),
156
+ Text(f"{colunas} colunas"),
157
+ ],
158
+ main_axis_alignment=MainAxisAlignment.START,
159
+ )
160
+
161
+ def build_body_app(self, context: BuildContext, ctrl: ControllerHomePage) -> FlutterWidget:
162
+
163
+ return Column(
164
+ children=[
165
+ # ── barra de ações ──────────────────────────
166
+ Row(
167
+ children=[
168
+ ElevatedButton(
169
+ "← Voltar",
170
+ on_pressed=lambda: self.action_back_page(context),
171
+ ),
172
+ Divider(orientation="horizontal"),
173
+ ElevatedButton(
174
+ "Recarregar dados",
175
+ on_pressed=lambda: self.action_load_data(context),
176
+ ),
177
+ # ── info do dataset ─────────────────────────
178
+ Consumer(ControllerHomePage, self.build_info_sheet),
179
+ ],
180
+ main_axis_alignment=MainAxisAlignment.START,
181
+ ),
182
+ Divider(),
183
+
184
+ Divider(),
185
+ # ── seletor de coluna para remoção ──────────
186
+ Consumer(CommandEditColumns, self.build_edit_options),
187
+ Divider(),
188
+ # ── tabela ──────────────────────────────────
189
+ Expanded(Consumer(ControllerHomePage, self.build_table)),
190
+ ],
191
+ )
192
+
193
+ def build(self, context: BuildContext) -> FlutterWidget:
194
+ context.name = "Build do Edit"
195
+
196
+ return Scaffold(
197
+ Consumer(ControllerHomePage, builder=self.build_body_app),
198
+ #app_bar=AppBar("Edição de dados"),
199
+ )
@@ -0,0 +1,297 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Callable
3
+
4
+ import pandas as pd
5
+ from pandas import DataFrame
6
+
7
+ from table_stream import (
8
+ SheetLoader, SheetType, SaveData, csvEncoding, csvSeparator,
9
+ CsvOptions, OptionsIOSheet, ParserData, FactorySaveData,
10
+ FactorySheetLoader, ArrayList, HashMapDict, ControllerThread,
11
+ ConsumerThread, ThreadStateContext, ContextState
12
+ )
13
+ from table_stream.sysfiles import Directory, File
14
+
15
+
16
+ from pyflutter_ui import ChangeNotifier, CommandNotifier, ValueNotifier
17
+ from converter.excel.model import ExcelModel
18
+
19
+
20
+ class FileNotifier(ValueNotifier[HashMapDict[str, File]]):
21
+ """
22
+ Controller/Notificador
23
+
24
+ Notifica sobre a alteração de arquivos selecionados/adicionados pelo usuário.
25
+ """
26
+
27
+
28
+ def __init__(self, value: Any) -> None:
29
+ super().__init__(value)
30
+
31
+ def get_file_from_name(self, name: str) -> File:
32
+ if not self.value.header().contains(name):
33
+ raise ValueError()
34
+ return self.value.get_value(name)
35
+
36
+ def clear(self) -> None:
37
+ self._value.clear()
38
+
39
+ def delete_file(self, name: str):
40
+ self._value.delete_items([name])
41
+
42
+ def add_files(self, files: list[File]) -> None:
43
+ """
44
+ Adiciona apenas arquivos NÃO EXISTENTES. Se o basename de um
45
+ arquivo da lista for igual o basename de outro arquivo já existente
46
+ tal arquivo será ignorado, mantendo-se apenas o arquivo já existente.
47
+ """
48
+ file: File
49
+ for file in files:
50
+ if not self._value.header().contains(file.basename()):
51
+ self._value.set_value(file.basename(), file)
52
+ self.notify_listeners()
53
+
54
+
55
+ class DataNotifer(ValueNotifier[pd.DataFrame]):
56
+
57
+ def __init__(self, value: DataFrame) -> None:
58
+ super().__init__(value)
59
+ self.running: bool = False
60
+ cols = ArrayList(value.columns.tolist())
61
+ self.selectedColumnSheet: str = cols.get_first() if cols.size() > 0 else "-"
62
+
63
+ @property
64
+ def value(self) -> DataFrame:
65
+ return self._value
66
+
67
+ @value.setter
68
+ def value(self, value: pd.DataFrame) -> None:
69
+ self._value = value
70
+
71
+ if (self.selectedColumnSheet is None) or (self.selectedColumnSheet == "-"):
72
+ columns = self.value.columns.tolist()
73
+ if len(columns) > 0:
74
+ self.selectedColumnSheet = columns[0]
75
+
76
+ self.notify_listeners()
77
+
78
+ def set_selected_column_sheet(self, col: str):
79
+ self.selectedColumnSheet = col
80
+ print(f'{__class__.__name__} coluna alterada: {col}')
81
+
82
+ def clear(self):
83
+ self.running = False
84
+ self.value = pd.DataFrame()
85
+
86
+
87
+ class CommandEditColumns(ValueNotifier[ArrayList[str]]):
88
+
89
+ def __init__(self, value: ArrayList[str], controller: ControllerHomePage) -> None:
90
+ super().__init__(value)
91
+ self.running: bool = False
92
+ self.controller: ControllerHomePage = controller
93
+ self._columnsEdit: ArrayList[str] = value
94
+ self.selectedColumn: str | None = None
95
+
96
+ @property
97
+ def columns(self) -> ArrayList[str]:
98
+ return ArrayList(self.controller.data_notifier.value.columns.tolist())
99
+
100
+ @property
101
+ def data(self) -> pd.DataFrame:
102
+ return self.controller.data_notifier.value
103
+
104
+ @data.setter
105
+ def data(self, data: pd.DataFrame):
106
+ self.controller.data_notifier.value = data
107
+
108
+ def get_columns_edit(self) -> ArrayList[str]:
109
+ return self._columnsEdit
110
+
111
+ def set_columns_edit(self, cols: ArrayList[str]):
112
+ self._columnsEdit = cols
113
+
114
+ def add_column_edit(self, col: str):
115
+ if col is None:
116
+ return
117
+ if not self.columns.contains(col):
118
+ return
119
+
120
+ if not self.get_columns_edit().contains(col):
121
+ self._columnsEdit.append(col)
122
+ self.notify_listeners()
123
+ print(f'* [DEBUG] coluna adicionada ==> {col}')
124
+
125
+ def delete_columns(self, cols: ArrayList[str]):
126
+ self.running = True
127
+ self.notify_listeners()
128
+
129
+ remove_list = ArrayList()
130
+ for col in cols:
131
+ if self.columns.contains(col):
132
+ remove_list.append(col)
133
+
134
+ if remove_list.size() < 1:
135
+ self.running = False
136
+ self.notify_listeners()
137
+ return
138
+
139
+ parser = ParserData(self.data)
140
+ parser.delete_columns(remove_list)
141
+ self.data = parser.get_data()
142
+ self.running = False
143
+ self.notify_listeners()
144
+
145
+
146
+ class ControllerHomePage(ValueNotifier[DataFrame]):
147
+
148
+ def __init__(self, model: ExcelModel, *, value: pd.DataFrame = None) -> None:
149
+ if value is None:
150
+ value = pd.DataFrame()
151
+ super().__init__(value)
152
+ self.model: ExcelModel = model
153
+ self.operation_state: ContextState = ContextState()
154
+
155
+ # Opções para leitura de uma planilha
156
+ self.csv_options = CsvOptions()
157
+ self._file: File = None
158
+ self._sheet_name: str = None
159
+ self.sheet_names: ArrayList[str] = ArrayList()
160
+
161
+ # Mapa de arquivos importados pelo usuário
162
+ self.files_notifier: FileNotifier = FileNotifier(HashMapDict())
163
+
164
+ # Opções de exportação
165
+ self._options_save: OptionsIOSheet = OptionsIOSheet(SheetType.EXCEL)
166
+
167
+ #======================================================#
168
+ # Leitura dos dados
169
+ #======================================================#
170
+ self.data_notifier = DataNotifer(pd.DataFrame())
171
+ self.data_columns = CommandEditColumns(ArrayList(), self)
172
+
173
+ # Thread
174
+ self._currentThread: ControllerThread = None
175
+ self.thread_context: ThreadStateContext = ThreadStateContext()
176
+ # Menssagens
177
+ self.msg_erro = None
178
+ self.msg_info = None
179
+
180
+ @property
181
+ def sheet_name(self) -> str:
182
+ return self._sheet_name
183
+
184
+ @sheet_name.setter
185
+ def sheet_name(self, new: str):
186
+ if not self.sheet_names.contains(new):
187
+ print(f'[DEBUG] Aba ... {new} não está disponível para o arquivo: {self._file}')
188
+ return
189
+ self._sheet_name = new
190
+ self.msg_info = f"Aba atualizada ... {self._sheet_name}"
191
+ self.notify_listeners()
192
+ self.command_load_data()
193
+
194
+ @property
195
+ def options_load(self) -> OptionsIOSheet | None:
196
+ if self._file is None:
197
+ return None
198
+
199
+ opt_load = OptionsIOSheet(SheetType.EXCEL if self._file.is_excel() else SheetType.CSV)
200
+
201
+ if self._file.is_ods():
202
+ opt_load.sheet_type = SheetType.ODS
203
+ if self._sheet_name is not None:
204
+ opt_load.sheet_name = self._sheet_name
205
+
206
+ return opt_load
207
+
208
+ @property
209
+ def sheet_loader(self) -> SheetLoader:
210
+ if self._file is None:
211
+ raise ValueError('Nenhum arquivo disponível')
212
+ return SheetLoader.create(self.options_load, self._file)
213
+
214
+ @property
215
+ def running(self) -> bool:
216
+ return self.operation_state.state.is_running()
217
+
218
+ @property
219
+ def files(self) -> ArrayList[File]:
220
+ return self.files_notifier.value.values()
221
+
222
+ def create_consumer(self, on_finish: Callable[[ThreadStateContext]]) -> ConsumerThread:
223
+ consumer = ConsumerThread(ThreadStateContext, on_finish=on_finish)
224
+ self.thread_context.add_listener(consumer)
225
+ return consumer
226
+
227
+ def init_thread(self) -> None:
228
+ self._currentThread._state_context = self.thread_context
229
+ self._currentThread.start()
230
+ self.operation_state.start()
231
+ self.notify_listeners()
232
+
233
+ def set_file_sheet(self, file: File):
234
+ self._file = file
235
+ self.sheet_names = self.sheet_loader.get_sheet_names()
236
+ self.notify_listeners()
237
+ self.sheet_name = self.sheet_names.get_first()
238
+
239
+ def get_file_sheet(self) -> File:
240
+ return self._file
241
+
242
+ def add_files(self, files: ArrayList[File]):
243
+ self.files_notifier.add_files(files)
244
+ self.notify_listeners()
245
+
246
+ def command_delete_columns(self):
247
+ if self.running:
248
+ raise RuntimeError("Já existe outra operação de leitura em andamento...")
249
+
250
+ self._currentThread = ControllerThread(
251
+ target=self.data_columns.delete_columns, args=self.data_columns.value
252
+ )
253
+ self.create_consumer(lambda context: self.operation_state.finish(context.is_error()))
254
+
255
+ self.init_thread()
256
+
257
+ def action_finish_delete_columns(self, result: ThreadStateContext[None]):
258
+ self.operation_state.finish(result.is_error())
259
+ self.notify_listeners()
260
+
261
+ def command_load_data(self) -> None:
262
+ if self.running:
263
+ raise RuntimeError("Já existe outra operação de leitura em andamento...")
264
+
265
+ loader = self.sheet_loader
266
+ self._currentThread = ControllerThread(target=loader.load)
267
+ c = self.create_consumer(self.action_finish_load)
268
+ c.on_error = self._action_error_load_data
269
+
270
+ self.data_notifier.running = True
271
+ self.data_notifier.value = pd.DataFrame()
272
+ self.msg_info = "Carregando dados..."
273
+ self.init_thread()
274
+
275
+ def _action_error_load_data(self, result: ThreadStateContext):
276
+ self.data_notifier.running = False
277
+ self.msg_erro = f'{result.get_exception()}'
278
+
279
+ def action_finish_load(self, result: ThreadStateContext[pd.DataFrame]) -> None:
280
+ self.data_notifier.running = False
281
+ self.data_notifier.value = result.value
282
+ self._currentThread = None
283
+
284
+ if not self.data_notifier.value.empty:
285
+ self.msg_info = "Planilha carregada com sucesso..."
286
+
287
+ self.operation_state.finish(result.is_error())
288
+ self.notify_listeners()
289
+
290
+ def init_state(self, data: HashMapDict = None):
291
+ if data is not None:
292
+ pass
293
+
294
+ def dispose(self):
295
+ self._options_load = None
296
+ self._options_save = None
297
+ self.files_notifier.value.clear()
@@ -0,0 +1,14 @@
1
+ import pandas as pd
2
+ from pandas import DataFrame
3
+ from table_stream.hash_map import ArrayList, HashMapDict
4
+ from table_stream.sheet import (
5
+ SheetLoader, SheetType, SaveData, csvEncoding, csvSeparator,
6
+ CsvOptions, OptionsIOSheet, ParserData, FactorySaveData,
7
+ FactorySheetLoader,
8
+ )
9
+
10
+
11
+ class ExcelModel(object):
12
+
13
+ def __init__(self) -> None:
14
+ pass