data_drain 0.3.2 → 0.5.0

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.
@@ -55,6 +55,18 @@ module DataDrain
55
55
  raise NotImplementedError, "#{self.class} debe implementar #destroy_partitions"
56
56
  end
57
57
 
58
+ # Sube un archivo local al storage.
59
+ #
60
+ # @param local_path [String]
61
+ # @param bucket [String]
62
+ # @param s3_key [String] key relativo (ej. "scripts/export.py")
63
+ # @param content_type [String, nil]
64
+ # @return [String] URI completo del archivo subido
65
+ # @raise [NotImplementedError]
66
+ def upload_file(local_path, bucket, s3_key, content_type: nil)
67
+ raise NotImplementedError, "#{self.class} debe implementar #upload_file"
68
+ end
69
+
58
70
  protected
59
71
 
60
72
  # @param bucket [String]
@@ -27,6 +27,19 @@ module DataDrain
27
27
  "#{build_path_base(bucket, folder_name, partition_path)}/**/*.parquet"
28
28
  end
29
29
 
30
+ # @param local_path [String]
31
+ # @param bucket [String] Directorio destino
32
+ # @param s3_key [String] Path relativo dentro del bucket
33
+ # @param content_type [String, nil] Ignorado en modo local
34
+ # @return [String] Path absoluto al archivo destino
35
+ def upload_file(local_path, bucket, s3_key, content_type: nil)
36
+ _ = content_type
37
+ dest_path = File.join(bucket, s3_key)
38
+ FileUtils.mkdir_p(File.dirname(dest_path))
39
+ FileUtils.cp(local_path, dest_path)
40
+ dest_path
41
+ end
42
+
30
43
  # @param bucket [String]
31
44
  # @param folder_name [String]
32
45
  # @param partition_keys [Array<Symbol>]
@@ -38,6 +38,23 @@ module DataDrain
38
38
  delete_in_batches(client, bucket, objects)
39
39
  end
40
40
 
41
+ # @param local_path [String]
42
+ # @param bucket [String]
43
+ # @param s3_key [String]
44
+ # @param content_type [String, nil]
45
+ # @return [String] "s3://bucket/key"
46
+ def upload_file(local_path, bucket, s3_key, content_type: nil)
47
+ client = s3_client
48
+
49
+ File.open(local_path, "rb") do |file|
50
+ params = { bucket: bucket, key: s3_key, body: file }
51
+ params[:content_type] = content_type if content_type
52
+ client.put_object(**params)
53
+ end
54
+
55
+ "s3://#{bucket}/#{s3_key}"
56
+ end
57
+
41
58
  private
42
59
 
43
60
  # @return [Aws::S3::Client]
@@ -6,9 +6,17 @@ module DataDrain
6
6
  # Regex que valida identificadores SQL (tablas, columnas, etc.).
7
7
  # Permite letras, guiones bajos y números (no al inicio).
8
8
  IDENTIFIER_REGEX = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
9
+ GLUE_NAME_REGEX = /\A(?![_-])[a-zA-Z0-9_-]+\z/
9
10
 
10
11
  module_function
11
12
 
13
+ def validate_glue_name!(name, value)
14
+ return if GLUE_NAME_REGEX.match?(value.to_s)
15
+
16
+ raise DataDrain::ConfigurationError,
17
+ "#{name} '#{value}' no es un nombre válido para Glue Job (usa solo letras, números, '-' y '_')"
18
+ end
19
+
12
20
  def validate_identifier!(name, value)
13
21
  return if IDENTIFIER_REGEX.match?(value.to_s)
14
22
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DataDrain
4
4
  # @return [String] versión semver de la gema
5
- VERSION = "0.3.2"
5
+ VERSION = "0.5.0"
6
6
  end
data/skill/SKILL.md CHANGED
@@ -8,12 +8,14 @@ Skill de conocimiento completo sobre DataDrain. Consultame para cualquier pregun
8
8
  - **Engine** — Motor principal que orquesta el flujo Conteo → Export → Verify → Purge.
9
9
  - **FileIngestor** — Convierte archivos crudos (CSV/JSON/Parquet) a Parquet particionado en el Data Lake.
10
10
  - **Record** — Clase base ORM analítico (tipo ActiveRecord) read-only sobre Parquet vía DuckDB.
11
- - **GlueRunner** — Orquestador de AWS Glue Jobs para tablas de gran volumen (>500GB-1TB).
11
+ - **GlueRunner** — Orquestador de AWS Glue Jobs para tablas de gran volumen (>500GB-1TB). Soporta lifecycle completo: crear, actualizar, eliminar y verificar jobs.
12
12
  - **Storage Adapter** — Patrón Strategy con dos implementaciones: `Storage::Local` y `Storage::S3`. Cacheado en `Storage.adapter`.
