caarleexx commited on
Commit
872702a
·
verified ·
1 Parent(s): 4a7e26d

Upload d3f0e49f.py

Browse files
Files changed (1) hide show
  1. api/services/d3f0e49f.py +391 -0
api/services/d3f0e49f.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Serviço de validação de dados para para.AI API v3.0
3
+ Valida formato de acórdãos, arquivos JSONL e outros dados
4
+ """
5
+ from typing import Dict, List, Optional, Any, Tuple
6
+ import json
7
+ import re
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from api.utils.exceptions import ValidationError
12
+ from api.utils.logger import setup_logger
13
+
14
+ logger = setup_logger(__name__)
15
+
16
+
17
+ class ValidationService:
18
+ """
19
+ Serviço centralizado de validação de dados.
20
+
21
+ Valida:
22
+ - Arquivos JSONL
23
+ - Registros de acórdãos
24
+ - Dados de upload
25
+ - Formatos de campo
26
+ """
27
+
28
+ # ========================================================================
29
+ # CONSTANTES
30
+ # ========================================================================
31
+
32
+ # Campos obrigatórios em acórdãos
33
+ REQUIRED_FIELDS = ["ementa", "integra"]
34
+
35
+ # Campos opcionais mas recomendados
36
+ OPTIONAL_FIELDS = [
37
+ "acordao_id", "numero", "tribunal", "ano", "data_julgamento",
38
+ "relator", "orgao_julgador", "tipo_decisao", "classe_processual"
39
+ ]
40
+
41
+ # Siglas válidas de tribunais (principais)
42
+ VALID_TRIBUNAIS = [
43
+ "STF", "STJ", "TST", "TSE", "STM", # Superiores
44
+ "TRF1", "TRF2", "TRF3", "TRF4", "TRF5", "TRF6", # Federais
45
+ "TJAC", "TJAL", "TJAP", "TJAM", "TJBA", "TJCE", "TJDF", "TJES",
46
+ "TJGO", "TJMA", "TJMT", "TJMS", "TJMG", "TJPA", "TJPB", "TJPR",
47
+ "TJPE", "TJPI", "TJRJ", "TJRN", "TJRS", "TJRO", "TJRR", "TJSC",
48
+ "TJSP", "TJSE", "TJTO" # Estaduais
49
+ ]
50
+
51
+ # Tamanhos mínimos de texto
52
+ MIN_EMENTA_LENGTH = 50
53
+ MIN_INTEGRA_LENGTH = 100
54
+
55
+ # Tamanhos máximos de texto
56
+ MAX_EMENTA_LENGTH = 50000 # ~50KB
57
+ MAX_INTEGRA_LENGTH = 500000 # ~500KB
58
+
59
+ # Padrões regex
60
+ PATTERN_NUMERO_PROCESSO = re.compile(
61
+ r'^\d{7}-\d{2}\.\d{4}\.\d{1}\.\d{2}\.\d{4}$' # CNJ
62
+ )
63
+ PATTERN_ANO = re.compile(r'^(19|20)\d{2}$')
64
+ PATTERN_DATA = re.compile(r'^\d{4}-\d{2}-\d{2}')
65
+
66
+ # ========================================================================
67
+ # VALIDAÇÃO DE ARQUIVO JSONL
68
+ # ========================================================================
69
+
70
+ def validate_jsonl_file(
71
+ self,
72
+ file_path: str,
73
+ max_size_mb: int = 500
74
+ ) -> Tuple[bool, Optional[str], Dict[str, Any]]:
75
+ """
76
+ Valida arquivo JSONL completo.
77
+
78
+ Args:
79
+ file_path: Caminho do arquivo
80
+ max_size_mb: Tamanho máximo permitido em MB
81
+
82
+ Returns:
83
+ Tuple[bool, Optional[str], Dict]: (is_valid, error_message, metadata)
84
+ """
85
+ try:
86
+ path = Path(file_path)
87
+
88
+ # Verificar se existe
89
+ if not path.exists():
90
+ return False, f"Arquivo não encontrado: {file_path}", {}
91
+
92
+ # Verificar extensão
93
+ if path.suffix.lower() not in ['.jsonl', '.json']:
94
+ return False, "Arquivo deve ter extensão .jsonl ou .json", {}
95
+
96
+ # Verificar tamanho
97
+ size_mb = path.stat().st_size / (1024 * 1024)
98
+ if size_mb > max_size_mb:
99
+ return False, f"Arquivo muito grande: {size_mb:.2f}MB (máx: {max_size_mb}MB)", {}
100
+
101
+ # Validar conteúdo
102
+ with open(file_path, 'r', encoding='utf-8') as f:
103
+ content = f.read()
104
+
105
+ valid, error, metadata = self.validate_jsonl_content(content)
106
+ metadata['file_size_mb'] = size_mb
107
+ metadata['file_path'] = str(path)
108
+
109
+ return valid, error, metadata
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error validating JSONL file: {e}")
113
+ return False, f"Erro ao validar arquivo: {str(e)}", {}
114
+
115
+ def validate_jsonl_content(
116
+ self,
117
+ content: str
118
+ ) -> Tuple[bool, Optional[str], Dict[str, Any]]:
119
+ """
120
+ Valida conteúdo JSONL (string).
121
+
122
+ Args:
123
+ content: Conteúdo do arquivo JSONL
124
+
125
+ Returns:
126
+ Tuple[bool, Optional[str], Dict]: (is_valid, error_message, metadata)
127
+ """
128
+ try:
129
+ lines = content.strip().split('\n')
130
+ total_lines = len(lines)
131
+ valid_records = 0
132
+ invalid_records = 0
133
+ errors = []
134
+
135
+ for line_num, line in enumerate(lines, 1):
136
+ # Pular linhas vazias
137
+ if not line.strip():
138
+ continue
139
+
140
+ # Tentar fazer parse JSON
141
+ try:
142
+ record = json.loads(line)
143
+ except json.JSONDecodeError as e:
144
+ invalid_records += 1
145
+ error_msg = f"Linha {line_num}: JSON inválido - {str(e)}"
146
+ errors.append(error_msg)
147
+ if len(errors) <= 5: # Limitar erros reportados
148
+ logger.warning(error_msg)
149
+ continue
150
+
151
+ # Validar estrutura do registro
152
+ is_valid, error = self.validate_acordao_record(record)
153
+
154
+ if is_valid:
155
+ valid_records += 1
156
+ else:
157
+ invalid_records += 1
158
+ error_msg = f"Linha {line_num}: {error}"
159
+ errors.append(error_msg)
160
+ if len(errors) <= 5:
161
+ logger.warning(error_msg)
162
+
163
+ # Verificar se tem pelo menos 1 registro válido
164
+ if valid_records == 0:
165
+ return False, "Nenhum registro válido encontrado", {
166
+ "total_lines": total_lines,
167
+ "valid_records": 0,
168
+ "invalid_records": invalid_records,
169
+ "errors": errors[:10] # Primeiros 10 erros
170
+ }
171
+
172
+ # Se mais de 50% inválidos, considerar arquivo inválido
173
+ if invalid_records > valid_records:
174
+ return False, f"Muitos registros inválidos ({invalid_records}/{total_lines})", {
175
+ "total_lines": total_lines,
176
+ "valid_records": valid_records,
177
+ "invalid_records": invalid_records,
178
+ "errors": errors[:10]
179
+ }
180
+
181
+ metadata = {
182
+ "total_lines": total_lines,
183
+ "valid_records": valid_records,
184
+ "invalid_records": invalid_records,
185
+ "success_rate": (valid_records / (valid_records + invalid_records)) * 100
186
+ }
187
+
188
+ if errors:
189
+ metadata["errors"] = errors[:10] # Primeiros 10 erros
190
+
191
+ return True, None, metadata
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error validating JSONL content: {e}")
195
+ return False, f"Erro ao validar conteúdo: {str(e)}", {}
196
+
197
+ # ========================================================================
198
+ # VALIDAÇÃO DE REGISTRO DE ACÓRDÃO
199
+ # ========================================================================
200
+
201
+ def validate_acordao_record(
202
+ self,
203
+ record: Dict[str, Any]
204
+ ) -> Tuple[bool, Optional[str]]:
205
+ """
206
+ Valida um registro de acórdão individual.
207
+
208
+ Args:
209
+ record: Dicionário com dados do acórdão
210
+
211
+ Returns:
212
+ Tuple[bool, Optional[str]]: (is_valid, error_message)
213
+ """
214
+ # Verificar se é dict
215
+ if not isinstance(record, dict):
216
+ return False, "Registro deve ser um objeto JSON"
217
+
218
+ # Verificar campos obrigatórios
219
+ for field in self.REQUIRED_FIELDS:
220
+ if field not in record:
221
+ return False, f"Campo obrigatório ausente: '{field}'"
222
+
223
+ if not record[field] or not str(record[field]).strip():
224
+ return False, f"Campo obrigatório vazio: '{field}'"
225
+
226
+ # Validar ementa
227
+ ementa = str(record.get("ementa", "")).strip()
228
+ if len(ementa) < self.MIN_EMENTA_LENGTH:
229
+ return False, f"Ementa muito curta (mín: {self.MIN_EMENTA_LENGTH} chars)"
230
+
231
+ if len(ementa) > self.MAX_EMENTA_LENGTH:
232
+ return False, f"Ementa muito longa (máx: {self.MAX_EMENTA_LENGTH} chars)"
233
+
234
+ # Validar integra
235
+ integra = str(record.get("integra", "")).strip()
236
+ if len(integra) < self.MIN_INTEGRA_LENGTH:
237
+ return False, f"Íntegra muito curta (mín: {self.MIN_INTEGRA_LENGTH} chars)"
238
+
239
+ if len(integra) > self.MAX_INTEGRA_LENGTH:
240
+ return False, f"Íntegra muito longa (máx: {self.MAX_INTEGRA_LENGTH} chars)"
241
+
242
+ # Validar campos opcionais (se presentes)
243
+ if "tribunal" in record:
244
+ tribunal = str(record["tribunal"]).upper().strip()
245
+ if tribunal and tribunal not in self.VALID_TRIBUNAIS:
246
+ logger.warning(f"Tribunal não reconhecido: {tribunal}")
247
+
248
+ if "numero" in record and record["numero"]:
249
+ numero = str(record["numero"]).strip()
250
+ if not self.PATTERN_NUMERO_PROCESSO.match(numero):
251
+ logger.warning(f"Número de processo não segue padrão CNJ: {numero}")
252
+
253
+ if "ano" in record and record["ano"]:
254
+ ano = str(record["ano"]).strip()
255
+ if not self.PATTERN_ANO.match(ano):
256
+ return False, f"Ano inválido: {ano}"
257
+
258
+ return True, None
259
+
260
+ # ========================================================================
261
+ # VALIDAÇÃO DE CAMPOS ESPECÍFICOS
262
+ # ========================================================================
263
+
264
+ def validate_tribunal(self, sigla: str) -> bool:
265
+ """Valida sigla de tribunal."""
266
+ return sigla.upper().strip() in self.VALID_TRIBUNAIS
267
+
268
+ def validate_numero_processo(self, numero: str) -> bool:
269
+ """Valida número de processo (padrão CNJ)."""
270
+ return bool(self.PATTERN_NUMERO_PROCESSO.match(numero.strip()))
271
+
272
+ def validate_ano(self, ano: str) -> bool:
273
+ """Valida ano (1900-2099)."""
274
+ return bool(self.PATTERN_ANO.match(str(ano).strip()))
275
+
276
+ def validate_data(self, data: str) -> bool:
277
+ """Valida data (formato ISO: YYYY-MM-DD)."""
278
+ try:
279
+ datetime.fromisoformat(data.strip())
280
+ return True
281
+ except:
282
+ return False
283
+
284
+ # ========================================================================
285
+ # SANITIZAÇÃO
286
+ # ========================================================================
287
+
288
+ def sanitize_text(self, text: str, max_length: Optional[int] = None) -> str:
289
+ """
290
+ Sanitiza texto removendo caracteres problemáticos.
291
+
292
+ Args:
293
+ text: Texto a sanitizar
294
+ max_length: Tamanho máximo (trunca se necessário)
295
+
296
+ Returns:
297
+ Texto sanitizado
298
+ """
299
+ if not text:
300
+ return ""
301
+
302
+ # Remover caracteres de controle (exceto \n, \r, \t)
303
+ text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', text)
304
+
305
+ # Normalizar espaços
306
+ text = re.sub(r'\s+', ' ', text)
307
+
308
+ # Truncar se necessário
309
+ if max_length and len(text) > max_length:
310
+ text = text[:max_length]
311
+
312
+ return text.strip()
313
+
314
+ def sanitize_record(self, record: Dict[str, Any]) -> Dict[str, Any]:
315
+ """
316
+ Sanitiza todos os campos de texto de um registro.
317
+
318
+ Args:
319
+ record: Registro a sanitizar
320
+
321
+ Returns:
322
+ Registro sanitizado
323
+ """
324
+ sanitized = {}
325
+
326
+ for key, value in record.items():
327
+ if isinstance(value, str):
328
+ if key == "ementa":
329
+ sanitized[key] = self.sanitize_text(value, self.MAX_EMENTA_LENGTH)
330
+ elif key == "integra":
331
+ sanitized[key] = self.sanitize_text(value, self.MAX_INTEGRA_LENGTH)
332
+ else:
333
+ sanitized[key] = self.sanitize_text(value, 1000)
334
+ else:
335
+ sanitized[key] = value
336
+
337
+ return sanitized
338
+
339
+ # ========================================================================
340
+ # HELPERS
341
+ # ========================================================================
342
+
343
+ def get_validation_summary(
344
+ self,
345
+ valid_count: int,
346
+ invalid_count: int,
347
+ errors: List[str]
348
+ ) -> Dict[str, Any]:
349
+ """
350
+ Gera sumário de validação.
351
+
352
+ Args:
353
+ valid_count: Número de registros válidos
354
+ invalid_count: Número de registros inválidos
355
+ errors: Lista de erros encontrados
356
+
357
+ Returns:
358
+ Dicionário com sumário
359
+ """
360
+ total = valid_count + invalid_count
361
+
362
+ return {
363
+ "total": total,
364
+ "valid": valid_count,
365
+ "invalid": invalid_count,
366
+ "success_rate": (valid_count / total * 100) if total > 0 else 0,
367
+ "errors_count": len(errors),
368
+ "errors_sample": errors[:5] if errors else []
369
+ }
370
+
371
+
372
+ # ============================================================================
373
+ # INSTÂNCIA GLOBAL
374
+ # ============================================================================
375
+
376
+ _validation_service = None
377
+
378
+
379
+ def get_validation_service() -> ValidationService:
380
+ """
381
+ Retorna instância singleton do ValidationService.
382
+
383
+ Returns:
384
+ ValidationService: Instância do serviço
385
+ """
386
+ global _validation_service
387
+
388
+ if _validation_service is None:
389
+ _validation_service = ValidationService()
390
+
391
+ return _validation_service