csc-essentials 1.2.2__tar.gz → 1.2.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csc-essentials
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: Biblioteca de utilitários essenciais para projetos CSC
5
5
  Author-email: David Braga <david.braga@stone.com.br>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csc-essentials"
7
- version = "1.2.2"
7
+ version = "1.2.4"
8
8
  description = "Biblioteca de utilitários essenciais para projetos CSC"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -25,17 +25,12 @@ def ajusta_nome_coluna(nome_coluna):
25
25
 
26
26
 
27
27
  def converter_valor(row, schema_map, gerador_log):
28
- """Converte e sanitiza os valores de uma linha segundo o `schema_map`.
29
-
30
- - Recebe `row` (dict coluna->valor) e `schema_map` (coluna->TIPO BigQuery).
31
- - Converte tipos básicos (STRING, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME).
32
- - Sanitiza NaN/Inf, strings nulas do Pandas e formata datas no padrão aceito pelo BigQuery.
33
- - Trata formatos de moeda BR/US, datas BR (DD/MM/YYYY), datetime BR e padrão MMM-AA.
34
- - Em falhas de conversão registra avisos e armazena `None` no campo.
28
+ """
29
+ Converte os valores e sanitiza para evitar erros de JSON parsing no BigQuery (NaN, Infinity).
35
30
  """
36
31
 
37
32
  if not schema_map:
38
- return row
33
+ return row
39
34
 
40
35
  nova_linha = {}
41
36
 
@@ -47,17 +42,15 @@ def converter_valor(row, schema_map, gerador_log):
47
42
  )
48
43
 
49
44
  for col, val in row.items():
50
- # Captura "nan" vindo do Pandas e demais strings que representam nulo
45
+ # MELHORIA: Capturar "nan" vindo do Pandas ou strings que representam nulo
51
46
  val_str_check = str(val).strip().lower() if val is not None else ""
52
47
  if val is None or val_str_check in ("", "nan", "none", "null", "undefined"):
53
48
  nova_linha[col] = None
54
49
  continue
55
-
56
50
  if col == "data_import":
57
51
  nova_linha[col] = val
58
52
  continue
59
53
 
60
- # Serializa estruturas complexas para JSON
61
54
  if isinstance(val, (dict, list)):
62
55
  try:
63
56
  nova_linha[col] = json.dumps(val, ensure_ascii=False)
@@ -68,9 +61,9 @@ def converter_valor(row, schema_map, gerador_log):
68
61
  tipo_bq = schema_map.get(col, "STRING")
69
62
 
70
63
  try:
71
- if tipo_bq == "STRING":
64
+ if tipo_bq == "STRING":
72
65
  # Remove quebras de linha e caracteres de controle que corrompem o JSONL
73
- val_str = str(val).replace("\n", " ").replace("\r", "").replace("\t", " ").strip()
66
+ val_str = str(val).replace('\n', ' ').replace('\r', '').replace('\t', ' ').strip()
74
67
  # Remove caracteres não-imprimíveis (ASCII < 32) exceto espaço
75
68
  val_str = "".join(ch for ch in val_str if ord(ch) >= 32)
76
69
  nova_linha[col] = val_str
@@ -80,28 +73,27 @@ def converter_valor(row, schema_map, gerador_log):
80
73
  nova_linha[col] = int(float(str(val)))
81
74
 
82
75
  elif tipo_bq in ("FLOAT", "FLOAT64", "NUMERIC"):
83
- # Limpeza inicial de moeda e espaços
84
- val_str = str(val).replace("R$", "").replace(" ", "").strip()
76
+ # 1. Limpa moeda e espaços
77
+ val_str = str(val).replace('R$', '').replace(' ', '').strip()
85
78
 
86
- # Trata traço como zero (comum em planilhas)
87
- if val_str in ("-", ""):
79
+ if val_str in ('-', ''):
88
80
  nova_linha[col] = 0.0
89
81
  else:
90
- # Lógica inteligente para vírgulas e pontos
91
- if "," in val_str and "." in val_str:
92
- # Se a vírgula vem antes do ponto (ex: 1,510,000.00) -> padrão US
93
- if val_str.find(",") < val_str.rfind("."):
94
- val_str = val_str.replace(",", "")
82
+ # 2. Lógica inteligente para vírgulas e pontos
83
+ if ',' in val_str and '.' in val_str:
84
+ # Se tem os dois, e a vírgula vem antes do ponto (ex: 1,510,000.00)
85
+ if val_str.find(',') < val_str.rfind('.'):
86
+ val_str = val_str.replace(',', '') # Padrão US: remove vírgula
95
87
  else:
96
- # Padrão BR: 1.000,00
97
- val_str = val_str.replace(".", "").replace(",", ".")
98
- elif "," in val_str:
99
- # vírgula: se parte decimal tem <= 2 dígitos trata como decimal
100
- partes = val_str.split(",")
88
+ val_str = val_str.replace('.', '').replace(',', '.') # Padrão BR
89
+ elif ',' in val_str:
90
+ # Se só tem vírgula, checamos se parece decimal (ex: 10,50) ou milhar (1,000)
91
+ # Se houver apenas uma vírgula e ela estiver nas últimas 3 posições: decimal
92
+ partes = val_str.split(',')
101
93
  if len(partes[-1]) <= 2:
102
- val_str = val_str.replace(".", "").replace(",", ".")
94
+ val_str = val_str.replace('.', '').replace(',', '.')
103
95
  else:
104
- val_str = val_str.replace(",", "")
96
+ val_str = val_str.replace(',', '')
105
97
 
106
98
  try:
107
99
  valor_float = float(val_str)
@@ -110,85 +102,71 @@ def converter_valor(row, schema_map, gerador_log):
110
102
  else:
111
103
  nova_linha[col] = valor_float
112
104
  except ValueError:
113
- gerador_log.add(
114
- f"AVISO: Não foi possível converter '{val}' para FLOAT. Coluna: {col}. Enviando NULL.",
115
- severity="WARNING",
116
- )
105
+ gerador_log.add(f"AVISO: Não foi possível converter '{val}' para FLOAT. Coluna: {col}. Enviando NULL.")
117
106
  nova_linha[col] = None
118
107
 
119
108
  elif tipo_bq in ("BOOLEAN", "BOOL"):
120
109
  nova_linha[col] = str(val).lower() in ("true", "1", "t", "yes")
121
110
 
122
111
  elif tipo_bq == "DATE":
123
- val_str = str(val).strip().upper()
124
-
125
- meses_pt_en = {
126
- "JAN": "01", "FEV": "02", "FEB": "02", "MAR": "03",
127
- "ABR": "04", "APR": "04", "MAI": "05", "MAY": "05",
128
- "JUN": "06", "JUL": "07", "AGO": "08", "AUG": "08",
129
- "SET": "09", "SEP": "09", "OUT": "10", "OCT": "10",
130
- "NOV": "11", "DEZ": "12", "DEC": "12",
131
- }
132
-
133
- # Trata formato DD/MMM/AA (ex: 22/JAN/26 ou 22/jan/26)
134
- match_dma = re.match(r"^(\d{2})/([A-Z]{3})/(\d{2})$", val_str)
135
- # Trata formato MMM-AA (ex: FEB-26)
136
- match_ma = re.match(r"^([A-Z]{3})-(\d{2})$", val_str)
137
- # Trata formato DD/MM/YYYY
138
- match_br = regex_data_br.match(val_str)
139
-
140
- if match_dma:
141
- mes = meses_pt_en.get(match_dma.group(2), "01")
142
- ano = f"20{match_dma.group(3)}"
143
- nova_linha[col] = f"{ano}-{mes}-{match_dma.group(1)}"
144
- elif match_ma:
145
- mes = meses_pt_en.get(match_ma.group(1), "01")
146
- ano = f"20{match_ma.group(2)}"
112
+ val_str = str(val).strip().upper() # Garante que FEB-26 fique maiúsculo
113
+
114
+ # 1. Trata o caso 'FEB-26' (Mês-Ano)
115
+ # Regex para capturar algo como AAA-99
116
+ regex_mes_ano = re.compile(r"^([A-Z]{3})-(\d{2})$")
117
+ match_ma = regex_mes_ano.match(val_str)
118
+
119
+ if match_ma:
120
+ meses = {
121
+ 'JAN': '01', 'FEB': '02', 'MAR': '03', 'APR': '04', 'MAY': '05', 'JUN': '06',
122
+ 'JUL': '07', 'AUG': '08', 'SEP': '09', 'OCT': '10', 'NOV': '11', 'DEC': '12'
123
+ }
124
+ mes = meses.get(match_ma.group(1), '01')
125
+ ano = f"20{match_ma.group(2)}" # Assume 20xx
126
+ # Como o BQ exige dia, vamos fixar no dia 01 do mês
147
127
  nova_linha[col] = f"{ano}-{mes}-01"
148
- elif match_br:
149
- nova_linha[col] = f"{match_br.group(3)}-{match_br.group(2)}-{match_br.group(1)}"
128
+
150
129
  else:
151
- nova_linha[col] = val_str[:10] if len(val_str) >= 10 else None
130
+ # 2. Lógica que tínhamos para DD/MM/YYYY
131
+ val_str = val_str.replace('0224','2024').replace('0202','2022').replace('0204','2024')
132
+ match_br = regex_data_br.match(val_str)
133
+ if match_br:
134
+ nova_linha[col] = f"{match_br.group(3)}-{match_br.group(2)}-{match_br.group(1)}"
135
+ else:
136
+ # Tenta pegar apenas os primeiros 10 caracteres (YYYY-MM-DD)
137
+ nova_linha[col] = val_str[:10] if len(val_str) >= 10 else None
152
138
 
153
139
  elif tipo_bq == "DATETIME":
154
140
  val_str = str(val).strip()
155
-
156
- # Tenta formato brasileiro DD/MM/YYYY HH:MM:SS
157
- regex_dt_br = re.compile(
158
- r"^(\d{2})/(\d{2})/(\d{4})\s+(\d{2}:\d{2}:\d{2})(?:\.\d+)?$"
159
- )
141
+
142
+ # Tenta detectar formato brasileiro DD/MM/YYYY HH:MM:SS
143
+ regex_dt_br = re.compile(r"^(\d{2})/(\d{2})/(\d{4})\s+(\d{2}:\d{2}:\d{2})(?:\.\d+)?$")
160
144
  match_dt_br = regex_dt_br.match(val_str)
145
+
161
146
  if match_dt_br:
162
- nova_linha[col] = (
163
- f"{match_dt_br.group(3)}-{match_dt_br.group(2)}-{match_dt_br.group(1)}"
164
- f" {match_dt_br.group(4)}"
165
- )
147
+ # Converte para YYYY-MM-DD HH:MM:SS
148
+ nova_linha[col] = f"{match_dt_br.group(3)}-{match_dt_br.group(2)}-{match_dt_br.group(1)} {match_dt_br.group(4)}"
166
149
  else:
167
150
  match_iso = regex_iso_tz.match(val_str)
168
151
  if match_iso:
169
152
  nova_linha[col] = f"{match_iso.group(1)} {match_iso.group(2)}"
170
153
  else:
171
- # Limpeza robusta de timezone para evitar erro de formato
154
+ # Limpeza robusta existente
172
155
  limpo = val_str.replace("T", " ").replace("Z", "")
173
- if "+" in limpo:
174
- limpo = limpo.split("+")[0]
175
- if "-" in limpo and len(limpo.split("-")[-1]) == 5:
176
- limpo = limpo.rsplit("-", 1)[0]
156
+ if '+' in limpo: limpo = limpo.split('+')[0]
157
+ if '-' in limpo and len(limpo.split('-')[-1]) == 5:
158
+ limpo = limpo.rsplit('-', 1)[0]
177
159
  nova_linha[col] = limpo.strip()[:23]
178
160
 
179
- else:
180
- # Tipo não tratado: devolve o valor como veio
181
- nova_linha[col] = val
182
-
183
161
  except Exception:
184
- # Se falhar a conversão, envia NULL para evitar Type Mismatch no BigQuery
185
- gerador_log.add(
186
- f"AVISO: Falha conversão coluna '{col}' valor '{val}'. Enviando NULL."
187
- )
162
+ # CORREÇÃO DE FALLBACK:
163
+ # Se falhar a conversão (ex: texto em campo Inteiro), envia NULL
164
+ # Enviar str(val) causaria erro de "Type Mismatch" no BigQuery
165
+ gerador_log.add(f"AVISO: Falha conversão coluna '{col}' valor '{val}'. Enviando NULL.")
188
166
  nova_linha[col] = None
189
167
 
190
168
  return nova_linha
191
-
169
+
192
170
 
193
171
  def executar_com_fallback(func, bq_client, timeout, project, dataset, tabela, gerador_log):
194
172
  """Executa `func` em um executor com timeout e captura falhas.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csc-essentials
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: Biblioteca de utilitários essenciais para projetos CSC
5
5
  Author-email: David Braga <david.braga@stone.com.br>
6
6
  License: MIT
File without changes
File without changes
File without changes