responds-forms 0.4__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.
cartas/origin.py ADDED
@@ -0,0 +1,1196 @@
1
+ # arquivo cartas/__init__.py
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ import re
5
+ from enum import StrEnum
6
+ from typing import Any
7
+ from abc import ABC, abstractmethod
8
+ from os import rename as rename_file
9
+ from datetime import datetime
10
+
11
+ import pandas as pd
12
+
13
+ from table_stream import (
14
+ # Array
15
+ HashMapDict,
16
+ HashMap,
17
+ ArrayList,
18
+ Set,
19
+ # Tabelas
20
+ TableData,
21
+ ArrayTables,
22
+ ColumnV,
23
+ RowH,
24
+ HeadersSizeNotIqualError,
25
+ # Arquivos.
26
+ HeaderMetaData,
27
+ hashFile,
28
+ File,
29
+ Directory,
30
+ ContentFiles,
31
+ load_file_hash,
32
+ )
33
+
34
+ from ocr_stream import HeaderOcr, BuilderConfigOcr, CreateRelatorioOcr
35
+
36
+ from whatsapp_files import (
37
+ RowWhatsApp,
38
+ TableWhatsApp,
39
+ HeaderWhatsApp,
40
+ BuilderTableWhatsApp,
41
+ ControllerWhatsApp,
42
+ move_path,
43
+ )
44
+
45
+ __version__ = "0.1"
46
+
47
+
48
+ @dataclass
49
+ class ConfigTOI:
50
+
51
+ file_relatorio_toi: File
52
+ file_relatorio_forms: File
53
+ file_table_toi: File
54
+ file_master_forms: File # NOVO
55
+ dir_base_toi: Directory
56
+ dir_log_excel: Directory
57
+ dir_relatorios: Directory
58
+ work_dir: Directory
59
+
60
+
61
+ class BuilderConfigTOI(object):
62
+
63
+ _instance = None # Atributo de classe para armazenar a instância singleton
64
+
65
+ def __new__(cls, *args, **kwargs):
66
+ if cls._instance is None:
67
+ cls._instance = super(BuilderConfigTOI, cls).__new__(cls)
68
+ return cls._instance
69
+
70
+ def __init__(self) -> None:
71
+
72
+ # Garante que __init__ não será executado mais de uma vez
73
+ if hasattr(self, "_initialized") and self._initialized:
74
+ return
75
+
76
+ super().__init__()
77
+ self._initialized: bool = True
78
+ self._builder_ocr = BuilderConfigOcr.get_instance()
79
+ self._config = self._create()
80
+
81
+ @property
82
+ def dir_base_work(self) -> Directory:
83
+ return self._builder_ocr.builder().dir_base_work
84
+
85
+ @property
86
+ def dir_retorno_de_campo(self) -> Directory:
87
+ return self.dir_base_work.concat("Cartas-TOI", True).concat(
88
+ "Retorno-de-campo", True
89
+ )
90
+
91
+ @property
92
+ def dir_log_excel(self) -> Directory:
93
+ return self.dir_base_work.concat("Cartas-TOI", True).concat("Logs", True)
94
+
95
+ @property
96
+ def dir_relatorios(self) -> Directory:
97
+ return self.dir_base_work.concat("Cartas-TOI", True).concat(
98
+ "Relatorios-toi", True
99
+ )
100
+
101
+ @property
102
+ def file_relatorio_toi(self) -> File:
103
+ d = self._builder_ocr.builder().dir_table
104
+ return d.join_file("Relatorio-tois.xlsx")
105
+
106
+ @property
107
+ def file_relatorio_forms(self) -> File:
108
+ d = self._builder_ocr.builder().dir_table
109
+ return d.join_file("cartas-toi-lancadas.xlsx")
110
+
111
+ @property
112
+ def file_master_forms(self) -> File:
113
+ d = self._builder_ocr.builder().dir_table
114
+ return d.join_file("master-forms-toi.xlsx")
115
+
116
+ @property
117
+ def file_table_toi(self) -> File:
118
+ d = self._builder_ocr.builder().dir_table
119
+ return d.join_file("tabela-entregas-tois.xlsx")
120
+
121
+ def _create(self) -> ConfigTOI:
122
+ return ConfigTOI(
123
+ work_dir=self.dir_base_work,
124
+ dir_base_toi=self.dir_retorno_de_campo,
125
+ file_relatorio_toi=self.file_relatorio_toi,
126
+ file_table_toi=self.file_table_toi,
127
+ file_relatorio_forms=self.file_relatorio_forms,
128
+ dir_log_excel=self.dir_log_excel,
129
+ dir_relatorios=self.dir_relatorios,
130
+ file_master_forms=self.file_master_forms,
131
+ )
132
+
133
+ def builder(self) -> ConfigTOI:
134
+ if self._config is None:
135
+ self._config = self._create()
136
+ return self._config
137
+
138
+
139
+ # ============================================================#
140
+ # Extração de UC/TOI tolerante a falhas de OCR
141
+ # ============================================================#
142
+ #
143
+ # UC: 1305934 TOI: 193166923 ...
144
+ # UC: 1305934 TO! 193166923 ... (TOI virou TO!, ":" some)
145
+ # UC| 1305934 T0I: 193166923 ... (0 no lugar do O)
146
+ #
147
+
148
+ UC_TOI_PATTERN = re.compile(
149
+ r"U[C0]\s*[:;|!]?\s*(\d{4,10})" r".{0,60}?" r"T[O0][I1L!l]?\s*[:;|!]?\s*(\d{4,10})",
150
+ re.IGNORECASE | re.DOTALL,
151
+ )
152
+
153
+
154
+ def extrair_uc_toi(texto: str) -> tuple[str, str] | None:
155
+ """
156
+ Retorna (uc, toi) a partir do texto OCR completo de um arquivo,
157
+ ou None se o padrão não for encontrado.
158
+ """
159
+ m = UC_TOI_PATTERN.search(texto)
160
+ if not m:
161
+ return None
162
+ return m.group(1), m.group(2)
163
+
164
+
165
+ # ========================================================#
166
+ # Tabelas TOI.
167
+ # ========================================================#
168
+
169
+
170
+ class HeaderEntregaTOI(StrEnum):
171
+ """
172
+ Enumerador que representa a relação entre um documento e os dados
173
+ de entrega:
174
+ UC, TOI, DAY, MON, YEAR, DATE E HASH_FILE.
175
+
176
+ a relação/key de uma entrega e a hash do documento.
177
+ """
178
+
179
+ UC = "uc"
180
+ TOI = "toi"
181
+ DAY = "day"
182
+ MON = "mon"
183
+ YEAR = "year"
184
+ DATE = "data_entrega"
185
+ HASH_FILE = HeaderMetaData.HASH_FILE.value
186
+
187
+
188
+ class HeaderFilesTOI(StrEnum):
189
+ """
190
+ Dados completos de um arquivo TOI, incluindo os metadados e
191
+ caminho absoluto.
192
+ """
193
+
194
+ UC = HeaderEntregaTOI.UC
195
+ TOI = HeaderEntregaTOI.TOI
196
+ DAY = HeaderEntregaTOI.DAY
197
+ MON = HeaderEntregaTOI.MON
198
+ YEAR = HeaderEntregaTOI.YEAR
199
+ DATE = HeaderEntregaTOI.DATE
200
+ HASH_FILE = HeaderEntregaTOI.HASH_FILE
201
+
202
+ LOCAL_ENTREGA = "local_entrega"
203
+ TIPO_DOC = "tipo_carta"
204
+
205
+ IS_FILE = "arquivo?"
206
+ FULL_PATH = HeaderMetaData.FILE_PATH.value
207
+ SRC = HeaderMetaData.PARENT_DIR.value
208
+ FILE_EXTENSION = HeaderMetaData.FILE_EXTENSION.value
209
+ FILE_BASENAME = "basename"
210
+
211
+
212
+ class TipoCarta(StrEnum):
213
+
214
+ CARTA_CALCULO = "CARTA-CALCULO"
215
+
216
+
217
+ class LocalEntregaCarta(StrEnum):
218
+
219
+ OUTRO = "OUTRO"
220
+ MAOS = "MÃOS"
221
+ CORREIOS = "CORREIOS"
222
+ PORTAO = "PORTÃO"
223
+
224
+
225
+ # ========================================================#
226
+ # Criar Tabelas.
227
+ # ========================================================#
228
+
229
+
230
+ class BuilderTOI(ABC):
231
+ """
232
+ Criar dados de uma entrega TOI apartir de um arquivo ou do texto
233
+ de um documento.
234
+ """
235
+
236
+ def __init__(self) -> None:
237
+ super().__init__()
238
+
239
+ @abstractmethod
240
+ def create_void(self) -> TableData[Any]:
241
+ pass
242
+
243
+ @abstractmethod
244
+ def from_data(self, df: pd.DataFrame) -> TableData[Any]:
245
+ pass
246
+
247
+ @abstractmethod
248
+ def from_path_name(self, path: File, md5: hashFile = None) -> TableData[Any] | None:
249
+ pass
250
+
251
+ @abstractmethod
252
+ def from_hash_files(
253
+ self, data: HashMap[hashFile, File], *, ignore_null: bool = True
254
+ ) -> TableData[Any] | None:
255
+ pass
256
+
257
+
258
+ class BuilderEntregaTOI(BuilderTOI):
259
+ """
260
+ Criar dados de uma entrega TOI apartir de um arquivo ou do texto
261
+ de um documento.
262
+ """
263
+
264
+ def __init__(self) -> None:
265
+ super().__init__()
266
+ self.relatorio_ocr = CreateRelatorioOcr()
267
+ self.relatorio_wp = ControllerWhatsApp()
268
+
269
+ self.default_cols_toi = ArrayList[HeaderEntregaTOI].from_list(
270
+ [
271
+ HeaderEntregaTOI.UC,
272
+ HeaderEntregaTOI.TOI,
273
+ HeaderEntregaTOI.DATE,
274
+ HeaderEntregaTOI.YEAR,
275
+ HeaderEntregaTOI.MON,
276
+ HeaderEntregaTOI.DAY,
277
+ HeaderEntregaTOI.HASH_FILE,
278
+ ]
279
+ )
280
+
281
+ def create_void(self) -> TableData[HeaderEntregaTOI]:
282
+ """
283
+ Cria uma tabela TOI vazia com todos os itens do cabeçalho HeaderTOI
284
+ """
285
+ tb = TableData[HeaderEntregaTOI]()
286
+ self.default_cols_toi.for_each(lambda c: tb.add(ColumnV(c)))
287
+ return tb
288
+
289
+ def from_data(self, df: pd.DataFrame) -> TableData[HeaderEntregaTOI]:
290
+ """
291
+ Criar tabela TOI apartir de um DataFrame().
292
+ """
293
+ df = df.astype(str)
294
+ df_cols = ArrayList(df.columns.tolist())
295
+ tb = TableData[HeaderEntregaTOI]()
296
+
297
+ c: HeaderEntregaTOI
298
+ for c in self.default_cols_toi:
299
+ if df_cols.contains(c.value):
300
+ tb.add(
301
+ ColumnV.create_from_list(c, df[c.value].astype(str).values.tolist())
302
+ )
303
+
304
+ if tb.header().size() != self.default_cols_toi.size():
305
+ raise HeadersSizeNotIqualError(
306
+ f"DataFrame() de entrega toi com colunas diferente do padrão: \n{tb.header()}"
307
+ )
308
+ return tb
309
+
310
+ def from_path_name(
311
+ self, path: File, md5: hashFile = None
312
+ ) -> TableData[HeaderEntregaTOI] | None:
313
+ """
314
+ Criar tabela apartir do nome de uma arquivo que esteja no padrão TOI.
315
+ """
316
+
317
+ if not path.exists():
318
+ return None
319
+
320
+ mp = self.create_map_from_filename(path.name())
321
+ if mp is None:
322
+ return None
323
+ if md5 is None:
324
+ md5 = load_file_hash(path)
325
+
326
+ mp.set_value(HeaderEntregaTOI.HASH_FILE, md5)
327
+ tb = TableData[HeaderEntregaTOI]()
328
+ c: HeaderEntregaTOI
329
+
330
+ for c in self.default_cols_toi:
331
+ tb.add(ColumnV.create_from_list(c, [mp.get_value(c)]))
332
+ return tb
333
+
334
+ def create_map_from_filename(
335
+ self,
336
+ filename: str,
337
+ ) -> HashMapDict[HeaderEntregaTOI, str] | None:
338
+ """
339
+ Retorna um Map de chave/valor com as seguintes chaves:
340
+
341
+ UC, TOI, DAY, MON, YEAR e DATE.
342
+ """
343
+
344
+ try:
345
+ values = filename.split(" ")
346
+ if len(values) < 5:
347
+ # Nome incompleto ou inválido.
348
+ return None
349
+ except:
350
+ return None
351
+ else:
352
+ for x in values:
353
+ if not x.isnumeric():
354
+ return None
355
+
356
+ data = HashMapDict()
357
+ data[HeaderEntregaTOI.UC] = values[0] # UC
358
+ data[HeaderEntregaTOI.TOI] = values[1] # TOI
359
+ data[HeaderEntregaTOI.DAY] = values[2] # DIA
360
+ data[HeaderEntregaTOI.MON] = values[3] # MÊS
361
+ data[HeaderEntregaTOI.YEAR] = values[4] # ANO
362
+ data[HeaderEntregaTOI.DATE] = f"{values[2]}/{values[3]}/{values[4]}"
363
+ return data
364
+
365
+ def from_hash_files(
366
+ self, data: HashMap[hashFile, File], *, ignore_null: bool = True
367
+ ) -> TableData[HeaderEntregaTOI] | None:
368
+
369
+ if data.is_empty():
370
+ return None
371
+
372
+ header_hashs = data.header().to_set()
373
+ table_ocr: TableData[HeaderOcr] | None = self.relatorio_ocr.create_from_hashs(
374
+ header_hashs
375
+ )
376
+
377
+ if table_ocr is None:
378
+ return None
379
+ if table_ocr.is_empty():
380
+ return None
381
+
382
+ # builder_wp = BuilderTableWhatsApp()
383
+
384
+ table_whatsapp: TableData[HeaderWhatsApp] = self.relatorio_wp.load_cache_table()
385
+ if table_whatsapp.is_empty():
386
+ if ignore_null:
387
+ return None
388
+
389
+ key_hash: hashFile
390
+ sequence_tables = ArrayTables[TableData[HeaderEntregaTOI]]()
391
+
392
+ for key_hash in header_hashs:
393
+ date_chat_file: str = TableData.NULL_STRING
394
+ day = TableData.NULL_STRING
395
+ mon = TableData.NULL_STRING
396
+ year = TableData.NULL_STRING
397
+ uc = TableData.NULL_STRING
398
+ toi = TableData.NULL_STRING
399
+
400
+ # =========================================================#
401
+ # Data de envio do arquivo no chat
402
+ # =========================================================#
403
+ if not table_whatsapp.is_empty():
404
+ tb_chat = table_whatsapp.search(HeaderWhatsApp.hash_md5, key_hash)
405
+
406
+ if tb_chat.is_empty() and ignore_null:
407
+ continue
408
+
409
+ if not tb_chat.is_empty():
410
+ # date_chat_file: datetime = tb_chat.get_row_at(0).get_date_hour()
411
+ # day = str(date_chat_file.day)
412
+ # mon = str(date_chat_file.month)
413
+ # year = str(date_chat_file.year)
414
+ date_chat_file: str = tb_chat.get_row_at(0).get_date().get_first()
415
+ day = date_chat_file.split(" ")[0]
416
+ mon = date_chat_file.split(" ")[1]
417
+ year = date_chat_file.split(" ")[2]
418
+
419
+ # =========================================================#
420
+ # Extrair UC e TOI
421
+ # =========================================================#
422
+ tb_ocr = table_ocr.search(HeaderOcr.HASH_FILE, key_hash)
423
+ if tb_ocr.is_empty() and ignore_null:
424
+ continue
425
+
426
+ if not tb_ocr.is_empty():
427
+ txt_ocr: str = tb_ocr.to_string(HeaderOcr.TEXT)
428
+ value_uc_toi: tuple[str, str] | None = extrair_uc_toi(txt_ocr)
429
+
430
+ if value_uc_toi is None and ignore_null:
431
+ continue
432
+
433
+ if value_uc_toi is not None:
434
+ uc = value_uc_toi[0]
435
+ toi = value_uc_toi[1]
436
+
437
+ row_toi = RowH[HeaderEntregaTOI, Any](0)
438
+ row_toi.set(HeaderEntregaTOI.UC, uc)
439
+ row_toi.set(HeaderEntregaTOI.TOI, toi)
440
+ row_toi.set(HeaderEntregaTOI.DAY, day)
441
+ row_toi.set(HeaderEntregaTOI.MON, mon)
442
+ row_toi.set(HeaderEntregaTOI.YEAR, year)
443
+ row_toi.set(HeaderEntregaTOI.HASH_FILE, key_hash)
444
+ row_toi.set(HeaderEntregaTOI.DATE, date_chat_file)
445
+
446
+ t = self.create_void()
447
+ t.add_row(row_toi)
448
+ sequence_tables.add(t)
449
+
450
+ if sequence_tables.empty():
451
+ return None
452
+
453
+ return sequence_tables.concat(TableData).drop_duplicates()
454
+
455
+
456
+ class BuilderTableFilesTOI(BuilderTOI):
457
+
458
+ def __init__(self) -> None:
459
+ super().__init__()
460
+
461
+ self.default_cols_cartas = ArrayList[HeaderFilesTOI].from_list(
462
+ [
463
+ HeaderFilesTOI.UC,
464
+ HeaderFilesTOI.TOI,
465
+ HeaderFilesTOI.DATE,
466
+ HeaderFilesTOI.YEAR,
467
+ HeaderFilesTOI.MON,
468
+ HeaderFilesTOI.DAY,
469
+ HeaderFilesTOI.LOCAL_ENTREGA,
470
+ HeaderFilesTOI.TIPO_DOC,
471
+ HeaderFilesTOI.HASH_FILE,
472
+ HeaderFilesTOI.IS_FILE,
473
+ HeaderFilesTOI.FILE_BASENAME,
474
+ HeaderFilesTOI.FILE_EXTENSION,
475
+ HeaderFilesTOI.FULL_PATH,
476
+ HeaderFilesTOI.SRC,
477
+ ]
478
+ )
479
+
480
+ self.factory_toi: BuilderEntregaTOI = BuilderEntregaTOI()
481
+
482
+ def get_factory_toi(self) -> BuilderEntregaTOI:
483
+ return self.factory_toi
484
+
485
+ def create_void(self) -> TableData[HeaderFilesTOI]:
486
+ """
487
+ Cria uma tabela com os cabeçalhos de HeaderFilesTOI.
488
+ """
489
+ tb = TableData()
490
+ self.default_cols_cartas.for_each(lambda c: tb.add(ColumnV(c)))
491
+ return tb
492
+
493
+ def from_data(self, df: pd.DataFrame) -> TableData[HeaderFilesTOI]:
494
+ df = df.astype(str)
495
+ df_cols = ArrayList(df.columns.tolist())
496
+ final_table = TableData()
497
+
498
+ c: HeaderFilesTOI
499
+ for c in self.default_cols_cartas:
500
+
501
+ if df_cols.contains(c.value):
502
+ final_table.add(
503
+ ColumnV.create_from_list(c, df[c.value].astype(str).values.tolist())
504
+ )
505
+
506
+ if final_table.header().size() != self.default_cols_cartas.size():
507
+ msg = "DataFrame() FORMS toi com colunas diferente do padrão:"
508
+ msg_header = f"Cabeçalho FORMS {final_table.header()}"
509
+ raise HeadersSizeNotIqualError(
510
+ f"{msg} \n\n{msg_header}\nCabeçalho externo {df.columns}"
511
+ )
512
+ return final_table
513
+
514
+ def decode_table_toi(
515
+ self, tb: TableData[HeaderEntregaTOI], data: HashMap[hashFile, File]
516
+ ) -> TableData[HeaderFilesTOI] | None:
517
+
518
+ if data.is_empty():
519
+ return None
520
+ if tb.is_empty():
521
+ return None
522
+
523
+ _df_toi = tb.to_dataframe().drop_duplicates(
524
+ subset=[HeaderEntregaTOI.HASH_FILE.value]
525
+ )
526
+
527
+ tb = self.get_factory_toi().from_data(_df_toi)
528
+ final_tb = TableData[HeaderFilesTOI].create_void(self.default_cols_cartas)
529
+
530
+ rows = tb.rows()
531
+ r: RowH[HeaderEntregaTOI, Any]
532
+ row_tb: RowH[HeaderFilesTOI, Any]
533
+
534
+ for r in rows:
535
+ row_tb = RowH(0)
536
+
537
+ row_tb.set(HeaderFilesTOI.UC, r.get(HeaderEntregaTOI.UC))
538
+ row_tb.set(HeaderFilesTOI.TOI, r.get(HeaderEntregaTOI.TOI))
539
+ row_tb.set(HeaderFilesTOI.DATE, r.get(HeaderEntregaTOI.DATE))
540
+ row_tb.set(HeaderFilesTOI.DAY, r.get(HeaderEntregaTOI.DAY))
541
+ row_tb.set(HeaderFilesTOI.MON, r.get(HeaderEntregaTOI.MON))
542
+ row_tb.set(HeaderFilesTOI.YEAR, r.get(HeaderEntregaTOI.YEAR))
543
+ row_tb.set(HeaderFilesTOI.HASH_FILE, r.get(HeaderEntregaTOI.HASH_FILE))
544
+
545
+ row_tb.set(HeaderFilesTOI.LOCAL_ENTREGA, LocalEntregaCarta.OUTRO)
546
+ row_tb.set(HeaderFilesTOI.TIPO_DOC, TipoCarta.CARTA_CALCULO)
547
+
548
+ # Incluir colunas adicionais.
549
+ _hash_file = r.get(HeaderEntregaTOI.HASH_FILE)
550
+ path = data.get_value(_hash_file)
551
+
552
+ row_tb.set(HeaderFilesTOI.FILE_BASENAME, path.basename())
553
+ row_tb.set(
554
+ HeaderFilesTOI.FILE_EXTENSION,
555
+ path.extension() or ".nan",
556
+ )
557
+ row_tb.set(HeaderFilesTOI.FULL_PATH, path.absolute())
558
+
559
+ row_tb.set(HeaderFilesTOI.SRC, path.parent().absolute())
560
+ row_tb.set(HeaderFilesTOI.IS_FILE, "S" if path.is_file() else "N")
561
+
562
+ final_tb.add_row(row_tb)
563
+
564
+ if final_tb.is_empty():
565
+ return None
566
+
567
+ _final_df = final_tb.to_dataframe().drop_duplicates(
568
+ subset=[HeaderFilesTOI.HASH_FILE.value]
569
+ )
570
+ return self.from_data(_final_df)
571
+
572
+ def from_path_name(
573
+ self, path: File, md5: hashFile = None
574
+ ) -> TableData[HeaderFilesTOI] | None:
575
+
576
+ if md5 is None:
577
+ md5 = load_file_hash(path)
578
+
579
+ tb_toi = self.get_factory_toi().from_path_name(path, md5)
580
+ if tb_toi is None:
581
+ return None
582
+
583
+ return self.decode_table_toi(tb_toi, HashMapDict({md5: path}))
584
+
585
+ def from_hash_files(
586
+ self, data: HashMap[hashFile, File], *, ignore_null: bool = True
587
+ ) -> TableData[HeaderFilesTOI] | None:
588
+ tb_toi = self.get_factory_toi().from_hash_files(data, ignore_null=ignore_null)
589
+ if tb_toi is None:
590
+ return None
591
+
592
+ return self.decode_table_toi(tb_toi, data)
593
+
594
+
595
+ @dataclass
596
+ class IdNames:
597
+ """
598
+ Identificar os índices de cada elemento do nome de uma carta.
599
+
600
+ 2026 3 03 1488594 193258476 e 1488594 193258476 03 03 2026 são
601
+
602
+ a mesma coisa porém nomeado com datas diferentes.
603
+ """
604
+
605
+ year: int = 0
606
+ mon: int = 1
607
+ day: int = 2
608
+ uc: int = 3
609
+ toi: int = 4
610
+
611
+
612
+ def create_table_from_ids(
613
+ filename: str, md5: hashFile, *, ids: IdNames = IdNames()
614
+ ) -> TableData[HeaderEntregaTOI]:
615
+
616
+ # Separar os nomes em uma lista
617
+ list_name: list[str] = filename.split(" ")
618
+
619
+ # Gerar a data no formato dia mês ano.
620
+ dt = f"{list_name[ids.day]} {list_name[ids.mon]} {list_name[ids.year]}"
621
+
622
+ row = RowH[HeaderEntregaTOI, str](0)
623
+ row.set(HeaderEntregaTOI.HASH_FILE, md5)
624
+ row.set(HeaderEntregaTOI.UC, list_name[ids.uc])
625
+ row.set(HeaderEntregaTOI.TOI, list_name[ids.toi])
626
+ row.set(HeaderEntregaTOI.YEAR, list_name[ids.year])
627
+ row.set(HeaderEntregaTOI.MON, list_name[ids.mon])
628
+ row.set(HeaderEntregaTOI.DAY, list_name[ids.day])
629
+ row.set(HeaderEntregaTOI.DATE, dt)
630
+ tb = BuilderEntregaTOI().create_void()
631
+ tb.add_row(row)
632
+ return tb
633
+
634
+
635
+ # ========================================================#
636
+ # Relatórios TOI.
637
+ # ========================================================#
638
+
639
+
640
+ class ReadWriteTableTOI(object):
641
+ """
642
+ Leitura e escrita de dados na TOI em cache. A tabela em cache contém
643
+ o cabeçalho HeaderEntregaTOI:
644
+ UC, TOI, DAY, MON, YEAR, DATE E HASH_FILE.
645
+
646
+ """
647
+
648
+ _instance = None # Atributo de classe para armazenar a instância singleton
649
+
650
+ def __new__(cls, *args, **kwargs):
651
+ if cls._instance is None:
652
+ cls._instance = super(ReadWriteTableTOI, cls).__new__(cls)
653
+ return cls._instance
654
+
655
+ def __init__(self) -> None:
656
+
657
+ # Garante que __init__ não será executado mais de uma vez
658
+ if hasattr(self, "_initialized") and self._initialized:
659
+ return
660
+ super().__init__()
661
+
662
+ self._initialized: bool = True
663
+ self.builder: BuilderConfigTOI = BuilderConfigTOI()
664
+ self._tb_toi: TableData[HeaderEntregaTOI] = None
665
+
666
+ @property
667
+ def file_table_toi(self) -> File:
668
+ return BuilderConfigTOI().builder().file_table_toi
669
+
670
+ def set_table_toi(self, tb: TableData[HeaderEntregaTOI]):
671
+ self._tb_toi = tb
672
+
673
+ def get_table_toi(self) -> TableData[HeaderEntregaTOI]:
674
+ if self._tb_toi is not None:
675
+ return self._tb_toi
676
+
677
+ f = self.file_table_toi
678
+ if not f.exists():
679
+ self._tb_toi = BuilderTableFilesTOI().get_factory_toi().create_void()
680
+ return self._tb_toi
681
+
682
+ print(f"Lendo tabela TOI em cache ... {f.absolute()}")
683
+ data = pd.read_excel(f.absolute())
684
+ data = data.drop_duplicates(subset=[HeaderFilesTOI.HASH_FILE.value])
685
+
686
+ # self._tb_toi = TableTOI.create_from_data(data)
687
+ self._tb_toi = BuilderEntregaTOI().from_data(data)
688
+ return self._tb_toi
689
+
690
+ def merge(self, tb: TableData[HeaderEntregaTOI]) -> None:
691
+ if tb.is_empty():
692
+ return None
693
+
694
+ print(f"Mergeando tabela TOI ... {self.file_table_toi.absolute()}")
695
+ tb_data = tb.to_dataframe().drop_duplicates(subset=[HeaderEntregaTOI.HASH_FILE])
696
+
697
+ content_data = (
698
+ self.get_table_toi()
699
+ .to_dataframe()
700
+ .drop_duplicates(subset=[HeaderEntregaTOI.HASH_FILE])
701
+ )
702
+
703
+ final = list()
704
+ if not tb_data.empty:
705
+ final.append(tb_data)
706
+ if not content_data.empty:
707
+ final.append(content_data)
708
+
709
+ if len(final) > 0:
710
+ df: pd.DataFrame = pd.concat(final)
711
+ self._tb_toi = BuilderTableFilesTOI().get_factory_toi().from_data(df)
712
+ self.save()
713
+
714
+ def save(self) -> None:
715
+ data = self.get_table_toi().to_dataframe()
716
+ path = self.file_table_toi.absolute()
717
+ tmp_file = f"{path}-tmp.xlsx"
718
+
719
+ print(f"Salvando tabela TOI ... {path}")
720
+ data.to_excel(tmp_file, index=False)
721
+ rename_file(tmp_file, path)
722
+
723
+
724
+ class ControllerTOI(object):
725
+ """
726
+ Cria uma tabela de Cartas TOI apartir dos chats do WhatsApp.
727
+ Conectando os arquivos das conversas aos textos extraídos via OCR.
728
+
729
+ TableWhatsApp() é usado para obter DATA e HORA do envio de um arquivo
730
+ no chat, pois a data de envio no chat e a data de entrega de uma carta em campo.
731
+ """
732
+
733
+ def __init__(self) -> None:
734
+ self.builder_toi_files: BuilderTableFilesTOI = BuilderTableFilesTOI()
735
+ self.builder_config: BuilderConfigTOI = BuilderConfigTOI()
736
+ self.rw: ReadWriteTableTOI = ReadWriteTableTOI()
737
+
738
+ @property
739
+ def relatorio_whatsapp(self) -> ControllerWhatsApp:
740
+ return self.get_factory_toi().relatorio_wp
741
+
742
+ @property
743
+ def relatorio_ocr(self) -> CreateRelatorioOcr:
744
+ return self.get_factory_toi().relatorio_ocr
745
+
746
+ def get_factory_toi(self) -> BuilderEntregaTOI:
747
+ return self.builder_toi_files.get_factory_toi()
748
+
749
+ # --------------------------------------------------#
750
+ # Consultar dados - READ
751
+ # --------------------------------------------------#
752
+ def load_table_whatsapp(self) -> TableWhatsApp:
753
+ return self.relatorio_whatsapp.load_cache_table()
754
+
755
+ def load_table_toi(self) -> TableData[HeaderEntregaTOI]:
756
+ return self.rw.get_table_toi()
757
+
758
+ def find_on_cache(self, data: HashMap[hashFile, File]) -> TableData[HeaderFilesTOI]:
759
+ """
760
+ Procura os textos de cada arquivo na tabela OCR apartir da hash e retorna
761
+ os dados no formato TableData()
762
+ """
763
+
764
+ if data.is_empty():
765
+ return self.builder_toi_files.create_void()
766
+
767
+ tb_cache: TableData[HeaderEntregaTOI] = self.load_table_toi()
768
+ if tb_cache.is_empty():
769
+ return self.builder_toi_files.create_void()
770
+
771
+ content_cache: TableData | None = tb_cache.search_all(
772
+ HeaderEntregaTOI.HASH_FILE, data.header()
773
+ )
774
+
775
+ if content_cache is None:
776
+ return self.builder_toi_files.create_void()
777
+
778
+ df_cache = content_cache.to_dataframe()
779
+ builder = BuilderEntregaTOI()
780
+
781
+ final = self.builder_toi_files.decode_table_toi(
782
+ builder.from_data(df_cache), data
783
+ )
784
+
785
+ if final is None:
786
+ return self.builder_toi_files.create_void()
787
+ return final
788
+
789
+ def load_from_map(
790
+ self, data_map: HashMapDict[hashFile, File], *, ignore_null: bool = True
791
+ ) -> TableData[HeaderFilesTOI] | None:
792
+ """
793
+ I) Usa a HASH MD5 dos arquivos para buscar a data de envio do arquivo na
794
+ tabela whatsapp;
795
+
796
+ II) Gera um relatório OCR com os metadados básicos
797
+ de cada arquivo;
798
+
799
+ III) Obtem a UC e TOI de cada arquivo.
800
+
801
+ IV) Retorna uma tabela apartir dos dados gerados.
802
+
803
+ Observação:
804
+ Se o parametro ignore_null for True as linhas que contém UC e TOI inválidos
805
+ serão ignorados.
806
+ """
807
+
808
+ if data_map.is_empty():
809
+ return None
810
+
811
+ final_table: TableData[HeaderFilesTOI] | None = (
812
+ self.builder_toi_files.from_hash_files(data_map, ignore_null=ignore_null)
813
+ )
814
+ if final_table is None:
815
+ return None
816
+
817
+ df = final_table.to_dataframe().drop_duplicates(
818
+ subset=[HeaderFilesTOI.HASH_FILE.value]
819
+ )
820
+
821
+ if ignore_null:
822
+ df = df[df[HeaderFilesTOI.UC] != TableData.NULL_STRING]
823
+
824
+ return self.builder_toi_files.from_data(df)
825
+
826
+ # --------------------------------------------------#
827
+ # Criar dados - CREATE
828
+ # --------------------------------------------------#
829
+ def add_hashs_on_table(
830
+ self, hash_files: Set[hashFile], *, ignore_null: bool = True
831
+ ) -> None:
832
+ """
833
+ I) Analisa arquivos TOI apartir da HASH MD5, pesquisa o texto de cada
834
+ hash na tabela OCR e extrai UC e TOI;
835
+
836
+ II) Na sequência, usa a hash para encontrar a data de envio do arquivo no chat
837
+ do whatsapp;
838
+
839
+ III) Inclui os dados TOI de cada arquivo na tabela TOI em cache.
840
+ """
841
+ if hash_files.empty():
842
+ print("Lista de arquivos vazia, nada a fazer!")
843
+ return None
844
+
845
+ # Filtrar
846
+ tb_cache_ocr: TableData[HeaderOcr] = self.relatorio_ocr.load_table_image()
847
+ tb_cache_ocr.merge(self.relatorio_ocr.load_table_pdf())
848
+
849
+ cache_ocr: TableData[HeaderOcr] | None = tb_cache_ocr.search_all(
850
+ HeaderOcr.HASH_FILE, hash_files.tolist()
851
+ )
852
+
853
+ if cache_ocr is None:
854
+ return None
855
+
856
+ # cache_ocr = cache_ocr.drop_duplicates()
857
+ table_whatsapp: TableWhatsApp = self.load_table_whatsapp()
858
+
859
+ # Cada arquivo gera/extrai a sua própria tabela, ao final essa
860
+ # lista de tabelas serão concatenadas em uma tabela única.
861
+ list_tables_toi = ArrayTables[TableData[HeaderEntregaTOI]]()
862
+ total = hash_files.size()
863
+ k: hashFile
864
+
865
+ print(f"[Pesquisando texto OCR] em {total} arquivos")
866
+ for n, k in enumerate(hash_files):
867
+
868
+ current_table_ocr: TableData[HeaderOcr] = cache_ocr.search(
869
+ HeaderOcr.HASH_FILE, k
870
+ )
871
+
872
+ text_ocr: str = None
873
+
874
+ if not current_table_ocr.is_empty():
875
+ text_ocr = current_table_ocr.to_string(HeaderOcr.TEXT)
876
+
877
+ txt_uc_toi: tuple | None = (
878
+ extrair_uc_toi(text_ocr) if text_ocr is not None else None
879
+ )
880
+
881
+ if txt_uc_toi is None:
882
+ if ignore_null:
883
+ continue
884
+
885
+ uc = TableData.NULL_STRING
886
+ toi = TableData.NULL_STRING
887
+ year = TableData.NULL_STRING
888
+ mon = TableData.NULL_STRING
889
+ day = TableData.NULL_STRING
890
+ dt: str = TableData.NULL_STRING
891
+
892
+ if txt_uc_toi is not None:
893
+ uc = txt_uc_toi[0]
894
+ toi = txt_uc_toi[1]
895
+
896
+ # Obter os dados no arquivo no chat whatsapp apartir da hash md5
897
+ current_table_whatsapp = table_whatsapp.search(HeaderWhatsApp.hash_md5, k)
898
+
899
+ if not current_table_whatsapp.is_empty():
900
+ # Arquivo encontrado no chat. Extrair dados da data de envio.
901
+ dt = current_table_whatsapp.get_row_at(0).get_date().get_first()
902
+ day = dt.split(" ")[0]
903
+ mon = dt.split(" ")[1]
904
+ year = dt.split(" ")[2]
905
+
906
+ if ignore_null:
907
+ if dt == TableData.NULL_STRING:
908
+ continue
909
+
910
+ tb_toi: TableData[HeaderEntregaTOI] = self.get_factory_toi().create_void()
911
+ row_forms = RowH[HeaderEntregaTOI, Any](0)
912
+
913
+ row_forms.set(HeaderEntregaTOI.UC, uc)
914
+ row_forms.set(HeaderEntregaTOI.TOI, toi)
915
+ row_forms.set(HeaderEntregaTOI.YEAR, year)
916
+ row_forms.set(HeaderEntregaTOI.MON, mon)
917
+ row_forms.set(HeaderEntregaTOI.DAY, day)
918
+ row_forms.set(HeaderEntregaTOI.DATE, dt)
919
+ row_forms.set(HeaderEntregaTOI.HASH_FILE, k)
920
+
921
+ tb_toi.add_row(row_forms)
922
+ list_tables_toi.add(tb_toi)
923
+
924
+ if list_tables_toi.empty():
925
+ return None
926
+
927
+ final = list_tables_toi.concat(TableData).drop_duplicates()
928
+ self.rw.merge(final)
929
+
930
+ @staticmethod
931
+ def filter_uc_toi(
932
+ hf: hashFile, table_ocr_file: TableData[HeaderOcr]
933
+ ) -> tuple[str, str] | None:
934
+ """
935
+ Filtra UC e TOI de uma carta apartir do texto OCR extraído.
936
+ """
937
+ tb_current_hash = table_ocr_file.search(HeaderOcr.HASH_FILE, hf)
938
+ if tb_current_hash.is_empty():
939
+ return None
940
+
941
+ values = tb_current_hash.get(HeaderOcr.TEXT)._elements
942
+
943
+ for v in values:
944
+ txt = extrair_uc_toi(v)
945
+ if txt is not None:
946
+ return txt
947
+ return None
948
+
949
+
950
+ def load_files_toi(files: Set[File]) -> TableData[HeaderFilesTOI] | None:
951
+ """
952
+ I) Analisa arquivos TOI apartir da HASH MD5;
953
+ II) Compara o texto na tabela OCR e inclui, UC e TOI na tabela TOI principal;
954
+ III) Retorna os dados no formato de tabela TOI.
955
+ """
956
+
957
+ if files.empty():
958
+ print(f"[DEBUG]: load_files_toi() lista de arquivos vazia!")
959
+ return None
960
+
961
+ f: File
962
+ for f in files:
963
+ if (not f.is_image()) and (not f.is_pdf()):
964
+ raise ValueError(
965
+ f"Tipo de documento TOI inválido, use PDF ou Imagem não: {f.extension()}"
966
+ )
967
+
968
+ data_map = HashMapDict[hashFile, File]()
969
+ files.for_each(lambda f: data_map.set_value(load_file_hash(f), f))
970
+
971
+ controller = ControllerTOI()
972
+ heder_hash = data_map.header()
973
+ controller.add_hashs_on_table(heder_hash.to_set())
974
+ return controller.find_on_cache(data_map)
975
+
976
+
977
+ def get_file_log(base_dir: Directory) -> File:
978
+ _dt = datetime.now()
979
+ y = str(_dt.year)
980
+ m = str(_dt.month)
981
+ day = str(_dt.day)
982
+ current_date = f"{y}-{m}-{day}"
983
+ dir_log: Directory = base_dir.concat(y, True).concat(f"{y}-{m}", True)
984
+ return dir_log.join_file(f"{current_date}.xlsx")
985
+
986
+
987
+ def get_date_from_row(r: RowH[HeaderFilesTOI, Any]) -> tuple[str, str, str] | None:
988
+ """
989
+ Retorna uma tupla de data no formato (ANO, MÊS, DIA)
990
+ """
991
+
992
+ y = r.get(HeaderFilesTOI.YEAR)
993
+ m = r.get(HeaderFilesTOI.MON)
994
+ d = r.get(HeaderFilesTOI.DAY)
995
+
996
+ if len(m) > 1:
997
+ if m[0] == "0":
998
+ m = m[1]
999
+
1000
+ values = (y, m, d)
1001
+ i: str
1002
+ for i in values:
1003
+ if not i.isnumeric():
1004
+ return None
1005
+ return values
1006
+
1007
+
1008
+ class RenameFilesTOI:
1009
+ """Organiza cartas TOI e registra o resultado de cada movimentação."""
1010
+
1011
+ def __init__(self, files: Set[File]) -> None:
1012
+ self.config: ConfigTOI = BuilderConfigTOI().builder()
1013
+ self.files = files
1014
+ self.file_log: File = get_file_log(self.config.dir_log_excel)
1015
+
1016
+ self.base_cartas: Directory = self.config.dir_base_toi
1017
+ self.error_uc_dir: Directory = self.base_cartas.concat(
1018
+ "CARTAS_UC_NAO_IDENTIFICADAS", True
1019
+ )
1020
+ self.error_date_dir: Directory = self.base_cartas.concat(
1021
+ "CARTAS_DATA_NAO_ENCONTRADA", True
1022
+ )
1023
+
1024
+ self.map_files: HashMapDict[hashFile, File] = HashMapDict()
1025
+ self.table_status = TableData.create_void(
1026
+ [HeaderFilesTOI.HASH_FILE.value, "src", "dest", "status", "exception"]
1027
+ )
1028
+
1029
+ def process_error_ucs(self):
1030
+ tb_wp = ControllerWhatsApp().load_cache_table()
1031
+ content = ContentFiles(self.error_uc_dir)
1032
+ files = content.get_files()
1033
+
1034
+ content_map = HashMapDict[hashFile, File]()
1035
+ files.for_each(lambda f: content_map.set_value(load_file_hash(f), f))
1036
+
1037
+ content_chat = tb_wp.search_all(HeaderWhatsApp.hash_md5, content_map.header())
1038
+
1039
+ if content_chat is None:
1040
+ return
1041
+ if content_chat.is_empty():
1042
+ return
1043
+
1044
+ rows = (
1045
+ BuilderTableWhatsApp().create_from_data(content_chat.to_dataframe()).rows()
1046
+ )
1047
+
1048
+ r: RowWhatsApp
1049
+ for r in rows:
1050
+ h = r.get(HeaderWhatsApp.hash_md5)
1051
+ file_path: File = content_map.get_value(h)
1052
+
1053
+ dt: datetime = r.get_date_hour()
1054
+ y = str(dt.year)
1055
+ m = str(dt.month)
1056
+ d = str(dt.day)
1057
+ _date = f"{y}-{m}-{d}"
1058
+
1059
+ current_dir = self.error_uc_dir.concat(y).concat(m).concat(_date, True)
1060
+ final_path: File = current_dir.join_file(file_path.basename())
1061
+
1062
+ try:
1063
+ move_path(file_path, final_path)
1064
+ except Exception as e:
1065
+ print(f"{__class__.__name__}(): {e}")
1066
+
1067
+ def _index_files(self) -> None:
1068
+ """Indexa os arquivos originais por hash."""
1069
+ for file in self.files:
1070
+ self.map_files.set_value(load_file_hash(file), file)
1071
+
1072
+ def _create_status_row(self, file_hash: hashFile, src: File, dest: File) -> RowH:
1073
+ row = RowH(0)
1074
+ row.set(HeaderFilesTOI.HASH_FILE.value, file_hash)
1075
+ row.set("src", src.absolute())
1076
+ row.set("dest", dest.absolute())
1077
+ return row
1078
+
1079
+ def _move(self, file_hash: hashFile, src: File, dest: File) -> bool:
1080
+ """Move o arquivo e registra o resultado real da operação."""
1081
+ row = self._create_status_row(file_hash, src, dest)
1082
+
1083
+ try:
1084
+ if not move_path(src, dest):
1085
+ raise OSError(
1086
+ f"move_path retornou False: {src.absolute()} -> {dest.absolute()}"
1087
+ )
1088
+ except Exception as exc:
1089
+ row.set("status", "FALHA")
1090
+ row.set("exception", str(exc))
1091
+ success = False
1092
+ else:
1093
+ row.set("status", "SUCESSO")
1094
+ row.set("exception", "")
1095
+ success = True
1096
+
1097
+ self.table_status.add_row(row)
1098
+ return success
1099
+
1100
+ @staticmethod
1101
+ def _is_missing(value: Any) -> bool:
1102
+ return (
1103
+ value is None
1104
+ or pd.isna(value)
1105
+ or value == TableData.NULL_STRING
1106
+ or str(value).strip() == ""
1107
+ )
1108
+
1109
+ def _get_destination(self, row: RowH) -> tuple[Directory, str]:
1110
+ """Define o destino com prioridade para a ausência de UC."""
1111
+ uc = row.get(HeaderFilesTOI.UC)
1112
+ toi = row.get(HeaderFilesTOI.TOI)
1113
+ date = get_date_from_row(row)
1114
+
1115
+ if self._is_missing(uc):
1116
+ return self.error_uc_dir, str(row.get(HeaderFilesTOI.HASH_FILE))
1117
+
1118
+ if date is None:
1119
+ return self.error_date_dir, f"{uc} {toi}"
1120
+
1121
+ year, month, day = date
1122
+ dest_dir = (
1123
+ self.base_cartas.concat(year)
1124
+ .concat(f"{year}-{month}")
1125
+ .concat(f"{year}-{month}-{day}", True)
1126
+ )
1127
+ return dest_dir, f"{year} {month} {day} {uc} {toi}"
1128
+
1129
+ def _process_identified_files(self) -> Set[hashFile]:
1130
+ """Processa os arquivos que possuem registro na tabela TOI."""
1131
+ controller = ControllerTOI()
1132
+ controller.add_hashs_on_table(self.map_files.header().to_set())
1133
+ table_files = controller.find_on_cache(self.map_files)
1134
+
1135
+ processed = Set[hashFile]()
1136
+
1137
+ for row in table_files.rows():
1138
+ file_hash: hashFile = row.get(HeaderFilesTOI.HASH_FILE)
1139
+ src: File = self.map_files.get_value(file_hash)
1140
+
1141
+ if src is None:
1142
+ continue
1143
+
1144
+ processed.add(file_hash)
1145
+ dest_dir, output_name = self._get_destination(row)
1146
+ dest = dest_dir.join_file(f"{output_name}{src.extension()}")
1147
+
1148
+ self._move(file_hash, src, dest)
1149
+
1150
+ return processed
1151
+
1152
+ def _process_unidentified_files(self, processed: Set[hashFile]) -> None:
1153
+ """Encaminha para a pasta de UC os arquivos sem registro TOI."""
1154
+ for file_hash in self.map_files.header():
1155
+ if processed.contains(file_hash):
1156
+ continue
1157
+
1158
+ src: File = self.map_files.get_value(file_hash)
1159
+ extension = src.extension() or ".nan"
1160
+ dest = self.error_uc_dir.join_file(f"{file_hash}{extension}")
1161
+
1162
+ self._move(file_hash, src, dest)
1163
+
1164
+ def _save_log(self) -> pd.DataFrame:
1165
+ """Acumula os resultados no relatório existente."""
1166
+ sequence = ArrayTables[TableData]()
1167
+ sequence.add(self.table_status)
1168
+
1169
+ if self.file_log.exists():
1170
+ df_previous = pd.read_excel(self.file_log.absolute())
1171
+ sequence.add(TableData(df_previous))
1172
+
1173
+ df_final = sequence.concat(TableData).to_dataframe()
1174
+ df_final.drop_duplicates(inplace=True)
1175
+
1176
+ print(f"Salvando: {self.file_log}")
1177
+ df_final.to_excel(self.file_log.absolute(), index=False)
1178
+ return df_final
1179
+
1180
+ def execute(self) -> pd.DataFrame:
1181
+ if self.files.empty():
1182
+ return self.table_status.to_dataframe()
1183
+
1184
+ self._index_files()
1185
+ processed = self._process_identified_files()
1186
+ self._process_unidentified_files(processed)
1187
+ return self._save_log()
1188
+
1189
+
1190
+ def rename_files_toi(files: Set[File]) -> RenameFilesTOI:
1191
+ """Renomear arquivos TOI"""
1192
+
1193
+ rn = RenameFilesTOI(files)
1194
+ rn.execute()
1195
+ rn.process_error_ucs()
1196
+ return rn