13
13
  - **Observability** — Módulo mixín (`include`/`extend`) con `safe_log` resiliente y logging KV estructurado.
14
14
  - **Hive Partitioning** — Estructura de carpetas `key1=val1/key2=val2/...` que DuckDB genera y consume nativamente para prefix scans eficientes.
15
15
  - **Semi-abierto** — Convención de rangos `[start, end)` con `<` (no `<=`) para evitar pérdida de microsegundos en límites de fecha.
16
16
  - **skip_export** — Modo del Engine donde delega export a herramienta externa (Glue/EMR) y solo verifica + purga.
17
+ - **ensure_job** — Wrapper idempotente de GlueRunner que crea o actualiza un job según config deseada. Incluye diffing de configuración para evitar API calls innecesarios.
18
+ - **changed_fields** — Helper privado de ensure_job que compara config deseada vs actual de un Glue Job y retorna qué campos difieren.
17
19
  - **Heartbeat** — Log de progreso emitido cada 100 lotes en purgas masivas (tablas 1TB).
18
20
  - **Wispro-Observability-Spec v1** — Estándar de logs KV: `component=` y `event=` primero, sufijo `_s` para tiempos float, `_count` para enteros, sin unidades en valores.
19
21
 
@@ -66,9 +68,9 @@ DataDrain resuelve el ciclo de vida de datos históricos en bases relacionales c
66
68
 
67
69
  ### Stack y dependencias
68
70
 
69
- - Ruby `>= 3.0.0`
71
+ - Ruby `>= 3.2.0`
70
72
  - Runtime: `activemodel >= 6.0`, `duckdb ~> 1.4`, `pg >= 1.2`, `aws-sdk-s3 ~> 1.114`, `aws-sdk-glue ~> 1.0`
71
- - Versión actual: `0.1.19`
73
+ - Versión actual: `0.5.0`
72
74
 
73
75
  ## API Pública (resumen)
74
76
 
@@ -123,6 +125,35 @@ ArchivedX.destroy_all(isp_id: 42) # => Integer (pa
123
125
 
124
126
  # 4. Glue para tablas 1TB+
125
127
  DataDrain::GlueRunner.run_and_wait("job-name", { "--key" => "val" }, polling_interval: 30)
128
+
129
+ # 4b. Glue Jobs Lifecycle (v0.4.0+)
130
+ # Verificar si existe
131
+ DataDrain::GlueRunner.job_exists?("my-job") # => true/false
132
+
133
+ # Obtener config completa
134
+ job = DataDrain::GlueRunner.get_job("my-job") # => Aws::Glue::Types::Job
135
+
136
+ # Crear job con script local (v0.5.0+)
137
+ job = DataDrain::GlueRunner.create_job(
138
+ "my-job",
139
+ role_arn: "arn:aws:iam::123:role/GlueRole",
140
+ script_path: "scripts/glue/export.py", # local → S3 automático
141
+ script_bucket: "my-bucket",
142
+ script_folder: "scripts",
143
+ timeout: 1440,
144
+ max_retries: 2
145
+ )
146
+
147
+ # Upsert idempotente con diffing de config
148
+ job = DataDrain::GlueRunner.ensure_job(
149
+ "my-job",
150
+ role_arn: "arn:aws:iam::123:role/GlueRole",
151
+ script_path: "scripts/glue/export.py",
152
+ script_bucket: "my-bucket"
153
+ )
154
+
155
+ # Eliminar job (idempotente)
156
+ DataDrain::GlueRunner.delete_job("my-job") # => true/false
126
157
  ```
127
158
 
128
159
  Detalle completo de firmas, parámetros, retornos y comportamientos en [API Detallada](references/api-detallada.md).
@@ -172,6 +203,28 @@ No. La gema NO emite `source=` manualmente — lo inyecta automáticamente `exis
172
203
 
173
204
  `component=data_drain event=<clase>.<suceso> [campos KV]`. Tiempos con sufijo `_s` y valor float. Contadores con `_count` y valor integer. Sin unidades en los valores. Detalle en [Eventos y Telemetría](references/eventos-telemetria.md).
174
205
 
206
+ ### ¿Cómo subo un script Glue desde mi repo?
207
+
208
+ Desde v0.5.0 podés usar `script_path:` en lugar de `script_location:`:
209
+
210
+ ```ruby
211
+ DataDrain::GlueRunner.ensure_job(
212
+ "my-export-job",
213
+ script_path: "scripts/glue/export.py",
214
+ script_bucket: "my-bucket",
215
+ script_folder: "scripts",
216
+ role_arn: ENV["GLUE_ROLE_ARN"]
217
+ )
218
+ ```
219
+
220
+ La gema sube el script a S3 usando el `Storage::S3` adapter existente
221
+ (con `credential_chain` si tenés IAM role). **Requiere `storage_mode = :s3`**.
222
+ Si `storage_mode = :local`, levanta `ConfigurationError`.
223
+
224
+ **Overwrite:** cada invocación sobrescribe el archivo en S3. Útil para que
225
+ el script siga al código del repo. Si necesitás versionar, usar `script_filename:`
226
+ con hash o timestamp.
227
+
175
228
  ## Errores
