csc-essentials 0.1.0.dev3__tar.gz → 0.2.3__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.
- {csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/LICENSE +21 -21
- csc_essentials-0.2.3/PKG-INFO +98 -0
- csc_essentials-0.2.3/README.md +59 -0
- {csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/pyproject.toml +17 -5
- {csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/setup.cfg +4 -4
- csc_essentials-0.2.3/src/csc_essentials/__init__.py +30 -0
- csc_essentials-0.2.3/src/csc_essentials/core.py +32 -0
- csc_essentials-0.2.3/src/csc_essentials/database.py +222 -0
- csc_essentials-0.2.3/src/csc_essentials/logging.py +159 -0
- csc_essentials-0.2.3/src/csc_essentials/secrets.py +28 -0
- csc_essentials-0.2.3/src/csc_essentials/utils.py +164 -0
- csc_essentials-0.2.3/src/csc_essentials.egg-info/PKG-INFO +98 -0
- csc_essentials-0.2.3/src/csc_essentials.egg-info/SOURCES.txt +14 -0
- {csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/src/csc_essentials.egg-info/requires.txt +1 -1
- csc_essentials-0.2.3/src/csc_essentials.egg-info/top_level.txt +1 -0
- csc_essentials-0.1.0.dev3/PKG-INFO +0 -38
- csc_essentials-0.1.0.dev3/README.md +0 -0
- csc_essentials-0.1.0.dev3/src/csc_essentials.egg-info/PKG-INFO +0 -38
- csc_essentials-0.1.0.dev3/src/csc_essentials.egg-info/SOURCES.txt +0 -8
- csc_essentials-0.1.0.dev3/src/csc_essentials.egg-info/top_level.txt +0 -1
- {csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/src/csc_essentials.egg-info/dependency_links.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026
|
|
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 all
|
|
13
|
-
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 THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csc-essentials
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Biblioteca de utilitários essenciais para projetos CSC
|
|
5
|
+
Author-email: David Braga <david.braga@stone.com.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: slack-sdk>=3.0.0
|
|
19
|
+
Requires-Dist: google-cloud-secret-manager>=2.0.0
|
|
20
|
+
Requires-Dist: google-cloud-compute>=1.15.0
|
|
21
|
+
Requires-Dist: google-cloud-storage>=2.10.0
|
|
22
|
+
Requires-Dist: google-cloud-bigquery>=3.10.0
|
|
23
|
+
Requires-Dist: google-cloud-logging>=3.5.0
|
|
24
|
+
Requires-Dist: google-api-core>=2.11.0
|
|
25
|
+
Requires-Dist: pytz>=2023.3
|
|
26
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
27
|
+
Requires-Dist: unidecode>=1.3.6
|
|
28
|
+
Requires-Dist: pandas>=2.0.0
|
|
29
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
30
|
+
Requires-Dist: protobuf>=5.0.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# csc-essentials
|
|
42
|
+
|
|
43
|
+
Pequeno utilitário para integração com Google Cloud (BigQuery, Secret Manager, Cloud Logging, Compute).
|
|
44
|
+
|
|
45
|
+
## Instalação
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install csc-essentials
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart (exemplo rápido)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from google.cloud import bigquery
|
|
55
|
+
from csc_essentials.logging import GeradorLog
|
|
56
|
+
from csc_essentials.secrets import get_secrets
|
|
57
|
+
from csc_essentials.database import inserir_banco_dados
|
|
58
|
+
|
|
59
|
+
log = GeradorLog("exemplo")
|
|
60
|
+
# Ler segredo do Secret Manager (ex.: token Slack)
|
|
61
|
+
raw = get_secrets("projects/MEU_PROJ/secrets/SLACK_SECRET/versions/latest")
|
|
62
|
+
|
|
63
|
+
# Preparar cliente BigQuery
|
|
64
|
+
client = bigquery.Client()
|
|
65
|
+
|
|
66
|
+
# Carregar arquivo NDJSON para uma tabela
|
|
67
|
+
inserir_banco_dados(
|
|
68
|
+
arquivo_path="/tmp/dados.ndjson",
|
|
69
|
+
projeto="meu-projeto",
|
|
70
|
+
dataset="meu_dataset",
|
|
71
|
+
tabela="minha_tabela",
|
|
72
|
+
log=log,
|
|
73
|
+
client=client,
|
|
74
|
+
wait_job=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print("Upload iniciado")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentação
|
|
81
|
+
|
|
82
|
+
- Documentação por módulo em `docs/` (veja `docs/overview.md`).
|
|
83
|
+
- Principais módulos: `logging`, `secrets`, `utils`, `database`.
|
|
84
|
+
|
|
85
|
+
## Contribuindo
|
|
86
|
+
|
|
87
|
+
- Abra uma issue descrevendo o problema ou feature.
|
|
88
|
+
- Envie um Pull Request com testes e descrição clara das mudanças.
|
|
89
|
+
- Formato de código: siga o estilo existing do projeto e execute os testes (`pytest`).
|
|
90
|
+
|
|
91
|
+
## Licença
|
|
92
|
+
|
|
93
|
+
Este repositório está licenciado conforme o arquivo `LICENSE` na raiz.
|
|
94
|
+
|
|
95
|
+
## Notas rápidas
|
|
96
|
+
|
|
97
|
+
- Verifique permissões GCP ao usar `bigquery.Client()` e `secretmanager.SecretManagerServiceClient`.
|
|
98
|
+
- Em desenvolvimento local, você pode desativar o envio para Cloud Logging configurando `GCP_LOGGING_ATIVO=false`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
# csc-essentials
|
|
3
|
+
|
|
4
|
+
Pequeno utilitário para integração com Google Cloud (BigQuery, Secret Manager, Cloud Logging, Compute).
|
|
5
|
+
|
|
6
|
+
## Instalação
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install csc-essentials
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart (exemplo rápido)
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from google.cloud import bigquery
|
|
16
|
+
from csc_essentials.logging import GeradorLog
|
|
17
|
+
from csc_essentials.secrets import get_secrets
|
|
18
|
+
from csc_essentials.database import inserir_banco_dados
|
|
19
|
+
|
|
20
|
+
log = GeradorLog("exemplo")
|
|
21
|
+
# Ler segredo do Secret Manager (ex.: token Slack)
|
|
22
|
+
raw = get_secrets("projects/MEU_PROJ/secrets/SLACK_SECRET/versions/latest")
|
|
23
|
+
|
|
24
|
+
# Preparar cliente BigQuery
|
|
25
|
+
client = bigquery.Client()
|
|
26
|
+
|
|
27
|
+
# Carregar arquivo NDJSON para uma tabela
|
|
28
|
+
inserir_banco_dados(
|
|
29
|
+
arquivo_path="/tmp/dados.ndjson",
|
|
30
|
+
projeto="meu-projeto",
|
|
31
|
+
dataset="meu_dataset",
|
|
32
|
+
tabela="minha_tabela",
|
|
33
|
+
log=log,
|
|
34
|
+
client=client,
|
|
35
|
+
wait_job=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
print("Upload iniciado")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Documentação
|
|
42
|
+
|
|
43
|
+
- Documentação por módulo em `docs/` (veja `docs/overview.md`).
|
|
44
|
+
- Principais módulos: `logging`, `secrets`, `utils`, `database`.
|
|
45
|
+
|
|
46
|
+
## Contribuindo
|
|
47
|
+
|
|
48
|
+
- Abra uma issue descrevendo o problema ou feature.
|
|
49
|
+
- Envie um Pull Request com testes e descrição clara das mudanças.
|
|
50
|
+
- Formato de código: siga o estilo existing do projeto e execute os testes (`pytest`).
|
|
51
|
+
|
|
52
|
+
## Licença
|
|
53
|
+
|
|
54
|
+
Este repositório está licenciado conforme o arquivo `LICENSE` na raiz.
|
|
55
|
+
|
|
56
|
+
## Notas rápidas
|
|
57
|
+
|
|
58
|
+
- Verifique permissões GCP ao usar `bigquery.Client()` e `secretmanager.SecretManagerServiceClient`.
|
|
59
|
+
- Em desenvolvimento local, você pode desativar o envio para Cloud Logging configurando `GCP_LOGGING_ATIVO=false`.
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "csc-essentials"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Biblioteca de utilitários essenciais para projetos CSC"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.9"
|
|
11
|
-
authors = [ { name = "
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [ { name = "David Braga", email = "david.braga@stone.com.br" } ]
|
|
12
12
|
license = { text = "MIT" }
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Programming Language :: Python :: 3",
|
|
@@ -33,7 +33,7 @@ dependencies = [
|
|
|
33
33
|
"unidecode>=1.3.6",
|
|
34
34
|
"pandas>=2.0.0",
|
|
35
35
|
"openpyxl>=3.1.0",
|
|
36
|
-
"
|
|
36
|
+
"protobuf>=5.0.0",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
@@ -50,4 +50,16 @@ dev = [
|
|
|
50
50
|
where = ["src"]
|
|
51
51
|
|
|
52
52
|
[tool.setuptools.package-data]
|
|
53
|
-
"*" = ["LICENSE", "README.md"]
|
|
53
|
+
"*" = ["LICENSE", "README.md"]
|
|
54
|
+
|
|
55
|
+
# Configuração para o UV encontrar dependências em ambos os repositórios
|
|
56
|
+
[[tool.uv.index]]
|
|
57
|
+
name = "test-pypi"
|
|
58
|
+
url = "https://test.pypi.org/simple/"
|
|
59
|
+
explicit = true
|
|
60
|
+
|
|
61
|
+
[[tool.uv.index]]
|
|
62
|
+
name = "pypi"
|
|
63
|
+
url = "https://pypi.org/simple/"
|
|
64
|
+
default = true
|
|
65
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""csc_essentials package exports.
|
|
2
|
+
|
|
3
|
+
Este módulo agrega exports de utilitários presentes em `csc_essentials` para
|
|
4
|
+
facilitar importações do pacote (ex.: `from csc_essentials import ajusta_nome_coluna`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .utils import ajusta_nome_coluna, converter_valor
|
|
8
|
+
from .database import (
|
|
9
|
+
inserir_banco_dados,
|
|
10
|
+
atualiza_tabela,
|
|
11
|
+
executar_com_fallback,
|
|
12
|
+
atualiza_tabelas_silvers,
|
|
13
|
+
obter_schema_map,
|
|
14
|
+
)
|
|
15
|
+
from .logging import GeradorLog, AlertaSlack
|
|
16
|
+
from .core import parar_vm
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ajusta_nome_coluna",
|
|
20
|
+
"obter_schema_map",
|
|
21
|
+
"converter_valor",
|
|
22
|
+
"inserir_banco_dados",
|
|
23
|
+
"atualiza_tabela",
|
|
24
|
+
"executar_com_fallback",
|
|
25
|
+
"atualiza_tabelas_silvers",
|
|
26
|
+
"parar_vm",
|
|
27
|
+
"GeradorLog",
|
|
28
|
+
"AlertaSlack",
|
|
29
|
+
]
|
|
30
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Operações relacionadas a armazenamento/infra (Compute).
|
|
2
|
+
|
|
3
|
+
Este módulo contém helpers pequenos para operações em Google Compute.
|
|
4
|
+
"""
|
|
5
|
+
from google.cloud import compute_v1
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parar_vm(instance_name, _project, _zona, gerador_log):
|
|
9
|
+
"""Para uma instância do Compute Engine.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
instance_name (str): nome da VM a ser parada.
|
|
13
|
+
_project (str): projeto GCP onde a VM está localizada.
|
|
14
|
+
_zona (str): zona da VM.
|
|
15
|
+
gerador_log (GeradorLog): instância de `GeradorLog` para registrar mensagens.
|
|
16
|
+
|
|
17
|
+
Observação: a função registra eventos no `gerador_log` e tenta parar a VM
|
|
18
|
+
usando o cliente `compute_v1.InstancesClient().stop`.
|
|
19
|
+
"""
|
|
20
|
+
# Log local de início da operação
|
|
21
|
+
gerador_log.add(f"Parando {instance_name}...")
|
|
22
|
+
try:
|
|
23
|
+
# Chamada ao client do Compute para parar a instância
|
|
24
|
+
compute_v1.InstancesClient().stop(
|
|
25
|
+
project=_project,
|
|
26
|
+
zone=_zona,
|
|
27
|
+
instance=instance_name,
|
|
28
|
+
)
|
|
29
|
+
print("VM Parada.")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
# Registra erro sem levantar, para não interromper fluxos que chamam esta util
|
|
32
|
+
gerador_log.add(f"Erro parar VM: {e}", severity="ERROR")
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Helpers para interação com BigQuery.
|
|
2
|
+
|
|
3
|
+
Fornece funções para obter o schema de tabelas, carregar arquivos NDJSON
|
|
4
|
+
e executar rotinas de atualização de tabelas (ex.: recriar a tabela a partir
|
|
5
|
+
de uma view `_vw`).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .utils import executar_com_fallback, converter_valor
|
|
9
|
+
import json
|
|
10
|
+
from google.api_core import exceptions
|
|
11
|
+
from google.cloud import bigquery
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def obter_schema_map(bq_client, projeto, dataset, tabela, gerador_log):
|
|
15
|
+
"""
|
|
16
|
+
Busca o schema da tabela no BigQuery e retorna um dicionário {coluna: TIPO}.
|
|
17
|
+
Ex: {'data_emissao': 'DATE', 'valor': 'FLOAT', 'criado_em': 'DATETIME'}
|
|
18
|
+
Se a tabela não existir, retorna um dict vazio (fallback).
|
|
19
|
+
"""
|
|
20
|
+
tabela_completa = f"{projeto}.{dataset}.{tabela}"
|
|
21
|
+
schema_map = {}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
table = bq_client.get_table(tabela_completa)
|
|
25
|
+
for field in table.schema:
|
|
26
|
+
schema_map[field.name] = field.field_type
|
|
27
|
+
except exceptions.NotFound:
|
|
28
|
+
gerador_log.add(
|
|
29
|
+
f"Tabela {tabela} ainda não existe no BQ. Conversão inteligente desativada para este fluxo.",
|
|
30
|
+
severity="WARNING",
|
|
31
|
+
)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
gerador_log.add(f"Erro ao buscar schema de {tabela}: {e}", severity="ERROR")
|
|
34
|
+
|
|
35
|
+
return schema_map
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def inserir_banco_dados(dados=None, arquivo_path=None, projeto=None, dataset=None, tabela=None, tabela_silver="", gerador_log=None, client=None, wait_job=False):
|
|
39
|
+
"""Inserção universal no BigQuery.
|
|
40
|
+
|
|
41
|
+
Pode receber `dados` (lista/NDJSON/JSON) ou um `arquivo_path` (NDJSON) para upload.
|
|
42
|
+
- `client` deve ser um `google.cloud.bigquery.Client`.
|
|
43
|
+
- Se `arquivo_path` for fornecido, usa `load_table_from_file`.
|
|
44
|
+
- Se `dados` for fornecido, normaliza e usa `insert_rows_json`.
|
|
45
|
+
Retorna True em sucesso.
|
|
46
|
+
"""
|
|
47
|
+
endpoint_name = f"BigQuery-Insert ({tabela})"
|
|
48
|
+
|
|
49
|
+
# Preferência pelo arquivo: se arquivo_path é fornecido, faz upload em massa
|
|
50
|
+
if arquivo_path:
|
|
51
|
+
if client is None:
|
|
52
|
+
gerador_log.add("client do BigQuery não fornecido para upload de arquivo.", severity="ERROR")
|
|
53
|
+
raise ValueError("client obrigatório para upload de arquivo")
|
|
54
|
+
|
|
55
|
+
tabela_completa = f"{projeto}.{dataset}.{tabela}"
|
|
56
|
+
schema_destino = None
|
|
57
|
+
autodetect_config = True
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
table_ref = client.get_table(tabela_completa)
|
|
61
|
+
schema_destino = table_ref.schema
|
|
62
|
+
autodetect_config = False
|
|
63
|
+
except exceptions.NotFound:
|
|
64
|
+
# Tabela ainda não existe; manter autodetect_config=True para o BigQuery
|
|
65
|
+
# inferir automaticamente o schema durante o carregamento.
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
job_config = bigquery.LoadJobConfig(
|
|
69
|
+
source_format=bigquery.SourceFormat.NEWLINE_DELIMITED_JSON,
|
|
70
|
+
write_disposition=bigquery.WriteDisposition.WRITE_APPEND,
|
|
71
|
+
ignore_unknown_values=True,
|
|
72
|
+
autodetect=autodetect_config,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
with open(arquivo_path, "rb") as f:
|
|
76
|
+
job = client.load_table_from_file(f, f"{projeto}.{dataset}.{tabela}", job_config=job_config)
|
|
77
|
+
|
|
78
|
+
if wait_job:
|
|
79
|
+
job.result()
|
|
80
|
+
gerador_log.add(f"Upload finalizado para {tabela}", severity="DEBUG")
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Caso contrário, usa `dados` em memória
|
|
85
|
+
if dados is None:
|
|
86
|
+
gerador_log.add("AVISO: Nenhum dado ou arquivo fornecido para inserção.", severity="WARNING")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Normalize incoming `dados` into a list of dicts.
|
|
90
|
+
try:
|
|
91
|
+
if isinstance(dados, str):
|
|
92
|
+
raw = dados.strip()
|
|
93
|
+
if "\n" in raw:
|
|
94
|
+
lines = [ln for ln in raw.splitlines() if ln.strip()]
|
|
95
|
+
dados = [json.loads(ln) for ln in lines]
|
|
96
|
+
else:
|
|
97
|
+
parsed = json.loads(raw)
|
|
98
|
+
dados = parsed if isinstance(parsed, list) else [parsed]
|
|
99
|
+
|
|
100
|
+
elif isinstance(dados, (list, tuple)):
|
|
101
|
+
normalized = []
|
|
102
|
+
for idx, el in enumerate(dados):
|
|
103
|
+
if isinstance(el, str):
|
|
104
|
+
try:
|
|
105
|
+
normalized.append(json.loads(el))
|
|
106
|
+
except Exception as e:
|
|
107
|
+
gerador_log.add(f"Ignorando elemento {idx} que não é JSON válido: {e}", severity="WARNING")
|
|
108
|
+
elif isinstance(el, dict):
|
|
109
|
+
normalized.append(el)
|
|
110
|
+
else:
|
|
111
|
+
gerador_log.add(f"Ignorando elemento {idx} de tipo {type(el).__name__}", severity="WARNING")
|
|
112
|
+
dados = normalized
|
|
113
|
+
else:
|
|
114
|
+
gerador_log.add(f"Tipo de 'dados' inválido: {type(dados).__name__}", severity="ERROR")
|
|
115
|
+
raise TypeError("'dados' deve ser uma lista/tupla, dict ou string JSON/NDJSON")
|
|
116
|
+
|
|
117
|
+
if not dados:
|
|
118
|
+
gerador_log.add("AVISO: Após normalização, não há dados válidos para inserção.", severity="WARNING")
|
|
119
|
+
return
|
|
120
|
+
except Exception:
|
|
121
|
+
gerador_log.add("Erro ao parsear/normalizar 'dados' para inserção.", severity="ERROR")
|
|
122
|
+
raise
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
if client is None:
|
|
126
|
+
raise ValueError("Para inserção via 'dados', é necessário fornecer um cliente BigQuery válido.")
|
|
127
|
+
tabela_completa = f"{projeto}.{dataset}.{tabela}"
|
|
128
|
+
table_ref = client.get_table(tabela_completa)
|
|
129
|
+
schema = table_ref.schema
|
|
130
|
+
|
|
131
|
+
tamanho_bloco = 1000
|
|
132
|
+
total_registros = len(dados)
|
|
133
|
+
|
|
134
|
+
gerador_log.add(f"Iniciando inserção de {total_registros} registros em blocos de {tamanho_bloco}...")
|
|
135
|
+
|
|
136
|
+
inserted = 0
|
|
137
|
+
for i in range(0, total_registros, tamanho_bloco):
|
|
138
|
+
bloco = dados[i:i+tamanho_bloco]
|
|
139
|
+
bloco_ajustado = []
|
|
140
|
+
for item_idx, item in enumerate(bloco):
|
|
141
|
+
if not isinstance(item, dict):
|
|
142
|
+
gerador_log.add(f"Ignorando registro no bloco (indice {i + item_idx}) pois não é um dict.", severity="WARNING")
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
novo_item = {}
|
|
146
|
+
for campo in schema:
|
|
147
|
+
nome_coluna = campo.name
|
|
148
|
+
if nome_coluna in item:
|
|
149
|
+
# `converter_valor` expects a mapping (row->tipo). Wrap the
|
|
150
|
+
# single column value into a dict and pass a single-column
|
|
151
|
+
# schema so converter_valor can safely iterate `.items()`.
|
|
152
|
+
single_row = {nome_coluna: item.get(nome_coluna)}
|
|
153
|
+
single_schema = {nome_coluna: campo.field_type}
|
|
154
|
+
converted = converter_valor(single_row, single_schema, gerador_log)
|
|
155
|
+
novo_item[nome_coluna] = converted.get(nome_coluna)
|
|
156
|
+
else:
|
|
157
|
+
novo_item[nome_coluna] = None
|
|
158
|
+
bloco_ajustado.append(novo_item)
|
|
159
|
+
|
|
160
|
+
if not bloco_ajustado:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
errors = client.insert_rows_json(table_ref, bloco_ajustado)
|
|
164
|
+
if errors:
|
|
165
|
+
error_details = f"A API do BigQuery retornou erros ao inserir dados: {errors}"
|
|
166
|
+
gerador_log.add(error_details, severity="ERROR")
|
|
167
|
+
raise Exception(error_details)
|
|
168
|
+
|
|
169
|
+
inserted += len(bloco_ajustado)
|
|
170
|
+
|
|
171
|
+
gerador_log.add(f"{inserted} registros inseridos com sucesso!")
|
|
172
|
+
|
|
173
|
+
if tabela_silver:
|
|
174
|
+
att = atualiza_tabela(client, projeto, dataset, tabela_silver, gerador_log)
|
|
175
|
+
gerador_log.add(f'Tabela silver: {att}')
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
gerador_log.add('Sem Tabela Silver')
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
gerador_log.add(f"Erro crítico ao tentar inserir dados no BigQuery: {e}", severity="CRITICAL")
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
def atualiza_tabela(client, projeto, dataset, tabela, gerador_log):
|
|
186
|
+
"""Recria a tabela destino a partir da view correspondente (`<tabela>_vw`).
|
|
187
|
+
|
|
188
|
+
Este helper executa um `CREATE OR REPLACE TABLE <tabela> AS SELECT * FROM <tabela>_vw`.
|
|
189
|
+
"""
|
|
190
|
+
tabela_completa = f"{projeto}.{dataset}.{tabela}"
|
|
191
|
+
insert_sql = f"create or replace table `{tabela_completa}` as SELECT * FROM `{tabela_completa}_vw`;"
|
|
192
|
+
try:
|
|
193
|
+
client.query(insert_sql).result()
|
|
194
|
+
gerador_log.add(f"Tabela {tabela_completa} atualizada.")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
# Log de erro e re-raise para que chamadores possam detectar falha
|
|
197
|
+
gerador_log.add(f"Erro query {tabela_completa}: {e}", severity="ERROR")
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def atualiza_tabelas_silvers(client_t1, client_t2, project, dataset, tabelas_silvers, gerador_log, timeout_minutos=5):
|
|
202
|
+
"""Atualiza múltiplas "silvers" com fallback entre dois clientes.
|
|
203
|
+
|
|
204
|
+
A ideia é tentar atualizar cada tabela usando `client_t1`. Se houver falha
|
|
205
|
+
no primeiro cliente (timeout/falha), ativa `secundario` e passa a usar
|
|
206
|
+
`client_t2` para as próximas tabelas.
|
|
207
|
+
"""
|
|
208
|
+
gerador_log.add("Iniciando Silvers...")
|
|
209
|
+
timeout = timeout_minutos * 60
|
|
210
|
+
secundario = False
|
|
211
|
+
for tb in tabelas_silvers:
|
|
212
|
+
if not secundario:
|
|
213
|
+
# Tenta no client principal com timeout/fallback
|
|
214
|
+
res = executar_com_fallback(atualiza_tabela, client_t1, timeout, project, dataset, tb, gerador_log)
|
|
215
|
+
if res != "sucesso":
|
|
216
|
+
# Marca para usar o secundário a partir de agora
|
|
217
|
+
secundario = True
|
|
218
|
+
executar_com_fallback(atualiza_tabela, client_t2, timeout, project, dataset, tb, gerador_log)
|
|
219
|
+
else:
|
|
220
|
+
# Já está no secundário: executa diretamente
|
|
221
|
+
executar_com_fallback(atualiza_tabela, client_t2, timeout, project, dataset, tb, gerador_log)
|
|
222
|
+
gerador_log.add("Fim Silvers.")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import pytz
|
|
6
|
+
from google.cloud import logging as cloud_logging
|
|
7
|
+
from google.cloud import secretmanager
|
|
8
|
+
from slack_sdk import WebClient
|
|
9
|
+
from slack_sdk.errors import SlackApiError
|
|
10
|
+
from .secrets import get_secrets
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
"""Integração de logging e alertas (Slack / Cloud Logging).
|
|
14
|
+
|
|
15
|
+
Contém a classe `GeradorLog` usada para imprimir logs no console,
|
|
16
|
+
enviar entradas estruturadas ao Cloud Logging e acumular erros para
|
|
17
|
+
relatório no Slack via `AlertaSlack`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GeradorLog:
|
|
22
|
+
"""Gerador de logs usado por fluxos de dados.
|
|
23
|
+
|
|
24
|
+
- Imprime mensagens no console imediatamente.
|
|
25
|
+
- Se `GCP_LOGGING_ATIVO` estiver habilitado envia logs ao Cloud Logging.
|
|
26
|
+
- Erros podem ser acumulados e posteriormente enviados ao Slack.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, nome_do_log: str):
|
|
30
|
+
# Lock para operações thread-safe sobre a lista de erros acumulados
|
|
31
|
+
self.lock = threading.Lock()
|
|
32
|
+
self.gcp_logging_ativo = os.environ.get("GCP_LOGGING_ATIVO", "true").lower() == "true"
|
|
33
|
+
self.erros_acumulados = []
|
|
34
|
+
|
|
35
|
+
if self.gcp_logging_ativo:
|
|
36
|
+
try:
|
|
37
|
+
self.logging_client = cloud_logging.Client()
|
|
38
|
+
self.gcp_logger = self.logging_client.logger(nome_do_log)
|
|
39
|
+
print("INFO: Logging para o Google Cloud ATIVADO.")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"CRÍTICO: Falha ao inicializar o cliente do Cloud Logging. Erro: {e}")
|
|
42
|
+
self.logging_client = None
|
|
43
|
+
self.gcp_logger = None
|
|
44
|
+
else:
|
|
45
|
+
self.logging_client = None
|
|
46
|
+
self.gcp_logger = None
|
|
47
|
+
print("INFO: Logging para o Google Cloud DESATIVADO localmente.")
|
|
48
|
+
|
|
49
|
+
# Inicializa o alerta Slack (usa SLACK_SECRET_PATH por padrão)
|
|
50
|
+
self.alertador = AlertaSlack(os.environ.get("SLACK_SECRET_PATH", ""), self)
|
|
51
|
+
|
|
52
|
+
def add(self, message, severity="INFO", notify_now=False, **extra_data):
|
|
53
|
+
"""Adiciona uma entrada de log.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
message (str): mensagem do log.
|
|
57
|
+
severity (str): nível (INFO, DEBUG, ERROR, etc.).
|
|
58
|
+
notify_now (bool): se True, em caso de ERROR dispara alerta imediato ao Slack.
|
|
59
|
+
**extra_data: dados extras enviados ao Cloud Logging e/ou usados no alerta.
|
|
60
|
+
"""
|
|
61
|
+
with self.lock:
|
|
62
|
+
log_console_message = f"[{severity.upper()}] {message}"
|
|
63
|
+
if extra_data:
|
|
64
|
+
log_console_message += f" | Dados: {extra_data}"
|
|
65
|
+
|
|
66
|
+
# Impressão local (sempre)
|
|
67
|
+
print(log_console_message)
|
|
68
|
+
|
|
69
|
+
# Envia para Cloud Logging se configurado
|
|
70
|
+
if self.gcp_logger:
|
|
71
|
+
payload = {"message": message, **extra_data}
|
|
72
|
+
self.gcp_logger.log_struct(payload, severity=severity)
|
|
73
|
+
|
|
74
|
+
# Acumula ou envia alertas imediatos para severidades altas
|
|
75
|
+
if severity.upper() in ["ERROR", "CRITICAL", "ALERT"]:
|
|
76
|
+
timestamp = datetime.now(pytz.timezone("America/Sao_Paulo")).strftime("%H:%M:%S")
|
|
77
|
+
if notify_now:
|
|
78
|
+
self.alertador.envia_alerta(
|
|
79
|
+
consumo=extra_data.get("endpoint", "Processo Principal"),
|
|
80
|
+
data_execucao=datetime.now(pytz.timezone("America/Sao_Paulo")).strftime("%d/%m/%Y %H:%M:%S"),
|
|
81
|
+
exception_msg=message,
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
erro_formatado = f"[{timestamp}] {extra_data.get('endpoint', '')}: {message}"
|
|
85
|
+
self.erros_acumulados.append(erro_formatado)
|
|
86
|
+
|
|
87
|
+
def reportar_erros_acumulados(self):
|
|
88
|
+
"""Envia relatório consolidado dos erros acumulados para o Slack."""
|
|
89
|
+
with self.lock:
|
|
90
|
+
if not self.erros_acumulados:
|
|
91
|
+
print("INFO: Nenhum erro acumulado para reportar.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
qtd_erros = len(self.erros_acumulados)
|
|
95
|
+
detalhes = "\n".join(self.erros_acumulados[:20])
|
|
96
|
+
if qtd_erros > 20:
|
|
97
|
+
detalhes += f"\n... e mais {qtd_erros - 20} erros."
|
|
98
|
+
|
|
99
|
+
msg_final = (
|
|
100
|
+
"Houve "
|
|
101
|
+
f"{qtd_erros} falhas de requisição/página durante a execução.\n\n{detalhes}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print("INFO: Enviando relatório consolidado de erros para o Slack.")
|
|
105
|
+
self.alertador.envia_alerta(
|
|
106
|
+
consumo="Relatório de Erros em Lote",
|
|
107
|
+
data_execucao=datetime.now(pytz.timezone("America/Sao_Paulo")).strftime("%d/%m/%Y %H:%M:%S"),
|
|
108
|
+
exception_msg=msg_final,
|
|
109
|
+
)
|
|
110
|
+
# Limpa a lista após envio
|
|
111
|
+
self.erros_acumulados = []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AlertaSlack:
|
|
115
|
+
"""Cliente simples para envio de alertas ao Slack usando token em Secret Manager.
|
|
116
|
+
|
|
117
|
+
A classe busca um segredo (JSON) que deve conter a chave `token` e opcionalmente
|
|
118
|
+
`users` com menções.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, secret_path, gerador_log):
|
|
122
|
+
self.gerador_log = gerador_log
|
|
123
|
+
self.secret_path = secret_path
|
|
124
|
+
self.slack_client = None
|
|
125
|
+
self.channel_id = "dados_csc_tecnico"
|
|
126
|
+
self.users_to_mention = ""
|
|
127
|
+
self.alertas_ativos = os.environ.get("ALERTAS_SLACK_ATIVOS", "true").lower() == "true"
|
|
128
|
+
if self.alertas_ativos:
|
|
129
|
+
self._inicializar_cliente()
|
|
130
|
+
|
|
131
|
+
def _inicializar_cliente(self):
|
|
132
|
+
"""Inicializa o `WebClient` do Slack lendo o segredo do Secret Manager."""
|
|
133
|
+
try:
|
|
134
|
+
response = get_secrets(self.secret_path)
|
|
135
|
+
if not response:
|
|
136
|
+
print("CRÍTICO: Segredo do Slack não encontrado ou vazio.")
|
|
137
|
+
return
|
|
138
|
+
config = json.loads(response)
|
|
139
|
+
self.slack_client = WebClient(token=config.get("token"))
|
|
140
|
+
self.users_to_mention = config.get("users", "")
|
|
141
|
+
print("INFO: Cliente do Slack inicializado.")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"CRÍTICO: Erro Slack: {e}")
|
|
144
|
+
|
|
145
|
+
def envia_alerta(self, consumo, data_execucao, exception_msg):
|
|
146
|
+
"""Envia mensagem formatada para o canal do Slack configurado."""
|
|
147
|
+
if not self.alertas_ativos or not self.slack_client:
|
|
148
|
+
return
|
|
149
|
+
msg = (
|
|
150
|
+
"*:red_circle: Falha!*\n"
|
|
151
|
+
f"*Consumo:* `{consumo}`\n"
|
|
152
|
+
f"*Data:* `{data_execucao}`\n"
|
|
153
|
+
f"```{exception_msg}``` {self.users_to_mention}"
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
self.slack_client.chat_postMessage(channel=self.channel_id, text=msg)
|
|
157
|
+
except SlackApiError:
|
|
158
|
+
# Falha no envio do Slack é silenciosa (não queremos quebrar o fluxo)
|
|
159
|
+
pass
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Helpers para acessar o Google Secret Manager.
|
|
2
|
+
|
|
3
|
+
Fornece uma função utilitária simples para recuperar o payload de uma
|
|
4
|
+
versão de segredo como string (geralmente JSON).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from google.cloud import secretmanager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_secrets(secret_name):
|
|
12
|
+
"""Recupera o valor de um segredo do Secret Manager.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
secret_name (str): recurso completo da versão do segredo
|
|
16
|
+
(ex.: "projects/<proj>/secrets/<name>/versions/latest").
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str | None: valor do segredo decodificado ou `None` em caso de falha.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
sm_client = secretmanager.SecretManagerServiceClient()
|
|
23
|
+
response = sm_client.access_secret_version(request={"name": secret_name})
|
|
24
|
+
secret_value = response.payload.data.decode("UTF-8")
|
|
25
|
+
return secret_value
|
|
26
|
+
except Exception:
|
|
27
|
+
# Em caso de erro retornamos None para que chamadores possam tratar
|
|
28
|
+
return None
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Funções utilitárias: normalização, conversão e execução com timeout.
|
|
2
|
+
|
|
3
|
+
Contém helpers usados em pipelines para preparar dados antes do envio
|
|
4
|
+
ao BigQuery e para executar tarefas com timeout/fallback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
9
|
+
from unidecode import unidecode
|
|
10
|
+
import re
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ajusta_nome_coluna(nome_coluna):
|
|
15
|
+
"""Normaliza um nome de coluna para formato compatível com BigQuery.
|
|
16
|
+
|
|
17
|
+
- converte para minúsculas
|
|
18
|
+
- substitui espaços por `_`
|
|
19
|
+
- remove acentos e caracteres especiais
|
|
20
|
+
- compacta múltiplos underscores
|
|
21
|
+
"""
|
|
22
|
+
normalized = unidecode(nome_coluna.lower().replace(" ", "_"))
|
|
23
|
+
sem_caractere_especial = re.sub(r"[^a-zA-Z0-9_]", "", normalized)
|
|
24
|
+
return re.sub(r"_{2,}", "_", sem_caractere_especial)
|
|
25
|
+
|
|
26
|
+
|
|
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 e formata datas no padrão aceito pelo BigQuery.
|
|
33
|
+
- Em falhas de conversão registra avisos e armazena `None` no campo.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if not schema_map:
|
|
37
|
+
return row
|
|
38
|
+
|
|
39
|
+
nova_linha = {}
|
|
40
|
+
|
|
41
|
+
regex_data_br = re.compile(r"^(\d{2})/(\d{2})/(\d{4})$")
|
|
42
|
+
regex_iso_tz = re.compile(
|
|
43
|
+
r"^(\d{4}-\d{2}-\d{2})[T ]" r"(\d{2}:\d{2}:\d{2}(?:\.\d+)?)" r"(?:Z|[+-]\d{2}:?\d{2})?$"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for col, val in row.items():
|
|
47
|
+
# Valores vazios viram NULL
|
|
48
|
+
if val is None or val == "":
|
|
49
|
+
nova_linha[col] = None
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# Serializa estruturas complexas para JSON
|
|
53
|
+
if isinstance(val, (dict, list)):
|
|
54
|
+
try:
|
|
55
|
+
nova_linha[col] = json.dumps(val, ensure_ascii=False)
|
|
56
|
+
except Exception:
|
|
57
|
+
nova_linha[col] = str(val)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
tipo_bq = schema_map.get(col, "STRING")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
if tipo_bq == "STRING":
|
|
64
|
+
val_str = str(val).replace("\n", " ").replace("\r", "").strip()
|
|
65
|
+
nova_linha[col] = val_str
|
|
66
|
+
|
|
67
|
+
elif tipo_bq in ("INTEGER", "INT64"):
|
|
68
|
+
# Garante conversão segura string -> float -> int (ex: "10.0" vira 10)
|
|
69
|
+
nova_linha[col] = int(float(str(val)))
|
|
70
|
+
|
|
71
|
+
elif tipo_bq in ("FLOAT", "FLOAT64", "NUMERIC"):
|
|
72
|
+
# Limpeza inicial de moeda e espaços
|
|
73
|
+
val_str = str(val).replace("R$", "").replace(" ", "").strip()
|
|
74
|
+
|
|
75
|
+
# Trata traço como zero (comum em planilhas)
|
|
76
|
+
if val_str in ("-", ""):
|
|
77
|
+
nova_linha[col] = 0.0
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
# Converte formato brasileiro 1.000,00 -> 1000.00
|
|
81
|
+
if "," in val_str:
|
|
82
|
+
val_str = val_str.replace(".", "").replace(",", ".")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
valor_float = float(val_str)
|
|
86
|
+
|
|
87
|
+
# Verifica NaN/Infinity
|
|
88
|
+
if math.isnan(valor_float) or math.isinf(valor_float):
|
|
89
|
+
nova_linha[col] = None
|
|
90
|
+
else:
|
|
91
|
+
nova_linha[col] = valor_float
|
|
92
|
+
except ValueError:
|
|
93
|
+
gerador_log.add(
|
|
94
|
+
f"AVISO: Não foi possível converter '{val}' para FLOAT. Coluna: {col}. Enviando NULL.",
|
|
95
|
+
severity="WARNING",
|
|
96
|
+
)
|
|
97
|
+
nova_linha[col] = None
|
|
98
|
+
|
|
99
|
+
elif tipo_bq in ("BOOLEAN", "BOOL"):
|
|
100
|
+
nova_linha[col] = str(val).lower() in ("true", "1", "t", "yes")
|
|
101
|
+
|
|
102
|
+
elif tipo_bq == "DATE":
|
|
103
|
+
val_str = str(val).strip()
|
|
104
|
+
match_br = regex_data_br.match(val_str)
|
|
105
|
+
if match_br:
|
|
106
|
+
nova_linha[col] = f"{match_br.group(3)}-{match_br.group(2)}-{match_br.group(1)}"
|
|
107
|
+
else:
|
|
108
|
+
nova_linha[col] = val_str[:10]
|
|
109
|
+
|
|
110
|
+
elif tipo_bq == "DATETIME":
|
|
111
|
+
val_str = str(val).strip()
|
|
112
|
+
match_iso = regex_iso_tz.match(val_str)
|
|
113
|
+
if match_iso:
|
|
114
|
+
nova_linha[col] = f"{match_iso.group(1)} {match_iso.group(2)}"
|
|
115
|
+
else:
|
|
116
|
+
# Limpeza robusta de timezone para evitar erro de formato
|
|
117
|
+
limpo = val_str.replace("T", " ").replace("Z", "")
|
|
118
|
+
# Remove offsets como -03:00 ou +00:00 manualmente se regex falhar
|
|
119
|
+
if "+" in limpo:
|
|
120
|
+
limpo = limpo.split("+")[0]
|
|
121
|
+
if "-" in limpo and len(limpo.split("-")[-1]) == 5: # tenta pegar offsets tipo -03:00
|
|
122
|
+
limpo = limpo.rsplit("-", 1)[0]
|
|
123
|
+
nova_linha[col] = limpo.strip()[:23]
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
# Tipo não tratado: devolve o valor como veio
|
|
127
|
+
nova_linha[col] = val
|
|
128
|
+
|
|
129
|
+
except Exception:
|
|
130
|
+
# Se falhar a conversão, registra aviso e envia NULL para evitar erros BQ
|
|
131
|
+
gerador_log.add(
|
|
132
|
+
f"AVISO: Falha conversão coluna '{col}' valor '{val}'. Enviando NULL."
|
|
133
|
+
)
|
|
134
|
+
nova_linha[col] = None
|
|
135
|
+
|
|
136
|
+
return nova_linha
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def executar_com_fallback(func, bq_client, timeout, project, dataset, tabela, gerador_log):
|
|
140
|
+
"""Executa `func` em um executor com timeout e captura falhas.
|
|
141
|
+
|
|
142
|
+
Retorna uma string entre: `"sucesso"`, `"timeout"`, `"falha"`.
|
|
143
|
+
"""
|
|
144
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
145
|
+
future = executor.submit(func, bq_client, project, dataset, tabela, gerador_log)
|
|
146
|
+
shutdown_done = False
|
|
147
|
+
try:
|
|
148
|
+
future.result(timeout=timeout)
|
|
149
|
+
gerador_log.add(f"✅ {func.__name__} concluída no projeto {bq_client.project}")
|
|
150
|
+
return "sucesso"
|
|
151
|
+
except TimeoutError:
|
|
152
|
+
# Cancela a execução e encerra o executor sem bloquear,
|
|
153
|
+
# garantindo que o timeout seja efetivo.
|
|
154
|
+
future.cancel()
|
|
155
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
156
|
+
shutdown_done = True
|
|
157
|
+
return "timeout"
|
|
158
|
+
except Exception as e:
|
|
159
|
+
gerador_log.add(f"❌ Erro {func.__name__}: {e}")
|
|
160
|
+
return "falha"
|
|
161
|
+
finally:
|
|
162
|
+
if not shutdown_done:
|
|
163
|
+
# Encerra o executor normalmente nos demais cenários.
|
|
164
|
+
executor.shutdown(wait=True, cancel_futures=True)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csc-essentials
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Biblioteca de utilitários essenciais para projetos CSC
|
|
5
|
+
Author-email: David Braga <david.braga@stone.com.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: slack-sdk>=3.0.0
|
|
19
|
+
Requires-Dist: google-cloud-secret-manager>=2.0.0
|
|
20
|
+
Requires-Dist: google-cloud-compute>=1.15.0
|
|
21
|
+
Requires-Dist: google-cloud-storage>=2.10.0
|
|
22
|
+
Requires-Dist: google-cloud-bigquery>=3.10.0
|
|
23
|
+
Requires-Dist: google-cloud-logging>=3.5.0
|
|
24
|
+
Requires-Dist: google-api-core>=2.11.0
|
|
25
|
+
Requires-Dist: pytz>=2023.3
|
|
26
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
27
|
+
Requires-Dist: unidecode>=1.3.6
|
|
28
|
+
Requires-Dist: pandas>=2.0.0
|
|
29
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
30
|
+
Requires-Dist: protobuf>=5.0.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# csc-essentials
|
|
42
|
+
|
|
43
|
+
Pequeno utilitário para integração com Google Cloud (BigQuery, Secret Manager, Cloud Logging, Compute).
|
|
44
|
+
|
|
45
|
+
## Instalação
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install csc-essentials
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart (exemplo rápido)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from google.cloud import bigquery
|
|
55
|
+
from csc_essentials.logging import GeradorLog
|
|
56
|
+
from csc_essentials.secrets import get_secrets
|
|
57
|
+
from csc_essentials.database import inserir_banco_dados
|
|
58
|
+
|
|
59
|
+
log = GeradorLog("exemplo")
|
|
60
|
+
# Ler segredo do Secret Manager (ex.: token Slack)
|
|
61
|
+
raw = get_secrets("projects/MEU_PROJ/secrets/SLACK_SECRET/versions/latest")
|
|
62
|
+
|
|
63
|
+
# Preparar cliente BigQuery
|
|
64
|
+
client = bigquery.Client()
|
|
65
|
+
|
|
66
|
+
# Carregar arquivo NDJSON para uma tabela
|
|
67
|
+
inserir_banco_dados(
|
|
68
|
+
arquivo_path="/tmp/dados.ndjson",
|
|
69
|
+
projeto="meu-projeto",
|
|
70
|
+
dataset="meu_dataset",
|
|
71
|
+
tabela="minha_tabela",
|
|
72
|
+
log=log,
|
|
73
|
+
client=client,
|
|
74
|
+
wait_job=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print("Upload iniciado")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentação
|
|
81
|
+
|
|
82
|
+
- Documentação por módulo em `docs/` (veja `docs/overview.md`).
|
|
83
|
+
- Principais módulos: `logging`, `secrets`, `utils`, `database`.
|
|
84
|
+
|
|
85
|
+
## Contribuindo
|
|
86
|
+
|
|
87
|
+
- Abra uma issue descrevendo o problema ou feature.
|
|
88
|
+
- Envie um Pull Request com testes e descrição clara das mudanças.
|
|
89
|
+
- Formato de código: siga o estilo existing do projeto e execute os testes (`pytest`).
|
|
90
|
+
|
|
91
|
+
## Licença
|
|
92
|
+
|
|
93
|
+
Este repositório está licenciado conforme o arquivo `LICENSE` na raiz.
|
|
94
|
+
|
|
95
|
+
## Notas rápidas
|
|
96
|
+
|
|
97
|
+
- Verifique permissões GCP ao usar `bigquery.Client()` e `secretmanager.SecretManagerServiceClient`.
|
|
98
|
+
- Em desenvolvimento local, você pode desativar o envio para Cloud Logging configurando `GCP_LOGGING_ATIVO=false`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/csc_essentials/__init__.py
|
|
5
|
+
src/csc_essentials/core.py
|
|
6
|
+
src/csc_essentials/database.py
|
|
7
|
+
src/csc_essentials/logging.py
|
|
8
|
+
src/csc_essentials/secrets.py
|
|
9
|
+
src/csc_essentials/utils.py
|
|
10
|
+
src/csc_essentials.egg-info/PKG-INFO
|
|
11
|
+
src/csc_essentials.egg-info/SOURCES.txt
|
|
12
|
+
src/csc_essentials.egg-info/dependency_links.txt
|
|
13
|
+
src/csc_essentials.egg-info/requires.txt
|
|
14
|
+
src/csc_essentials.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
csc_essentials
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: csc-essentials
|
|
3
|
-
Version: 0.1.0.dev3
|
|
4
|
-
Summary: Biblioteca de utilitários essenciais para projetos CSC
|
|
5
|
-
Author-email: Seu Nome <seuemail@exemplo.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Requires-Python: >=3.9
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Requires-Dist: slack-sdk>=3.0.0
|
|
19
|
-
Requires-Dist: google-cloud-secret-manager>=2.0.0
|
|
20
|
-
Requires-Dist: google-cloud-compute>=1.15.0
|
|
21
|
-
Requires-Dist: google-cloud-storage>=2.10.0
|
|
22
|
-
Requires-Dist: google-cloud-bigquery>=3.10.0
|
|
23
|
-
Requires-Dist: google-cloud-logging>=3.5.0
|
|
24
|
-
Requires-Dist: google-api-core>=2.11.0
|
|
25
|
-
Requires-Dist: pytz>=2023.3
|
|
26
|
-
Requires-Dist: python-dateutil>=2.8.2
|
|
27
|
-
Requires-Dist: unidecode>=1.3.6
|
|
28
|
-
Requires-Dist: pandas>=2.0.0
|
|
29
|
-
Requires-Dist: openpyxl>=3.1.0
|
|
30
|
-
Requires-Dist: setuptools>=61.0.0
|
|
31
|
-
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
-
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
35
|
-
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
36
|
-
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
37
|
-
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
38
|
-
Dynamic: license-file
|
|
File without changes
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: csc-essentials
|
|
3
|
-
Version: 0.1.0.dev3
|
|
4
|
-
Summary: Biblioteca de utilitários essenciais para projetos CSC
|
|
5
|
-
Author-email: Seu Nome <seuemail@exemplo.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Requires-Python: >=3.9
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Requires-Dist: slack-sdk>=3.0.0
|
|
19
|
-
Requires-Dist: google-cloud-secret-manager>=2.0.0
|
|
20
|
-
Requires-Dist: google-cloud-compute>=1.15.0
|
|
21
|
-
Requires-Dist: google-cloud-storage>=2.10.0
|
|
22
|
-
Requires-Dist: google-cloud-bigquery>=3.10.0
|
|
23
|
-
Requires-Dist: google-cloud-logging>=3.5.0
|
|
24
|
-
Requires-Dist: google-api-core>=2.11.0
|
|
25
|
-
Requires-Dist: pytz>=2023.3
|
|
26
|
-
Requires-Dist: python-dateutil>=2.8.2
|
|
27
|
-
Requires-Dist: unidecode>=1.3.6
|
|
28
|
-
Requires-Dist: pandas>=2.0.0
|
|
29
|
-
Requires-Dist: openpyxl>=3.1.0
|
|
30
|
-
Requires-Dist: setuptools>=61.0.0
|
|
31
|
-
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
-
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
35
|
-
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
36
|
-
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
37
|
-
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
38
|
-
Dynamic: license-file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
{csc_essentials-0.1.0.dev3 → csc_essentials-0.2.3}/src/csc_essentials.egg-info/dependency_links.txt
RENAMED
|
File without changes
|