jiboia-gpu 0.1.0__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.
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lucas M.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
23
+ USE OF THIS SOFTWARE IS AT YOUR OWN RISK. THE AUTHORS PROVIDE NO WARRANTIES,
24
+ AND ARE NOT LIABLE FOR ANY DAMAGES, DIRECT OR INDIRECT, ARISING FROM ITS USE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: jiboia_gpu
3
+ Version: 0.1.0
4
+ Summary: jiboia_gpu is a python package for automatically normalizing DataFrames and optimizing types efficiently.
5
+ Author-email: Lucas Muffato <lucasmuffato.eng@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Lucas M.
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ THE SOFTWARE.
27
+
28
+ USE OF THIS SOFTWARE IS AT YOUR OWN RISK. THE AUTHORS PROVIDE NO WARRANTIES,
29
+ AND ARE NOT LIABLE FOR ANY DAMAGES, DIRECT OR INDIRECT, ARISING FROM ITS USE.
30
+
31
+ Project-URL: Homepage, https://github.com/lmuffato/jiboia_gpu
32
+ Project-URL: Bug_Tracker, https://github.com/lmuffato/jiboia_gpu/issues
33
+ Requires-Python: >=3.9
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: cudf-cu12>=25.8
37
+ Dynamic: license-file
38
+
39
+ # Jiboia
40
+
41
+ **jiboia-gpu** is a Python package to **normalize and optimize DataFrames automatically** efficiently using the Nvidia GPU in the RAPIDS ecosystem.
42
+
43
+ Key features:
44
+ - **String normalization**:
45
+ - Removes extra spaces.
46
+ - Strips leading and trailing spaces.
47
+ - Detects data pollution (e.g., columns that should be numeric but contain strings).
48
+ - **Type conversion**:
49
+ - Numeric strings and floats ending in `.0` → integers (`int8`, `int16`, `int32`, …).
50
+ - Converts floats and integers to the most memory-efficient type.
51
+ - Converts strings in various date formats to `datetime` (`yyyy?mm?dd`, `dd?mm?yyyy`, `yyyymmd`, `dd?mm?yy`).
52
+ - Converts time strings (`hhmm UTC`, `hh:mm:ss`, `hh:mm:ss.s`) to `timedelta`.
53
+ - **Null standardization** → converts different null representations to `pd.NA`.
54
+ - **Automatic CSV detection**:
55
+ - Detects delimiter.
56
+ - Detects encoding.
57
+ - **Memory optimization**:
58
+ - Provides memory usage information for DataFrames.
59
+ - Converts columns to the most compact types possible.
60
+
61
+ ---
62
+
63
+ ## Example Usage
64
+
65
+ ```python
66
+ from jiboia-gpu import jiboia_gpu as jb
67
+
68
+ pd.normalize_category("data_frame_cudf")
69
+
70
+ ```
@@ -0,0 +1,32 @@
1
+ # Jiboia
2
+
3
+ **jiboia-gpu** is a Python package to **normalize and optimize DataFrames automatically** efficiently using the Nvidia GPU in the RAPIDS ecosystem.
4
+
5
+ Key features:
6
+ - **String normalization**:
7
+ - Removes extra spaces.
8
+ - Strips leading and trailing spaces.
9
+ - Detects data pollution (e.g., columns that should be numeric but contain strings).
10
+ - **Type conversion**:
11
+ - Numeric strings and floats ending in `.0` → integers (`int8`, `int16`, `int32`, …).
12
+ - Converts floats and integers to the most memory-efficient type.
13
+ - Converts strings in various date formats to `datetime` (`yyyy?mm?dd`, `dd?mm?yyyy`, `yyyymmd`, `dd?mm?yy`).
14
+ - Converts time strings (`hhmm UTC`, `hh:mm:ss`, `hh:mm:ss.s`) to `timedelta`.
15
+ - **Null standardization** → converts different null representations to `pd.NA`.
16
+ - **Automatic CSV detection**:
17
+ - Detects delimiter.
18
+ - Detects encoding.
19
+ - **Memory optimization**:
20
+ - Provides memory usage information for DataFrames.
21
+ - Converts columns to the most compact types possible.
22
+
23
+ ---
24
+
25
+ ## Example Usage
26
+
27
+ ```python
28
+ from jiboia-gpu import jiboia_gpu as jb
29
+
30
+ pd.normalize_category("data_frame_cudf")
31
+
32
+ ```
@@ -0,0 +1,70 @@
1
+ from .gpu_normalize_string_utils import GPUNormalizeStringUtils
2
+ from .gpu_normalize_null_utils import GPUNormalizeNullUtils
3
+ from .gpu_analysis_utils import GPUAnalysisUtils
4
+ from .gpu_normalize_date_utils import GPUNormalizeDateUtils
5
+ from .gpu_normalize_time_utils import GPUNormalizeTimeUtils
6
+ from .gpu_normalize_datetime_utils import GPUNormalizeDateTimeUtils
7
+ from .gpu_normalize_boolean_utils import GPUNormalizeBooleanUtils
8
+ from .gpu_normalize_numeric_utils import GPUNormalizeNumericUtils
9
+ from .gpu_file_analysis_utils import GPUFileAnalysisUtils
10
+
11
+
12
+ class JiboiaGPU:
13
+ # STRING
14
+ normalize_str = staticmethod(GPUNormalizeStringUtils.normalize_str)
15
+ to_category = staticmethod(GPUNormalizeStringUtils.to_category)
16
+ normalize_category = staticmethod(GPUNormalizeStringUtils.normalize_category)
17
+ normalize_unique_values = staticmethod(GPUNormalizeStringUtils.normalize_unique_values)
18
+
19
+ # NULL
20
+ normalize_na = staticmethod(GPUNormalizeNullUtils.normalize_na)
21
+
22
+ # BOOL
23
+ normalize_bool = staticmethod(GPUNormalizeBooleanUtils.normalize_bool)
24
+
25
+ # NUMBER
26
+ normalize_number = staticmethod(GPUNormalizeNumericUtils.normalize_number)
27
+ is_probably_number = staticmethod(GPUNormalizeNumericUtils.is_probably_number)
28
+
29
+ # DATE
30
+ normalize_date = staticmethod(GPUNormalizeDateUtils.normalize_date)
31
+ normalize_datetime = staticmethod(GPUNormalizeDateUtils.normalize_datetime)
32
+ is_date_column_by_samples = staticmethod(GPUNormalizeDateUtils.is_date_column_by_samples)
33
+ is_date = staticmethod(GPUNormalizeDateUtils.is_date)
34
+ to_date_iso = staticmethod(GPUNormalizeDateUtils.to_date_iso)
35
+ to_datetime = staticmethod(GPUNormalizeDateUtils.to_datetime)
36
+
37
+
38
+ # TIME
39
+ normalize_time = staticmethod(GPUNormalizeTimeUtils.normalize_time)
40
+ is_time = staticmethod(GPUNormalizeTimeUtils.is_time)
41
+
42
+ # TIMEDELTA
43
+ normalize_timedelta = staticmethod(GPUNormalizeTimeUtils.normalize_timedelta)
44
+
45
+ # DATETIME
46
+ create_datetime_column_from_date_and_time = staticmethod(GPUNormalizeDateTimeUtils.create_datetime_column_from_date_and_time)
47
+ create_day_week_column = staticmethod(GPUNormalizeDateTimeUtils.create_day_week_column)
48
+ create_month_br_category_column = staticmethod(GPUNormalizeDateTimeUtils.create_month_br_category_column)
49
+ create_day_week_br_category_column = staticmethod(GPUNormalizeDateTimeUtils.create_day_week_br_category_column)
50
+ create_hour_category_column = staticmethod(GPUNormalizeDateTimeUtils.create_hour_category_column)
51
+
52
+ # ANALYSIS
53
+ drop_columns = staticmethod(GPUAnalysisUtils.drop_columns)
54
+ cudf_size_info = staticmethod(GPUAnalysisUtils.cudf_size_info)
55
+ pandasdf_size_info = staticmethod(GPUAnalysisUtils.pandasdf_size_info)
56
+ get_gpu_memory_info = staticmethod(GPUAnalysisUtils.get_gpu_memory_info)
57
+ is_vram_use_limit = staticmethod(GPUAnalysisUtils.is_vram_use_limit)
58
+ get_index_samples = staticmethod(GPUAnalysisUtils.get_index_samples)
59
+ infer_by_sample = staticmethod(GPUAnalysisUtils.infer_by_sample)
60
+
61
+ # FILE
62
+ get_csv_info = staticmethod(GPUFileAnalysisUtils.get_csv_info)
63
+ convert_csv_to_utf8 = staticmethod(GPUFileAnalysisUtils.convert_csv_to_utf8)
64
+ convert_all_csvs_to_utf8 = staticmethod(GPUFileAnalysisUtils.convert_all_csvs_to_utf8)
65
+ read_csv_files = staticmethod(GPUFileAnalysisUtils.read_csv_files)
66
+
67
+ # instância única para ser usada como namespace
68
+ jiboia_gpu = JiboiaGPU()
69
+
70
+ __all__ = ["jiboia_gpu", "JiboiaGPU"]
@@ -0,0 +1,274 @@
1
+ import cudf
2
+ import cupy as cp
3
+ import pandas as pd
4
+
5
+
6
+ def print_text_red(text: str) -> str:
7
+ return f"\033[1;31m{text}\033[0m"
8
+
9
+ def print_text_yellow(text: str) -> str:
10
+ return f"\033[1;33m{text}\033[0m"
11
+
12
+ def print_text_green(text: str) -> str:
13
+ return f"\033[1;32m{text}\033[0m"
14
+
15
+
16
+ def print_job_drop_column_done(
17
+ columns_to_delete: list[str]
18
+ ) -> None:
19
+ colored_names: list[str] = [
20
+ print_text_yellow(name) for name in columns_to_delete
21
+ ]
22
+
23
+ msg: str = ", ".join(colored_names)
24
+
25
+ print(
26
+ print_text_green("Done!"),
27
+ "column",
28
+ msg,
29
+ "was",
30
+ print_text_red("dropped")
31
+ )
32
+
33
+
34
+ class GPUAnalysisUtils:
35
+ @staticmethod
36
+ def drop_columns(
37
+ current_df: cudf.DataFrame,
38
+ column_names: list[str],
39
+ ) -> None:
40
+
41
+ columns_to_delete: list = []
42
+
43
+ for column_name in column_names:
44
+
45
+ if column_name in current_df.columns:
46
+ columns_to_delete.append(column_name)
47
+
48
+
49
+ if len(columns_to_delete) >= 1:
50
+
51
+ current_df.drop(
52
+ columns=columns_to_delete,
53
+ inplace=True
54
+ )
55
+ print_job_drop_column_done(columns_to_delete)
56
+
57
+
58
+ @staticmethod
59
+ def cudf_size_info(current_df: cudf.DataFrame, print_info: bool = False) -> None:
60
+
61
+ rows: int = current_df.shape[0]
62
+ columns: int = current_df.shape[1]
63
+ vram_size_mb: float = round(current_df.memory_usage(index=True, deep=True).sum() / (1024 * 1024), 2)
64
+
65
+ cudf_info: dict[str, any] = {
66
+ "rows": rows,
67
+ "columns": columns,
68
+ "VRAM size Mb": vram_size_mb
69
+ }
70
+
71
+ if print_info:
72
+ print(
73
+ print_text_green("Done!"),
74
+ "rows:",
75
+ print_text_yellow(rows),
76
+ "columns:",
77
+ print_text_yellow(columns),
78
+ "VRAM size Mb:",
79
+ print_text_yellow(vram_size_mb),
80
+ )
81
+
82
+ return cudf_info
83
+
84
+
85
+ @staticmethod
86
+ def get_gpu_memory_info(device_id: int = 0) -> dict[str, int]:
87
+ free_bytes, total_bytes = cp.cuda.runtime.memGetInfo()
88
+ return {
89
+ "free_mb": round(free_bytes / (1024 * 1024), 2),
90
+ "total": round(total_bytes / (1024 * 1024), 2),
91
+ "used_mb": round((total_bytes - free_bytes) / (1024 * 1024), 2),
92
+ }
93
+
94
+ @staticmethod
95
+ def is_vram_use_limit(device_id: int = 0) -> dict[str, int]:
96
+ free_bytes, total_bytes = cp.cuda.runtime.memGetInfo()
97
+ vram_percent_in_use: float = round(((total_bytes - free_bytes) / total_bytes) * 100, 1) >= 90
98
+
99
+ if vram_percent_in_use >= 90:
100
+ return True
101
+
102
+ return False
103
+
104
+
105
+ @staticmethod
106
+ def pandasdf_size_info(current_df: pd.DataFrame) -> None:
107
+ return {
108
+ "rows": current_df.shape[0],
109
+ "columns": current_df.shape[1],
110
+ "RAM MB": round(current_df.memory_usage(index=True, deep=True).sum() / (1024 * 1024), 2)
111
+ }
112
+
113
+
114
+ def print_converted_column_type(
115
+ column_name: str,
116
+ column_type: str
117
+ ) -> None:
118
+ print(
119
+ "\033[1;32mDone!\033[0m",
120
+ "column",
121
+ # f"{column_name}",
122
+ f"\033[1;33m{column_name}\033[0m",
123
+ "converted to", f"\033[1;33m{column_type}\033[0m"
124
+ )
125
+
126
+ # @staticmethod
127
+ # def get_frequency_by_column(current_df: pd.DataFrame, column: str) -> pd.DataFrame:
128
+ # '''
129
+ # Gera um current_df com a frequencia de valores de uma coluna
130
+ # '''
131
+ # current_df_frequency: pd.DataFrame = (
132
+ # current_df[column]
133
+ # .value_counts()
134
+ # .reset_index()
135
+ # )
136
+
137
+ # current_df_frequency.columns = [column, 'frequency']
138
+
139
+ # current_df_frequency: pd.DataFrame = (
140
+ # current_df_frequency
141
+ # .sort_values(
142
+ # by='frequency',
143
+ # ascending=False
144
+ # )
145
+ # )
146
+
147
+ # return current_df_frequency
148
+
149
+
150
+ # @staticmethod
151
+ # def get_unique_values(current_df: pd.DataFrame, column: str) -> pd.DataFrame:
152
+ # '''
153
+ # Retorna um dataframe com os valores únicos de uma coluna
154
+ # '''
155
+ # df_unique = pd.DataFrame({
156
+ # column: current_df[column].dropna().unique()
157
+ # })
158
+
159
+ # df_unique = df_unique.sort_values(
160
+ # by=column,
161
+ # ascending=True
162
+ # ).reset_index(drop=True)
163
+
164
+ # return df_unique
165
+
166
+ @staticmethod
167
+ def get_index_samples(
168
+ series: cudf.Series,
169
+ n_parts: int = 10,
170
+ n_samples: int = 10
171
+ ) -> list[int]:
172
+ """
173
+ Verifica de forma performática na GPU se algum valor nas amostras de uma Series
174
+ corresponde a um padrão de data.
175
+
176
+ Args:
177
+ s (cudf.Series): A Series de strings que será analisada.
178
+ n_parts (int): O número de partes em que a Series será dividida para amostragem.
179
+ n_samples (int): O número de amostras a serem coletadas de cada parte.
180
+
181
+ Returns:
182
+ bool: True se um padrão de data for encontrado nas amostras, False caso contrário.
183
+ """
184
+
185
+ series_size = len(series)
186
+
187
+ # print(series.name)
188
+ # return
189
+
190
+ if ((n_parts * n_samples) >= series_size):
191
+ raise ValueError("The total number of samples requested exceeds or equals the series size. Please provide a smaller value for n_parts or n_samples.")
192
+
193
+ if (series_size // n_parts == 0):
194
+ raise ValueError("The number of parts is greater than the series size. Please provide a smaller value for n_parts.")
195
+
196
+ # Gera todos os índices de amostragem DE UMA VEZ na GPU
197
+ step_pass = series_size // n_parts
198
+
199
+ # Índices iniciais de cada bloco (ex: 0, 1000, 2000, ...)
200
+ start_indices = cp.arange(n_parts) * step_pass
201
+
202
+ # Offsets dentro de cada bloco (ex: 0, 1, 2, ... n_samples-1)
203
+ sample_offsets = cp.arange(n_samples)
204
+
205
+ all_indices = (start_indices[:, None] + sample_offsets).flatten()
206
+
207
+ # Garante que os índices não ultrapassem o tamanho da Series
208
+ all_indices = all_indices[all_indices < series_size]
209
+
210
+ return all_indices
211
+
212
+
213
+ @staticmethod
214
+ def infer_by_sample(
215
+ series: cudf.Series,
216
+ regex_patern: str,
217
+ n_parts: int = 10,
218
+ n_samples: int = 10
219
+ ) -> bool:
220
+ """
221
+ Verifica de forma performática na GPU se algum valor nas amostras de uma Series
222
+ corresponde a um padrão de data.
223
+
224
+ Args:
225
+ s (cudf.Series): A Series de strings que será analisada.
226
+ n_parts (int): O número de partes em que a Series será dividida para amostragem.
227
+ n_samples (int): O número de amostras a serem coletadas de cada parte.
228
+
229
+ Returns:
230
+ bool: True se um padrão de data for encontrado nas amostras, False caso contrário.
231
+ """
232
+ series_size = len(series)
233
+
234
+ if series_size == 0:
235
+ return False
236
+
237
+ # Coluna sem dados
238
+ if series.notna().sum() == 0:
239
+ return False
240
+
241
+ if series.dtype not in ["object", "string"]:
242
+ return False
243
+
244
+
245
+ # print(series.name)
246
+ # return
247
+
248
+ if ((n_parts * n_samples) >= series_size):
249
+ raise ValueError("The total number of samples requested exceeds or equals the series size. Please provide a smaller value for n_parts or n_samples.")
250
+
251
+ if (series_size // n_parts == 0):
252
+ raise ValueError("The number of parts is greater than the series size. Please provide a smaller value for n_parts.")
253
+
254
+ # Gera todos os índices de amostragem DE UMA VEZ na GPU
255
+ step_pass = series_size // n_parts
256
+
257
+ # Índices iniciais de cada bloco (ex: 0, 1000, 2000, ...)
258
+ start_indices = cp.arange(n_parts) * step_pass
259
+
260
+ # Offsets dentro de cada bloco (ex: 0, 1, 2, ... n_samples-1)
261
+ sample_offsets = cp.arange(n_samples)
262
+
263
+ all_indices = (start_indices[:, None] + sample_offsets).flatten()
264
+
265
+ # Garante que os índices não ultrapassem o tamanho da Series
266
+ all_indices = all_indices[all_indices < series_size]
267
+
268
+ # 3. Seleção de todas as amostras em uma única operação
269
+ samples = series.iloc[all_indices]
270
+
271
+ if (samples.str.contains(regex_patern).sum() == samples.notna().sum()):
272
+ return True
273
+
274
+ return False
@@ -0,0 +1,179 @@
1
+ import csv
2
+ import chardet
3
+ from pathlib import Path
4
+
5
+
6
+ def print_text_red(text: str) -> str:
7
+ return f"\033[1;31m{text}\033[0m"
8
+
9
+ def print_text_yellow(text: str) -> str:
10
+ return f"\033[1;33m{text}\033[0m"
11
+
12
+ def print_text_green(text: str) -> str:
13
+ return f"\033[1;32m{text}\033[0m"
14
+
15
+
16
+ class GPUFileAnalysisUtils:
17
+ @staticmethod
18
+ def get_csv_info(
19
+ file_path: str,
20
+ encoding: None|str = None,
21
+ skiprows: None|int = 0,
22
+ sample_characters: int = 2048,
23
+ sample_bytes: int = 100000
24
+ ) -> dict[str, any]:
25
+ """
26
+ Detecta o delimitador e o encoding de um arquivo CSV.
27
+
28
+ :param file_path: caminho do arquivo CSV
29
+ :param encoding: encoding forçado (se None, tenta detectar com chardet)
30
+ :param skiprows: linhas iniciais a ignorar
31
+ :param sample_characters: n de caracteres usados para sniffing
32
+ :param sample_bytes: n de bytes usados para detectar encoding
33
+ :return: (delimiter, encoding_detectado)
34
+ :exemplo_de_retorno:
35
+ {
36
+ 'encoding': 'utf-8',
37
+ 'confidence': 0.99,
38
+ 'language': 'pt',
39
+ 'delimiter': ','
40
+ }
41
+ """
42
+ csv_info: dict[str, any] = {
43
+ "encoding": encoding
44
+ }
45
+
46
+ # detecta encoding se não informado
47
+ if encoding is None:
48
+ with open(file_path, "rb") as f:
49
+ rawdata = f.read(sample_bytes)
50
+ enc_info = chardet.detect(rawdata)
51
+ encoding = enc_info["encoding"] or "utf-8"
52
+
53
+ csv_info = {**csv_info, **enc_info}
54
+
55
+ # detecta delimitador
56
+ with open(file_path, "r", encoding=encoding, errors="ignore") as file:
57
+ for _ in range(skiprows):
58
+ next(file, None) # ignora linhas iniciais
59
+
60
+ sample = file.read(sample_characters)
61
+ sniffer = csv.Sniffer()
62
+ try:
63
+ dialect = sniffer.sniff(sample)
64
+ delimiter = dialect.delimiter
65
+ except csv.Error:
66
+ delimiter = ";"
67
+
68
+ csv_info["delimiter"] = delimiter
69
+
70
+ print(
71
+ print_text_green("Done!"),
72
+ "file:",
73
+ print_text_yellow(file_path),
74
+ "delimiter:",
75
+ print_text_yellow(f'"{csv_info["delimiter"]}"'),
76
+ "encoding:",
77
+ print_text_yellow(csv_info["encoding"]),
78
+ )
79
+ return csv_info
80
+
81
+
82
+ @staticmethod
83
+ def convert_csv_to_utf8(file_path: str, output_path: str = None) -> str:
84
+ """
85
+ Converte um arquivo CSV para UTF-8.
86
+
87
+ :param file_path: caminho do arquivo original
88
+ :param output_path: caminho do arquivo convertido (se None, gera com _utf8.csv)
89
+ :return: caminho do arquivo convertido em UTF-8
90
+ """
91
+ file_path = Path(file_path)
92
+
93
+ # se não passar output_path, cria automático
94
+ if output_path is None:
95
+ output_path = file_path.with_name(file_path.stem + "_utf8.csv")
96
+
97
+ # detecta encoding
98
+ with open(file_path, "rb") as f:
99
+ rawdata = f.read(100000)
100
+ enc_info = chardet.detect(rawdata)
101
+ source_encoding = enc_info["encoding"] or "latin-1"
102
+ print(f"Encoding detectado: {source_encoding} (confiança={enc_info['confidence']:.2f})")
103
+
104
+ # lê no encoding original e salva em UTF-8
105
+ with open(file_path, "r", encoding=source_encoding, errors="ignore") as f_in:
106
+ data = f_in.read()
107
+
108
+ with open(output_path, "w", encoding="utf-8") as f_out:
109
+ f_out.write(data)
110
+
111
+ print(f"Arquivo convertido salvo em: {output_path}")
112
+ return str(output_path)
113
+
114
+
115
+ import chardet
116
+ from pathlib import Path
117
+
118
+
119
+ @staticmethod
120
+ def convert_all_csvs_to_utf8(folder_path: str, new_folder: bool = False) -> None:
121
+ """
122
+ Converte todos os arquivos CSV em uma pasta para UTF-8.
123
+ Detecta o encoding de cada arquivo automaticamente.
124
+
125
+ :param folder_path: caminho da pasta contendo os CSVs
126
+ :param new_folder: se True, cria uma subpasta 'utf8_converted',
127
+ se False, salva os arquivos na mesma pasta com sufixo '_utf8'
128
+ """
129
+ folder = Path(folder_path)
130
+
131
+ if new_folder:
132
+ output_folder = folder / "utf8_converted"
133
+ output_folder.mkdir(exist_ok=True)
134
+ else:
135
+ output_folder = folder # salva na mesma pasta
136
+
137
+ for csv_file in folder.glob("*.csv"):
138
+
139
+ # Detecta encoding
140
+ with open(csv_file, "rb") as f:
141
+ rawdata = f.read(100000) # lê parte do arquivo
142
+ result = chardet.detect(rawdata)
143
+ source_encoding = result["encoding"] or "latin-1"
144
+ confidence = result["confidence"]
145
+ # print(f"{csv_file.name}: encoding detectado = {source_encoding} (confiança={confidence:.2f})")
146
+
147
+ print(
148
+ "file:",
149
+ csv_file,
150
+ "encoding:",
151
+ print_text_yellow(result["encoding"]),
152
+ "confidence:",
153
+ print_text_yellow(confidence),
154
+ )
155
+
156
+ # Se já estiver em UTF-8, pula
157
+ if source_encoding.lower() == 'utf-8':
158
+ continue
159
+
160
+ # Lê e converte para UTF-8
161
+ with open(csv_file, "r", encoding=source_encoding, errors="ignore") as f_in:
162
+ data = f_in.read()
163
+
164
+ if new_folder:
165
+ output_file = output_folder / csv_file.name
166
+ else:
167
+ output_file = output_folder / f"{csv_file.stem}_utf8.csv"
168
+
169
+ with open(output_file, "w", encoding="utf-8") as f_out:
170
+ f_out.write(data)
171
+
172
+ # print(f"Arquivo convertido salvo em: {output_file}")
173
+ print(
174
+ print_text_green("Done!"),
175
+ "file:",
176
+ print_text_yellow(output_file),
177
+ "converted to",
178
+ print_text_yellow("utf-8"),
179
+ )