antigravity-lite 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.
- antigravity_lite-0.1.0/PKG-INFO +105 -0
- antigravity_lite-0.1.0/README.md +93 -0
- antigravity_lite-0.1.0/pyproject.toml +25 -0
- antigravity_lite-0.1.0/setup.cfg +4 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite/__init__.py +0 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite/auditor/__init__.py +0 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite/auditor/finops.py +142 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite/io/__init__.py +0 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite/io/s3_finalizer.py +165 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite.egg-info/PKG-INFO +105 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite.egg-info/SOURCES.txt +12 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite.egg-info/dependency_links.txt +1 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite.egg-info/requires.txt +1 -0
- antigravity_lite-0.1.0/src_lite/antigravity_lite.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: antigravity-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Herramienta open-source para auditar despilfarros financieros en clústeres PySpark (AWS Glue) y manipular S3.
|
|
5
|
+
Author-email: Arquitecto B2B <arquitecto@antigravity.dev>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: boto3>=1.26.0
|
|
12
|
+
|
|
13
|
+
# 🛸 Antigravity PySpark Framework
|
|
14
|
+
|
|
15
|
+
Enterprise framework para resolver problemas complejos de Big Data en AWS Glue y entornos PySpark puros sin esfuerzo. Este proyecto estandariza las mejores prácticas de la industria con un solo `import`.
|
|
16
|
+
|
|
17
|
+
## 📦 Empaquetado y Licenciamiento Comercial (SaaS B2B)
|
|
18
|
+
|
|
19
|
+
Si deseas vender y distribuir este framework a una empresa externa, es imperativo **proteger tu Propiedad Intelectual** (ofuscar el código fuente) y **venderlo con una licencia temporal** (para que te paguen la renovación).
|
|
20
|
+
|
|
21
|
+
### 1. Ofuscar el código y armar la "Bomba de Tiempo" (Expiración)
|
|
22
|
+
Antes de construir el paquete, encripta el código fuente y configúrale la fecha de vencimiento exacta del contrato del cliente (ej. 31 de Diciembre de 2027):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Asegúrate de estar en el entorno virtual
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
|
|
28
|
+
# 2. Generar el binario comercial ofuscado (.whl) con Bomba de Tiempo (2027)
|
|
29
|
+
python build_commercial.py
|
|
30
|
+
```
|
|
31
|
+
Este script (que te escribí a la medida) automatizará por ti la encriptación con PyArmor y el empaquetado directo hacia la carpeta `dist/`. Cualquier ingeniero externo que intente abrir tus archivos allí dentro verá código basura indescifrable.
|
|
32
|
+
|
|
33
|
+
### 2. Construcción de uso interno / Open-Source (`.whl`)
|
|
34
|
+
Para el empaquetado inicial (sin ofuscación) para probar el framework dentro de tu propio AWS Glue, ejecuta el empaquetado estándar de Python:
|
|
35
|
+
```bash
|
|
36
|
+
# Construye el antigravity-0.1.0-py3-none-any.whl en la carpeta dist/
|
|
37
|
+
pip install build
|
|
38
|
+
python -m build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Guía de Uso Rápido para Ingenieros
|
|
44
|
+
|
|
45
|
+
### Motor A: Optimizador Core (`DataFrameChunker`)
|
|
46
|
+
**¿Cuándo usarlo?** Cuando lidias con DataFrames anchos (Wide DataFrames) o realizas analítica que provoca errores de `OutOfMemory` (OOM), demoras extremas, o fallos de `StackOverflowError`. Funciona fluidamente con cualquier volumen de datos y columnas, auto-regulando la memoria del clúster para acelerar los cruces matemáticos pesados.
|
|
47
|
+
|
|
48
|
+
**¿Cómo funciona?**
|
|
49
|
+
Particiona el DataFrame verticalmente e inyecta `localCheckpoint()` para aislar la memoria del Catalyst Optimizer.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from antigravity.core import DataFrameChunker
|
|
53
|
+
|
|
54
|
+
# 1. Instancias el chunker indicando la llave primaria y cuántas columnas procesar a la vez.
|
|
55
|
+
chunker = DataFrameChunker(df_masivo, id_cols=["id_cliente"], chunk_size=50)
|
|
56
|
+
|
|
57
|
+
# 2. Defines tú lógica matemática. Spark solo le pasará 50 variables por iteración.
|
|
58
|
+
def mi_logica(chunk_df, indice):
|
|
59
|
+
# Calcula algo y retorna un DF más pequeño
|
|
60
|
+
return chunk_df.select("id_cliente", ...)
|
|
61
|
+
|
|
62
|
+
# 3. El framework ejecuta el bucle de forma segura en el clúster
|
|
63
|
+
resultados_parciales = chunker.process_chunks(mi_logica)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Motor B: Generador Legacy (`LegacyTextTransformer`)
|
|
67
|
+
**¿Cuándo usarlo?** Cuando el negocio, banco, o gobierno te exige generar archivos `.TXT` planos, asimétricos o posicionales a partir de datos tabulares modernos.
|
|
68
|
+
|
|
69
|
+
**¿Cómo funciona?**
|
|
70
|
+
Recibe el DataFrame y usa `RDD.flatMap` para aplicar lógica de python puro distribuidamente.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from antigravity.legacy import LegacyTextTransformer
|
|
74
|
+
|
|
75
|
+
# 1. Escribes las reglas del negocio (1 Fila de Spark = N líneas de texto)
|
|
76
|
+
def reglas_bancarias(fila):
|
|
77
|
+
yield f"HEADER_{fila.id}"
|
|
78
|
+
yield f"BODY_{fila.saldo}"
|
|
79
|
+
|
|
80
|
+
# 2. Invocas el framework
|
|
81
|
+
transformer = LegacyTextTransformer(df)
|
|
82
|
+
|
|
83
|
+
# 3. Generas la salida
|
|
84
|
+
transformer.save_as_text(reglas_bancarias, "s3://mi-bucket/banco_txt/")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Motor IO: Smart Exporter (`S3Finalizer`)
|
|
88
|
+
**¿Cuándo usarlo?** Cuando detestas que PySpark guarde tus archivos como `part-0000X.csv` y deje basura como los archivos `_SUCCESS` en tu S3.
|
|
89
|
+
|
|
90
|
+
**¿Cómo funciona?**
|
|
91
|
+
Se conecta mediante `boto3` para listar, ordenar, secuenciar y renombrar la salida de Spark.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from antigravity.io import S3Finalizer
|
|
95
|
+
|
|
96
|
+
# 1. Escribes tu DataFrame normal en Spark
|
|
97
|
+
df.write.parquet("s3://mi-bucket/salida_sucia/")
|
|
98
|
+
|
|
99
|
+
# 2. Invocas el framework para secuenciarlos inteligentemente e inyectar el formato
|
|
100
|
+
finalizador = S3Finalizer(bucket_name="mi-bucket")
|
|
101
|
+
finalizador.sequence_spark_outputs(
|
|
102
|
+
s3_prefix="salida_sucia/",
|
|
103
|
+
pattern="MI_EMPRESA_DATA_{seq:04d}.parquet" # Resultado: MI_EMPRESA_DATA_0001.parquet
|
|
104
|
+
)
|
|
105
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 🛸 Antigravity PySpark Framework
|
|
2
|
+
|
|
3
|
+
Enterprise framework para resolver problemas complejos de Big Data en AWS Glue y entornos PySpark puros sin esfuerzo. Este proyecto estandariza las mejores prácticas de la industria con un solo `import`.
|
|
4
|
+
|
|
5
|
+
## 📦 Empaquetado y Licenciamiento Comercial (SaaS B2B)
|
|
6
|
+
|
|
7
|
+
Si deseas vender y distribuir este framework a una empresa externa, es imperativo **proteger tu Propiedad Intelectual** (ofuscar el código fuente) y **venderlo con una licencia temporal** (para que te paguen la renovación).
|
|
8
|
+
|
|
9
|
+
### 1. Ofuscar el código y armar la "Bomba de Tiempo" (Expiración)
|
|
10
|
+
Antes de construir el paquete, encripta el código fuente y configúrale la fecha de vencimiento exacta del contrato del cliente (ej. 31 de Diciembre de 2027):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Asegúrate de estar en el entorno virtual
|
|
14
|
+
source .venv/bin/activate
|
|
15
|
+
|
|
16
|
+
# 2. Generar el binario comercial ofuscado (.whl) con Bomba de Tiempo (2027)
|
|
17
|
+
python build_commercial.py
|
|
18
|
+
```
|
|
19
|
+
Este script (que te escribí a la medida) automatizará por ti la encriptación con PyArmor y el empaquetado directo hacia la carpeta `dist/`. Cualquier ingeniero externo que intente abrir tus archivos allí dentro verá código basura indescifrable.
|
|
20
|
+
|
|
21
|
+
### 2. Construcción de uso interno / Open-Source (`.whl`)
|
|
22
|
+
Para el empaquetado inicial (sin ofuscación) para probar el framework dentro de tu propio AWS Glue, ejecuta el empaquetado estándar de Python:
|
|
23
|
+
```bash
|
|
24
|
+
# Construye el antigravity-0.1.0-py3-none-any.whl en la carpeta dist/
|
|
25
|
+
pip install build
|
|
26
|
+
python -m build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Guía de Uso Rápido para Ingenieros
|
|
32
|
+
|
|
33
|
+
### Motor A: Optimizador Core (`DataFrameChunker`)
|
|
34
|
+
**¿Cuándo usarlo?** Cuando lidias con DataFrames anchos (Wide DataFrames) o realizas analítica que provoca errores de `OutOfMemory` (OOM), demoras extremas, o fallos de `StackOverflowError`. Funciona fluidamente con cualquier volumen de datos y columnas, auto-regulando la memoria del clúster para acelerar los cruces matemáticos pesados.
|
|
35
|
+
|
|
36
|
+
**¿Cómo funciona?**
|
|
37
|
+
Particiona el DataFrame verticalmente e inyecta `localCheckpoint()` para aislar la memoria del Catalyst Optimizer.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from antigravity.core import DataFrameChunker
|
|
41
|
+
|
|
42
|
+
# 1. Instancias el chunker indicando la llave primaria y cuántas columnas procesar a la vez.
|
|
43
|
+
chunker = DataFrameChunker(df_masivo, id_cols=["id_cliente"], chunk_size=50)
|
|
44
|
+
|
|
45
|
+
# 2. Defines tú lógica matemática. Spark solo le pasará 50 variables por iteración.
|
|
46
|
+
def mi_logica(chunk_df, indice):
|
|
47
|
+
# Calcula algo y retorna un DF más pequeño
|
|
48
|
+
return chunk_df.select("id_cliente", ...)
|
|
49
|
+
|
|
50
|
+
# 3. El framework ejecuta el bucle de forma segura en el clúster
|
|
51
|
+
resultados_parciales = chunker.process_chunks(mi_logica)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Motor B: Generador Legacy (`LegacyTextTransformer`)
|
|
55
|
+
**¿Cuándo usarlo?** Cuando el negocio, banco, o gobierno te exige generar archivos `.TXT` planos, asimétricos o posicionales a partir de datos tabulares modernos.
|
|
56
|
+
|
|
57
|
+
**¿Cómo funciona?**
|
|
58
|
+
Recibe el DataFrame y usa `RDD.flatMap` para aplicar lógica de python puro distribuidamente.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from antigravity.legacy import LegacyTextTransformer
|
|
62
|
+
|
|
63
|
+
# 1. Escribes las reglas del negocio (1 Fila de Spark = N líneas de texto)
|
|
64
|
+
def reglas_bancarias(fila):
|
|
65
|
+
yield f"HEADER_{fila.id}"
|
|
66
|
+
yield f"BODY_{fila.saldo}"
|
|
67
|
+
|
|
68
|
+
# 2. Invocas el framework
|
|
69
|
+
transformer = LegacyTextTransformer(df)
|
|
70
|
+
|
|
71
|
+
# 3. Generas la salida
|
|
72
|
+
transformer.save_as_text(reglas_bancarias, "s3://mi-bucket/banco_txt/")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Motor IO: Smart Exporter (`S3Finalizer`)
|
|
76
|
+
**¿Cuándo usarlo?** Cuando detestas que PySpark guarde tus archivos como `part-0000X.csv` y deje basura como los archivos `_SUCCESS` en tu S3.
|
|
77
|
+
|
|
78
|
+
**¿Cómo funciona?**
|
|
79
|
+
Se conecta mediante `boto3` para listar, ordenar, secuenciar y renombrar la salida de Spark.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from antigravity.io import S3Finalizer
|
|
83
|
+
|
|
84
|
+
# 1. Escribes tu DataFrame normal en Spark
|
|
85
|
+
df.write.parquet("s3://mi-bucket/salida_sucia/")
|
|
86
|
+
|
|
87
|
+
# 2. Invocas el framework para secuenciarlos inteligentemente e inyectar el formato
|
|
88
|
+
finalizador = S3Finalizer(bucket_name="mi-bucket")
|
|
89
|
+
finalizador.sequence_spark_outputs(
|
|
90
|
+
s3_prefix="salida_sucia/",
|
|
91
|
+
pattern="MI_EMPRESA_DATA_{seq:04d}.parquet" # Resultado: MI_EMPRESA_DATA_0001.parquet
|
|
92
|
+
)
|
|
93
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "antigravity-lite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Herramienta open-source para auditar despilfarros financieros en clústeres PySpark (AWS Glue) y manipular S3."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Arquitecto B2B", email = "arquitecto@antigravity.dev" }
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"boto3>=1.26.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src_lite"}
|
|
25
|
+
packages = ["antigravity_lite", "antigravity_lite.io", "antigravity_lite.auditor"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class AgAuditor:
|
|
8
|
+
"""
|
|
9
|
+
Antigravity Lite - Glue Cost & FinOps Auditor
|
|
10
|
+
========================================
|
|
11
|
+
Herramienta de auditoría financiera y operativa para AWS Glue.
|
|
12
|
+
Evalúa si estás pagando sobrecostos masivos en la nube debido a bucles infinitos
|
|
13
|
+
o configuraciones de RAM infladas para encubrir fallos del Catalyst Optimizer.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_dpu_price(region: str) -> float:
|
|
18
|
+
# Standard AWS Glue DPU per hour cost
|
|
19
|
+
return 0.44
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def run_aws_audit(cls, region="us-east-1", dias_analisis=7):
|
|
23
|
+
print(f"\n🔍 [ANTIGRAVITY LITE] Iniciando Auditoría FinOps (Región: {region})...")
|
|
24
|
+
print(f"Analizando métricas de los últimos {dias_analisis} días...\n")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
glue_client = boto3.client('glue', region_name=region)
|
|
28
|
+
cw_client = boto3.client('cloudwatch', region_name=region)
|
|
29
|
+
glue_client.get_jobs(MaxResults=1)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print("❌ Error de Autenticación AWS. Verifique sus credenciales Boto3.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
tiempo_limite = datetime.now(timezone.utc) - timedelta(days=dias_analisis)
|
|
35
|
+
|
|
36
|
+
jobs = []
|
|
37
|
+
paginator = glue_client.get_paginator('get_jobs')
|
|
38
|
+
for page in paginator.paginate():
|
|
39
|
+
for job in page.get('Jobs', []):
|
|
40
|
+
jobs.append(job['Name'])
|
|
41
|
+
|
|
42
|
+
if not jobs:
|
|
43
|
+
print("No se encontraron Jobs de AWS Glue desplegados.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print(f"📡 Se descubrieron {len(jobs)} Jobs. Calculando desperdicio (DPUs)...\n")
|
|
47
|
+
|
|
48
|
+
costo_dpu_hora = cls.get_dpu_price(region)
|
|
49
|
+
resultados = []
|
|
50
|
+
|
|
51
|
+
print(f"{'NOMBRE DEL JOB':<35} | {'WORKER TYPE':<12} | {'DPUs':<6} | {'MAX RAM %':<12} | {'GASTO EST. (7d)'}")
|
|
52
|
+
print("-" * 90)
|
|
53
|
+
|
|
54
|
+
for job_name in jobs:
|
|
55
|
+
runs_validos = 0
|
|
56
|
+
total_horas_ejecucion = 0
|
|
57
|
+
dpus_reales_promedio = 0
|
|
58
|
+
ultimo_worker_type = "N/A"
|
|
59
|
+
|
|
60
|
+
runs_paginator = glue_client.get_paginator('get_job_runs')
|
|
61
|
+
try:
|
|
62
|
+
for page in runs_paginator.paginate(JobName=job_name):
|
|
63
|
+
for run in page.get('JobRuns', []):
|
|
64
|
+
if run.get('StartedOn', datetime.min.replace(tzinfo=timezone.utc)) < tiempo_limite:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
runs_validos += 1
|
|
68
|
+
|
|
69
|
+
if 'CompletedOn' in run and 'StartedOn' in run:
|
|
70
|
+
duracion_segundos = (run['CompletedOn'] - run['StartedOn']).total_seconds()
|
|
71
|
+
total_horas_ejecucion += (duracion_segundos / 3600.0)
|
|
72
|
+
|
|
73
|
+
worker_type = run.get('WorkerType', 'G.1X')
|
|
74
|
+
num_workers = run.get('NumberOfWorkers', run.get('MaxCapacity', 2))
|
|
75
|
+
|
|
76
|
+
peso_dpu = {'Standard': 1, 'G.1X': 1, 'G.2X': 2, 'G.4X': 4, 'G.8X': 8, 'Z.2X': 2}
|
|
77
|
+
dpus_reales = num_workers * peso_dpu.get(worker_type, 1)
|
|
78
|
+
|
|
79
|
+
dpus_reales_promedio = dpus_reales
|
|
80
|
+
ultimo_worker_type = worker_type
|
|
81
|
+
|
|
82
|
+
except Exception:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if runs_validos == 0:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
max_memory_percent = 0.0
|
|
89
|
+
try:
|
|
90
|
+
metric_response = cw_client.get_metric_statistics(
|
|
91
|
+
Namespace='Glue',
|
|
92
|
+
MetricName='glue.driver.jvm.heap.usage',
|
|
93
|
+
Dimensions=[
|
|
94
|
+
{'Name': 'JobName', 'Value': job_name},
|
|
95
|
+
{'Name': 'Type', 'Value': 'gauge'}
|
|
96
|
+
],
|
|
97
|
+
StartTime=tiempo_limite,
|
|
98
|
+
EndTime=datetime.now(timezone.utc),
|
|
99
|
+
Period=3600,
|
|
100
|
+
Statistics=['Maximum']
|
|
101
|
+
)
|
|
102
|
+
datapoints = metric_response.get('Datapoints', [])
|
|
103
|
+
if datapoints:
|
|
104
|
+
max_memory_percent = max([dp['Maximum'] for dp in datapoints]) * 100
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
gasto_total = total_horas_ejecucion * dpus_reales_promedio * costo_dpu_hora
|
|
109
|
+
|
|
110
|
+
if gasto_total > 0:
|
|
111
|
+
resultados.append({
|
|
112
|
+
"nombre": job_name,
|
|
113
|
+
"runs": runs_validos,
|
|
114
|
+
"dpus": dpus_reales_promedio,
|
|
115
|
+
"worker_type": ultimo_worker_type,
|
|
116
|
+
"memoria": max_memory_percent,
|
|
117
|
+
"gasto": gasto_total
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
resultados = sorted(resultados, key=lambda x: x["gasto"], reverse=True)
|
|
121
|
+
|
|
122
|
+
total_desperdicio = 0
|
|
123
|
+
for res in resultados[:10]:
|
|
124
|
+
mem_str = f"{res['memoria']:.1f}%" if res['memoria'] > 0 else "N/A"
|
|
125
|
+
|
|
126
|
+
if res['memoria'] > 85.0 or res['worker_type'] in ['G.4X', 'G.8X']:
|
|
127
|
+
total_desperdicio += (res['gasto'] * 0.6)
|
|
128
|
+
mem_str += " ⚠️ RIESGO AST"
|
|
129
|
+
|
|
130
|
+
print(f"{res['nombre']:<35} | {res['worker_type']:<12} | {res['dpus']:<6.1f} | {mem_str:<12} | ${res['gasto']:,.2f}")
|
|
131
|
+
|
|
132
|
+
print("-" * 90)
|
|
133
|
+
print(f"\n🚨 REPORTE DE FUGA DE CAPITAL AWS:")
|
|
134
|
+
print(f"Estás perdiendo un estimado de: ${total_desperdicio * 4:,.2f} USD al mes pagando instancias gigantes.")
|
|
135
|
+
|
|
136
|
+
print("\n=========================================================================================")
|
|
137
|
+
print("💡 [TROJAN HOOK] ¿Tu script se ahoga con Wide Dataframes y tienes que pagar más RAM?")
|
|
138
|
+
print("El Catalyst Optimizer de Spark es el culpable. No pagues más en AWS Glue.")
|
|
139
|
+
print("Descarga la licencia B2B de 'Antigravity Pro' para obtener el 'DataFrameChunker' matemático.")
|
|
140
|
+
print("Corta tu factura de G.4X a G.1X hoy mismo contactando al Arquitecto de Datos.")
|
|
141
|
+
print("Más información: linkedin.com/in/tu-perfil-aqui")
|
|
142
|
+
print("=========================================================================================\n")
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Motor IO: Smart S3 Exporter (S3Finalizer)
|
|
3
|
+
=========================================
|
|
4
|
+
Maneja la "Última Milla" del procesamiento de PySpark en AWS.
|
|
5
|
+
Convierte las salidas nativas de Spark (ej. part-0000...) en archivos limpios, legibles y secuenciados.
|
|
6
|
+
"""
|
|
7
|
+
import time
|
|
8
|
+
import logging
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import boto3
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
except ImportError:
|
|
16
|
+
boto3 = None
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
class S3Finalizer:
|
|
21
|
+
"""
|
|
22
|
+
Abstracción inteligente para manejar las operaciones en S3.
|
|
23
|
+
Evita el molesto comportamiento por defecto de Spark limpiando archivos
|
|
24
|
+
intermedios y renombrando la salida.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, bucket_name: str, aws_region: str = 'us-east-1', mock_s3_client=None):
|
|
28
|
+
self.bucket = bucket_name
|
|
29
|
+
self.region = aws_region
|
|
30
|
+
|
|
31
|
+
if mock_s3_client:
|
|
32
|
+
self.s3 = mock_s3_client
|
|
33
|
+
elif boto3:
|
|
34
|
+
self.s3 = boto3.client('s3', region_name=self.region)
|
|
35
|
+
else:
|
|
36
|
+
raise ImportError("La librería 'boto3' no está instalada.")
|
|
37
|
+
|
|
38
|
+
def list_keys(self, prefix: str) -> List[str]:
|
|
39
|
+
keys = []
|
|
40
|
+
paginator = self.s3.get_paginator('list_objects_v2')
|
|
41
|
+
pages = paginator.paginate(Bucket=self.bucket, Prefix=prefix)
|
|
42
|
+
for page in pages:
|
|
43
|
+
for obj in page.get('Contents', []):
|
|
44
|
+
keys.append(obj['Key'])
|
|
45
|
+
return keys
|
|
46
|
+
|
|
47
|
+
def rename_spark_output(self, s3_prefix: str, final_filename: str, max_retries: int = 3) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Archivos únicos: Cambia part-0000.ext a final_filename.
|
|
50
|
+
(Ideal para salidas coalescidas df.coalesce(1))
|
|
51
|
+
"""
|
|
52
|
+
if not s3_prefix.endswith('/'): s3_prefix += '/'
|
|
53
|
+
|
|
54
|
+
all_keys = []
|
|
55
|
+
for attempt in range(max_retries):
|
|
56
|
+
all_keys = self.list_keys(s3_prefix)
|
|
57
|
+
if any(k.split("/")[-1].startswith("part-") for k in all_keys):
|
|
58
|
+
break
|
|
59
|
+
time.sleep(2)
|
|
60
|
+
|
|
61
|
+
part_key = None
|
|
62
|
+
keys_to_delete = []
|
|
63
|
+
|
|
64
|
+
for key in all_keys:
|
|
65
|
+
basename = key.split("/")[-1]
|
|
66
|
+
if basename.startswith("part-"):
|
|
67
|
+
part_key = key
|
|
68
|
+
elif basename != final_filename:
|
|
69
|
+
keys_to_delete.append({'Key': key})
|
|
70
|
+
|
|
71
|
+
if not part_key:
|
|
72
|
+
logger.warning(f"No se encontró salida 'part-' en {self.bucket}/{s3_prefix}")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
new_key = f"{s3_prefix}{final_filename}"
|
|
76
|
+
try:
|
|
77
|
+
self.s3.copy_object(Bucket=self.bucket, CopySource={'Bucket': self.bucket, 'Key': part_key}, Key=new_key)
|
|
78
|
+
keys_to_delete.append({'Key': part_key})
|
|
79
|
+
logger.info(f"Éxito: Salida renombrada a s3://{self.bucket}/{new_key}")
|
|
80
|
+
self._batch_delete(keys_to_delete)
|
|
81
|
+
return True
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Fallo al renombrar {s3_prefix}: {str(e)}")
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def sequence_spark_outputs(self, s3_prefix: str, pattern: str, extension: Optional[str] = None, max_retries: int = 3, max_workers: int = 20) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Archivos Múltiples: Encuentra TODOS los part-*.ext generados por Spark,
|
|
89
|
+
los ordena, y los renombra secuencialmente usando "hilos" en paralelo de Boto3.
|
|
90
|
+
|
|
91
|
+
:param s3_prefix: La ruta de salida.
|
|
92
|
+
:param pattern: El patrón del nombre deseado, ej: 'SBX.202701.{seq:04d}.SAL.gz'
|
|
93
|
+
:param extension: Opcional. Si se provee, ignora partes que no tengan esta extensión.
|
|
94
|
+
:param max_workers: Cantidad de hilos de red concurrentes de AWS.
|
|
95
|
+
:return: Número de archivos renombrados correctamente.
|
|
96
|
+
"""
|
|
97
|
+
if not s3_prefix.endswith('/'): s3_prefix += '/'
|
|
98
|
+
|
|
99
|
+
all_keys = []
|
|
100
|
+
for attempt in range(max_retries):
|
|
101
|
+
all_keys = self.list_keys(s3_prefix)
|
|
102
|
+
if any(k.split("/")[-1].startswith("part-") for k in all_keys):
|
|
103
|
+
break
|
|
104
|
+
time.sleep(2)
|
|
105
|
+
|
|
106
|
+
part_keys = []
|
|
107
|
+
keys_to_delete = []
|
|
108
|
+
|
|
109
|
+
# Filtrar
|
|
110
|
+
for key in all_keys:
|
|
111
|
+
basename = key.split("/")[-1]
|
|
112
|
+
if basename.startswith("part-"):
|
|
113
|
+
if extension and not basename.endswith(extension):
|
|
114
|
+
keys_to_delete.append({'Key': key})
|
|
115
|
+
else:
|
|
116
|
+
part_keys.append(key)
|
|
117
|
+
elif "_SUCCESS" in basename:
|
|
118
|
+
keys_to_delete.append({'Key': key})
|
|
119
|
+
|
|
120
|
+
if not part_keys:
|
|
121
|
+
logger.warning(f"No hay partes coincidentes ('{extension}') en {s3_prefix}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
# Ordenar (part-00000, part-00001...) nos asegura secuenciación determinista
|
|
125
|
+
part_keys.sort()
|
|
126
|
+
|
|
127
|
+
rename_tasks = []
|
|
128
|
+
for i, old_part_key in enumerate(part_keys, start=1):
|
|
129
|
+
# Soporta formato {seq:04d} -> 0001, 0002...
|
|
130
|
+
try:
|
|
131
|
+
new_basename = pattern.format(seq=i)
|
|
132
|
+
except KeyError:
|
|
133
|
+
new_basename = pattern.replace("{seq}", str(i)) # Seguridad por si el usuario pasa un string roto
|
|
134
|
+
|
|
135
|
+
new_key = f"{s3_prefix}{new_basename}"
|
|
136
|
+
rename_tasks.append((old_part_key, new_key))
|
|
137
|
+
keys_to_delete.append({'Key': old_part_key})
|
|
138
|
+
|
|
139
|
+
successful_renames = 0
|
|
140
|
+
|
|
141
|
+
def _copy(task):
|
|
142
|
+
src, dst = task
|
|
143
|
+
try:
|
|
144
|
+
self.s3.copy_object(Bucket=self.bucket, CopySource={'Bucket': self.bucket, 'Key': src}, Key=dst)
|
|
145
|
+
return True
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Fallo copiando {src} a {dst}: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Procesamiento Paralelo en AWS
|
|
151
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
152
|
+
results = executor.map(_copy, rename_tasks)
|
|
153
|
+
successful_renames = sum(1 for r in results if r)
|
|
154
|
+
|
|
155
|
+
logger.info(f"Éxito: {successful_renames}/{len(part_keys)} archivos secuenciados.")
|
|
156
|
+
self._batch_delete(keys_to_delete)
|
|
157
|
+
return successful_renames
|
|
158
|
+
|
|
159
|
+
def _batch_delete(self, keys: List[dict]):
|
|
160
|
+
if not keys: return
|
|
161
|
+
for i in range(0, len(keys), 1000):
|
|
162
|
+
try: # maximoAWS permite 1000 borrados por batch
|
|
163
|
+
self.s3.delete_objects(Bucket=self.bucket, Delete={'Objects': keys[i:i+1000], 'Quiet': True})
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"Fallo limpiando basura S3: {e}")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: antigravity-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Herramienta open-source para auditar despilfarros financieros en clústeres PySpark (AWS Glue) y manipular S3.
|
|
5
|
+
Author-email: Arquitecto B2B <arquitecto@antigravity.dev>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: boto3>=1.26.0
|
|
12
|
+
|
|
13
|
+
# 🛸 Antigravity PySpark Framework
|
|
14
|
+
|
|
15
|
+
Enterprise framework para resolver problemas complejos de Big Data en AWS Glue y entornos PySpark puros sin esfuerzo. Este proyecto estandariza las mejores prácticas de la industria con un solo `import`.
|
|
16
|
+
|
|
17
|
+
## 📦 Empaquetado y Licenciamiento Comercial (SaaS B2B)
|
|
18
|
+
|
|
19
|
+
Si deseas vender y distribuir este framework a una empresa externa, es imperativo **proteger tu Propiedad Intelectual** (ofuscar el código fuente) y **venderlo con una licencia temporal** (para que te paguen la renovación).
|
|
20
|
+
|
|
21
|
+
### 1. Ofuscar el código y armar la "Bomba de Tiempo" (Expiración)
|
|
22
|
+
Antes de construir el paquete, encripta el código fuente y configúrale la fecha de vencimiento exacta del contrato del cliente (ej. 31 de Diciembre de 2027):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Asegúrate de estar en el entorno virtual
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
|
|
28
|
+
# 2. Generar el binario comercial ofuscado (.whl) con Bomba de Tiempo (2027)
|
|
29
|
+
python build_commercial.py
|
|
30
|
+
```
|
|
31
|
+
Este script (que te escribí a la medida) automatizará por ti la encriptación con PyArmor y el empaquetado directo hacia la carpeta `dist/`. Cualquier ingeniero externo que intente abrir tus archivos allí dentro verá código basura indescifrable.
|
|
32
|
+
|
|
33
|
+
### 2. Construcción de uso interno / Open-Source (`.whl`)
|
|
34
|
+
Para el empaquetado inicial (sin ofuscación) para probar el framework dentro de tu propio AWS Glue, ejecuta el empaquetado estándar de Python:
|
|
35
|
+
```bash
|
|
36
|
+
# Construye el antigravity-0.1.0-py3-none-any.whl en la carpeta dist/
|
|
37
|
+
pip install build
|
|
38
|
+
python -m build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Guía de Uso Rápido para Ingenieros
|
|
44
|
+
|
|
45
|
+
### Motor A: Optimizador Core (`DataFrameChunker`)
|
|
46
|
+
**¿Cuándo usarlo?** Cuando lidias con DataFrames anchos (Wide DataFrames) o realizas analítica que provoca errores de `OutOfMemory` (OOM), demoras extremas, o fallos de `StackOverflowError`. Funciona fluidamente con cualquier volumen de datos y columnas, auto-regulando la memoria del clúster para acelerar los cruces matemáticos pesados.
|
|
47
|
+
|
|
48
|
+
**¿Cómo funciona?**
|
|
49
|
+
Particiona el DataFrame verticalmente e inyecta `localCheckpoint()` para aislar la memoria del Catalyst Optimizer.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from antigravity.core import DataFrameChunker
|
|
53
|
+
|
|
54
|
+
# 1. Instancias el chunker indicando la llave primaria y cuántas columnas procesar a la vez.
|
|
55
|
+
chunker = DataFrameChunker(df_masivo, id_cols=["id_cliente"], chunk_size=50)
|
|
56
|
+
|
|
57
|
+
# 2. Defines tú lógica matemática. Spark solo le pasará 50 variables por iteración.
|
|
58
|
+
def mi_logica(chunk_df, indice):
|
|
59
|
+
# Calcula algo y retorna un DF más pequeño
|
|
60
|
+
return chunk_df.select("id_cliente", ...)
|
|
61
|
+
|
|
62
|
+
# 3. El framework ejecuta el bucle de forma segura en el clúster
|
|
63
|
+
resultados_parciales = chunker.process_chunks(mi_logica)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Motor B: Generador Legacy (`LegacyTextTransformer`)
|
|
67
|
+
**¿Cuándo usarlo?** Cuando el negocio, banco, o gobierno te exige generar archivos `.TXT` planos, asimétricos o posicionales a partir de datos tabulares modernos.
|
|
68
|
+
|
|
69
|
+
**¿Cómo funciona?**
|
|
70
|
+
Recibe el DataFrame y usa `RDD.flatMap` para aplicar lógica de python puro distribuidamente.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from antigravity.legacy import LegacyTextTransformer
|
|
74
|
+
|
|
75
|
+
# 1. Escribes las reglas del negocio (1 Fila de Spark = N líneas de texto)
|
|
76
|
+
def reglas_bancarias(fila):
|
|
77
|
+
yield f"HEADER_{fila.id}"
|
|
78
|
+
yield f"BODY_{fila.saldo}"
|
|
79
|
+
|
|
80
|
+
# 2. Invocas el framework
|
|
81
|
+
transformer = LegacyTextTransformer(df)
|
|
82
|
+
|
|
83
|
+
# 3. Generas la salida
|
|
84
|
+
transformer.save_as_text(reglas_bancarias, "s3://mi-bucket/banco_txt/")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Motor IO: Smart Exporter (`S3Finalizer`)
|
|
88
|
+
**¿Cuándo usarlo?** Cuando detestas que PySpark guarde tus archivos como `part-0000X.csv` y deje basura como los archivos `_SUCCESS` en tu S3.
|
|
89
|
+
|
|
90
|
+
**¿Cómo funciona?**
|
|
91
|
+
Se conecta mediante `boto3` para listar, ordenar, secuenciar y renombrar la salida de Spark.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from antigravity.io import S3Finalizer
|
|
95
|
+
|
|
96
|
+
# 1. Escribes tu DataFrame normal en Spark
|
|
97
|
+
df.write.parquet("s3://mi-bucket/salida_sucia/")
|
|
98
|
+
|
|
99
|
+
# 2. Invocas el framework para secuenciarlos inteligentemente e inyectar el formato
|
|
100
|
+
finalizador = S3Finalizer(bucket_name="mi-bucket")
|
|
101
|
+
finalizador.sequence_spark_outputs(
|
|
102
|
+
s3_prefix="salida_sucia/",
|
|
103
|
+
pattern="MI_EMPRESA_DATA_{seq:04d}.parquet" # Resultado: MI_EMPRESA_DATA_0001.parquet
|
|
104
|
+
)
|
|
105
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src_lite/antigravity_lite/__init__.py
|
|
4
|
+
src_lite/antigravity_lite.egg-info/PKG-INFO
|
|
5
|
+
src_lite/antigravity_lite.egg-info/SOURCES.txt
|
|
6
|
+
src_lite/antigravity_lite.egg-info/dependency_links.txt
|
|
7
|
+
src_lite/antigravity_lite.egg-info/requires.txt
|
|
8
|
+
src_lite/antigravity_lite.egg-info/top_level.txt
|
|
9
|
+
src_lite/antigravity_lite/auditor/__init__.py
|
|
10
|
+
src_lite/antigravity_lite/auditor/finops.py
|
|
11
|
+
src_lite/antigravity_lite/io/__init__.py
|
|
12
|
+
src_lite/antigravity_lite/io/s3_finalizer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
boto3>=1.26.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
antigravity_lite
|