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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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")
@@ -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