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.
@@ -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.1.0.dev3"
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" # Alterado para maior compatibilidade, ajuste se necessário
11
- authors = [ { name = "Seu Nome", email = "seuemail@exemplo.com" } ]
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
- "setuptools>=61.0.0", # Removido o excesso da versão 80.x, a menos que precise de algo muito específico
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
@@ -10,7 +10,7 @@ python-dateutil>=2.8.2
10
10
  unidecode>=1.3.6
11
11
  pandas>=2.0.0
12
12
  openpyxl>=3.1.0
13
- setuptools>=61.0.0
13
+ protobuf>=5.0.0
14
14
 
15
15
  [dev]
16
16
  pytest>=7.0.0
@@ -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,8 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- src/csc_essentials.egg-info/PKG-INFO
5
- src/csc_essentials.egg-info/SOURCES.txt
6
- src/csc_essentials.egg-info/dependency_links.txt
7
- src/csc_essentials.egg-info/requires.txt
8
- src/csc_essentials.egg-info/top_level.txt