176
229
 
177
230
  Catálogo top. Detalle completo y resolución en [API Detallada](references/api-detallada.md).
@@ -197,6 +250,12 @@ Errores de query DuckDB. En `Engine#verify_integrity` se captura y se loguea com
197
250
  ### `RuntimeError` desde `GlueRunner`
198
251
  Levantado cuando un Job de Glue termina con estado `FAILED`, `STOPPED` o `TIMEOUT`. **Mensaje:** `"Glue Job <name> (Run ID: <id>) falló con estado <status>."`
199
252
 
253
+ ### `Aws::Glue::Errors::EntityNotFoundException`
254
+ Job de Glue no existe. En `job_exists?` se rescata y retorna `false`. En `get_job`, `update_job` y `delete_job` se propaga.
255
+
256
+ ### `Aws::Glue::Errors::ServiceError`
257
+ Error genérico de AWS Glue. Se propaga en todos los métodos de lifecycle. Los métodos emiten `glue_runner.job_*_error` antes de propagar.
258
+
200
259
  ## Antipatrones
201
260
 
202
261
  Catálogo completo en [Antipatrones](references/antipatrones.md). Resumen de los más críticos:
@@ -207,10 +266,12 @@ Catálogo completo en [Antipatrones](references/antipatrones.md). Resumen de los
207
266
  4. **Validar `idle_in_transaction_session_timeout` con `.present?`** — `0.present?` es `false`, ignora la config.
208
267
  5. **Usar `<= end_of_day`** en rangos de fecha — pierde registros con microsegundos.
209
268
  6. **Loguear `source=`** manualmente — duplica el campo que inyecta `exis_ray`.
269
+ 7. **Usar nombres de Glue Job con guiones bajos al inicio** — `validate_glue_name!` rechaza `_my-job`. Usar `my-job` o `my_job` (sin underscore inicial).
210
270
 
211
271
  ## Referencias
212
272
 
213
273
  - [API Detallada](references/api-detallada.md) — Firmas completas, parámetros, retornos y comportamientos de cada clase pública.
274
+ - [Glue Jobs Lifecycle](https://github.com/gedera/data_drain/blob/main/docs/glue-jobs-lifecycle.md) — Guía completa de gestión de AWS Glue Jobs: crear, actualizar, eliminar, verificar y ejecutar jobs idempotentemente.
214
275
  - [Eventos y Telemetría](references/eventos-telemetria.md) — Catálogo completo de eventos KV emitidos por la gema.
215
276
  - [Antipatrones](references/antipatrones.md) — Qué NO hacer y alternativas correctas.
216
277
  - [Postgres Tuning](references/postgres-tuning.md) — Índices, VACUUM, particionamiento y diagnóstico por tamaño de tabla.
@@ -115,6 +115,14 @@ Catálogo completo de eventos KV emitidos por DataDrain. Formato Wispro-Observab
115
115
  **Nivel:** INFO. Emite antes de `start_job_run`.
116
116
  **Campos:** `job`.
117
117
 
118
+ ### `glue_runner.job_exists`
119
+ **Nivel:** INFO. Emite en `ensure_job` cuando el job ya existe y se actualiza.
120
+ **Campos:** `job`.
121
+
122
+ ### `glue_runner.job_created`
123
+ **Nivel:** INFO. Emite en `ensure_job` cuando el job se crea.
124
+ **Campos:** `job`.
125
+
118
126
  ### `glue_runner.polling`
119
127
  **Nivel:** INFO. Emite cada chequeo de estado mientras Job no terminó.
120
128
  **Campos:** `job`, `run_id`, `status`, `next_check_in_s`.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_drain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel
@@ -105,6 +105,11 @@ files:
105
105
  - docs/execution/archive/v0.3.1-OBSERVACIONES.md
106
106
  - docs/execution/archive/v0.3.1.md
107
107
  - docs/execution/v0.2.2.md
108
+ - docs/execution/v0.4.0-OBSERVACIONES.md
109
+ - docs/execution/v0.4.0.md
110
+ - docs/execution/v0.5.0-OBSERVACIONES.md
111
+ - docs/execution/v0.5.0.md
112
+ - docs/glue-jobs-lifecycle.md
108
113
  - docs/glue_pyspark_example.py
109
114
  - lib/data_drain.rb
110
115
  - lib/data_drain/configuration.rb