data_drain 0.3.2 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e7253dc94b9b7e2d000ba0c03b4e4d7692f12a26f16b422e71a884fa7a81efa
4
- data.tar.gz: c1f9f7eb1e0e861c7d2e0dbf6ba6c66125a97bcbd90e175aa1a859e8c5a898fa
3
+ metadata.gz: ff8a69a33cb9dc44d9252792b7b6531707ba11f330c81cc9c27f4613e74ef0be
4
+ data.tar.gz: c6503db21d32c3ea60fe2be121d6422fb5305bf222b07f200cc81364e7b9c152
5
5
  SHA512:
6
- metadata.gz: fdbf3431159bc83950adf972d68d8cff245bffa14481e0e2ef039a7959e3cbf884649c5bbaf40219a66a7ff0a8b24cad428001e5a7e05071873899bed3969b57
7
- data.tar.gz: 3f5acffe028c91b472dd5de9b4e03f34954ca8c8cffeeb3e1f3f3b725b14a8f7df449ebc4b6cf6d7728a2a935f579ba403f2e72bfb53a6aadde1ca281c2698b2
6
+ metadata.gz: b916b2ee021d9cf6060ae00b2c5f924811b3f42ef7d475b329960be4b80035e1a3348dfb28da49d0a8fc8ec5e6ec749d9145da643f95f18f952b8be1e4c45bde
7
+ data.tar.gz: 39c9d09e004e75a84f135651f12d7b0eec810f39083fd565ac7edeca3affc83a31f08db4b187312b85121e9284ddb50a4b3f6e4c2cd8d6fb46ff0e7e5888af3a
data/.rubocop.yml CHANGED
@@ -23,6 +23,18 @@ Metrics/BlockLength:
23
23
  - data_drain.gemspec
24
24
  - lib/**/*.rb
25
25
 
26
+ Metrics/ParameterLists:
27
+ Exclude:
28
+ - lib/**/*.rb
29
+
30
+ Metrics/CyclomaticComplexity:
31
+ Exclude:
32
+ - lib/**/*.rb
33
+
34
+ Metrics/PerceivedComplexity:
35
+ Exclude:
36
+ - lib/**/*.rb
37
+
26
38
  Layout/LineLength:
27
39
  Exclude:
28
40
  - lib/**/configuration.rb # connection string URL > 120 chars
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-04-15
4
+
5
+ ### Features
6
+
7
+ - `GlueRunner.job_exists?(job_name)`: verifica si un job existe. Retorna `true`/`false`. (item 35)
8
+ - `GlueRunner.get_job(job_name)`: obtiene la configuración completa de un job. Retorna `Aws::Glue::Types::Job`. (item 35)
9
+ - `GlueRunner.create_job(job_name, role_arn:, script_location:, ...)`: crea un job con configuración completa. Retorna el job creado. (item 32)
10
+ - `GlueRunner.update_job(job_name, ...)`: actualiza un job existente. Retorna el job actualizado. (item 33)
11
+ - `GlueRunner.delete_job(job_name)`: elimina un job. Retorna `nil`. (item 34)
12
+ - `GlueRunner.ensure_job(job_name, ...)`: upsert idempotente — crea si no existe, actualiza si existe. (item 36)
13
+
14
+ ### Validations
15
+
16
+ - `DataDrain::Validations.validate_glue_name!`: validación específica para nombres de Glue Jobs (letras, números, guiones; no permite guiones bajos ni espacios).
17
+
18
+ ### Tests
19
+
20
+ - 163 specs, coverage 97.39%.
21
+
22
+ ### Docs
23
+
24
+ - `docs/glue-jobs-lifecycle.md`: referencia completa de la API de Glue Jobs.
25
+ - README.md actualizado con ejemplos de todos los métodos.
26
+ - `skill/references/eventos-telemetria.md`: nuevos eventos `glue_runner.job_exists` y `glue_runner.job_created`.
27
+
3
28
  ## [0.3.2] - 2026-04-15
4
29
 
5
30
  ### Regresiónfix (desde v0.3.1)
data/README.md CHANGED
@@ -107,6 +107,36 @@ DataDrain::Engine.new(
107
107
  ### Orquestación con AWS Glue (tablas 1TB+)
108
108
 
109
109
  ```ruby
110
+ # Verificar si un job existe
111
+ DataDrain::GlueRunner.job_exists?("my-glue-export-job")
112
+ # => true / false
113
+
114
+ # Obtener configuración de un job
115
+ job = DataDrain::GlueRunner.get_job("my-glue-export-job")
116
+ # => Aws::Glue::Types::Job (Name, Command, DefaultArguments, etc.)
117
+
118
+ # Crear un job
119
+ job = DataDrain::GlueRunner.create_job(
120
+ "my-glue-export-job",
121
+ role_arn: "arn:aws:iam::123:role/GlueServiceRole",
122
+ script_location: "s3://my-bucket/scripts/export.py",
123
+ default_arguments: { "--extra-files" => "s3://my-bucket/scripts/udf.py" },
124
+ timeout: 1440,
125
+ max_retries: 2
126
+ )
127
+
128
+ # Asegurar job idempotente (crea si no existe, actualiza si existe)
129
+ job = DataDrain::GlueRunner.ensure_job(
130
+ "my-glue-export-job",
131
+ role_arn: "arn:aws:iam::123:role/GlueServiceRole",
132
+ script_location: "s3://my-bucket/scripts/export.py",
133
+ timeout: 1440
134
+ )
135
+
136
+ # Eliminar un job
137
+ DataDrain::GlueRunner.delete_job("my-glue-export-job")
138
+
139
+ # Ejecutar y esperar
110
140
  DataDrain::GlueRunner.run_and_wait(
111
141
  "my-glue-export-job",
112
142
  {
@@ -1521,3 +1521,117 @@ El workflow actual usa `bundler-cache: false`. Habilitar `bundler-cache: true` j
1521
1521
  ```
1522
1522
 
1523
1523
  **Riesgo:** Requiere que el step "Download DuckDB library" corra antes de bundle install para que Bundler cachee correctamente los gems compilados.
1524
+
1525
+ ---
1526
+
1527
+ ### Item 32 — Glue Jobs Lifecycle: create/update/delete atómicos
1528
+
1529
+ **Estado:** `[x]`
1530
+ **Prioridad:** P2
1531
+ **Tipo:** `feat`
1532
+ **Estimación:** M
1533
+ **Release sugerido:** v0.4.0
1534
+
1535
+ ##### Contexto
1536
+
1537
+ `GlueRunner.run_and_wait` solo ejecuta jobs pre-existentes. Para automatizar el ciclo de vida completo (infra-as-code), se agregan métodos para crear, actualizar y eliminar jobs.
1538
+
1539
+ ##### Cambios
1540
+
1541
+ 1. `GlueRunner.create_job(job_name, role_arn:, script_location:, ...)` — crea un Glue Job con defaults razonables. Retorna `Aws::Glue::Types::Job`.
1542
+ 2. `GlueRunner.update_job(job_name, ...)` — actualiza un job existente. Retorna el job actualizado.
1543
+ 3. `GlueRunner.delete_job(job_name)` — elimina un job. Retorna `nil`.
1544
+
1545
+ ##### Criterios de aceptación
1546
+
1547
+ - [x] `create_job` retorna el job object creado.
1548
+ - [x] `update_job` falla con EntityNotFoundException si no existe.
1549
+ - [x] `delete_job` retorna nil.
1550
+ - [x] `validate_glue_name!` permite guiones en nombres (regex `[a-zA-Z0-9-]`).
1551
+
1552
+ ---
1553
+
1554
+ ### Item 33 — `ensure_job` idempotente
1555
+
1556
+ **Estado:** `[x]`
1557
+ **Prioridad:** P2
1558
+ **Tipo:** `feat`
1559
+ **Estimación:** M
1560
+ **Release sugerido:** v0.4.0
1561
+
1562
+ ##### Contexto
1563
+
1564
+ Wrapper idempotente que garantiza un job existe con la config deseada: lo crea si no existe, lo actualiza si difiere.
1565
+
1566
+ ##### Cambios
1567
+
1568
+ ```ruby
1569
+ DataDrain::GlueRunner.ensure_job("my-job", role_arn: "...", script_location: "...")
1570
+ # => Aws::Glue::Types::Job
1571
+ ```
1572
+
1573
+ ##### Criterios de aceptación
1574
+
1575
+ - [x] Crea el job si no existe.
1576
+ - [x] Actualiza el job si ya existe.
1577
+ - [x] Emite `glue_runner.job_created` / `glue_runner.job_exists`.
1578
+
1579
+ ---
1580
+
1581
+ ### Item 34 — Helpers consultivos: `job_exists?` + `get_job`
1582
+
1583
+ **Estado:** `[x]`
1584
+ **Prioridad:** P2
1585
+ **Tipo:** `feat`
1586
+ **Estimación:** S
1587
+ **Release sugerido:** v0.4.0
1588
+
1589
+ ##### Contexto
1590
+
1591
+ Foundation para items 32 y 33. `get_job` retorna el Job object; `job_exists?` es boolean.
1592
+
1593
+ ##### Criterios de aceptación
1594
+
1595
+ - [x] `get_job` retorna `Aws::Glue::Types::Job`.
1596
+ - [x] `job_exists?` retorna boolean.
1597
+ - [x] EntityNotFoundException → false (no propaga en `job_exists?`).
1598
+
1599
+ ---
1600
+
1601
+ ### Item 35 — Tests consolidación Glue Jobs
1602
+
1603
+ **Estado:** `[x]`
1604
+ **Prioridad:** P2
1605
+ **Tipo:** `test`
1606
+ **Estimación:** M
1607
+ **Release sugerido:** v0.4.0
1608
+
1609
+ ##### Contexto
1610
+
1611
+ Suite de tests con `Aws::Glue::Client.stub_responses` para los 5 nuevos métodos. Coverage ≥ 90%.
1612
+
1613
+ ##### Criterios de aceptación
1614
+
1615
+ - [ ] Tests para todos los nuevos métodos.
1616
+ - [ ] Edge cases: `default_arguments` hash equality, Symbol vs String keys.
1617
+ - [ ] Coverage ≥ 90%.
1618
+
1619
+ ---
1620
+
1621
+ ### Item 36 — Docs: `glue-jobs-lifecycle.md`
1622
+
1623
+ **Estado:** `[x]`
1624
+ **Prioridad:** P2
1625
+ **Tipo:** `docs`
1626
+ **Estimación:** S
1627
+ **Release sugerido:** v0.4.0
1628
+
1629
+ ##### Contexto
1630
+
1631
+ Documentación del nuevo feature: pre-requisitos IAM, API de cada método, eventos de telemetría, limitaciones, patrón completo ensure+run.
1632
+
1633
+ ##### Criterios de aceptación
1634
+
1635
+ - [x] `docs/glue-jobs-lifecycle.md` creado.
1636
+ - [x] README actualizado con ejemplo.
1637
+ - [x] Eventos catalogados en `eventos-telemetria.md`.
@@ -0,0 +1,144 @@
1
+ # Observaciones — Plan v0.4.0
2
+
3
+ **Fecha:** 2026-04-15
4
+ **Proyecto:** data_drain
5
+ **Release:** v0.4.0 — Glue Jobs Lifecycle
6
+ **Estado:** En análisis
7
+
8
+ ---
9
+
10
+ ## Crítica — Validación de nombres de Glue Jobs (BLOCKING)
11
+
12
+ **Ubicación:** Fase 2, sección 2.4 + línea 1008-1009 del Plan B
13
+
14
+ **Problema:**
15
+ `Validations.validate_identifier!` usa regex `\A[a-zA-Z_][a-zA-Z0-9_]*\z` — no permite guiones (`-`). AWS Glue SÍ permite guiones en nombres de jobs:
16
+
17
+ ```
18
+ data-drain-export-versions ✅ válido en AWS
19
+ data_drain_export_versions ✅ válido en regex actual
20
+ ```
21
+
22
+ El caso de uso del plan (líneas 19-26) usa `name: "data-drain-export-versions"` con guiones.
23
+
24
+ **Impacto:** El feature completo queda bloqueado si no se resuelve antes de Fase 2.
25
+
26
+ **Solución propuesta:** Crear `validate_glue_name!` en `lib/data_drain/validations.rb`:
27
+
28
+ ```ruby
29
+ def self.validate_glue_name!(field_name, value)
30
+ return if value.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
31
+
32
+ raise ConfigurationError, "#{field_name} debe ser un Glue Job name válido (alfanumérico, guiones y guiones bajos)"
33
+ end
34
+ ```
35
+
36
+ Alternativas:
37
+ - Modificar `validate_identifier!` existente para permitir `-` → riesgo: afecta todos los usos existentes
38
+ - No validar → más simple pero menos defensive
39
+
40
+ **Recomendación:** Crear `validate_glue_name!` específica para Glue, no tocar `validate_identifier!`.
41
+
42
+ ---
43
+
44
+ ## Media — `extract_current_config` puede retornar nil silenciosamente
45
+
46
+ **Ubicación:** Fase 3, sección 3.2
47
+
48
+ **Problema:**
49
+ `extract_current_config` usa safe navigation (`&.`) para todos los campos anidados:
50
+
51
+ ```ruby
52
+ script_location: job.command&.script_location,
53
+ command_name: job.command&.name,
54
+ default_arguments: job.default_arguments&.to_h || {},
55
+ ```
56
+
57
+ Si AWS retorna un job sin `command` (edge case improbable pero posible), `script_location` retorna `nil`. Luego en `changed_fields`:
58
+
59
+ ```ruby
60
+ desired_config[field] != extracted[field] # nil != "s3://..." → true
61
+ ```
62
+
63
+ Esto generaría un false positive: `ensure_job` dispararía `update_job` por un campo que el job no soporta en ese estado.
64
+
65
+ **Mitigación:** Los stubs de test en el plan incluyen `command:` siempre. Pero el test "ignora campos no especificados por el caller" (línea 686-700) no verifica este edge case.
66
+
67
+ **Recomendación:** En `extract_current_config`, si un campo es `nil` y el caller no lo especificó, tratarlo como "no opinion" — no debería Disparar diff. Modificar `changed_fields` para excluir campos donde `extracted[field].nil?` Y `!desired_config.key?(field)`.
68
+
69
+ ---
70
+
71
+ ## Media — `update_job` API shape requiere verificación
72
+
73
+ **Ubicación:** Fase 2, sección 2.3, líneas 325-332
74
+
75
+ **Problema:**
76
+ El plan asume esta shape para `update_job`:
77
+
78
+ ```ruby
79
+ client.update_job(name: config[:name], job_update: job_update)
80
+ ```
81
+
82
+ Donde `job_update = aws_params.except(:name)`. Pero la AWS Glue API para `update_job` tiene quirks:
83
+
84
+ 1. `job_update` no puede incluir `Name` (es el path param)
85
+ 2. Algunos campos como `Command` requieren la estructura completa, no parcial
86
+ 3. `ExecutionProperty` requiere `{ max_concurrent_runs: Integer }` explícito
87
+
88
+ **Recomendación:** Antes de Fase 2, verificar con test de stub que la API acepta el hash generado. O escribir un test rápido contra el stub que captura los params enviados.
89
+
90
+ ---
91
+
92
+ ## Baja — Timestamp en `update_job` no manejado
93
+
94
+ **Ubicación:** Fase 3
95
+
96
+ **Problema:**
97
+ AWS Glue Jobs tienen campos `CreatedOn` y `LastModifiedOn` (timestamps). Cuando `get_job` retorna el job actual, estos timestamps siempre difieren de lo que el caller setearía (porque el caller no los setea).
98
+
99
+ Si `changed_fields` incluyera `CreatedOn` o `LastModifiedOn`, siempre dispararía update.
100
+
101
+ El plan filtra por `desired_config.key?(field)` así que no debería pasar — pero hay que asegurar que `extract_current_config` NO extraiga estos campos.
102
+
103
+ **Recomendación:** Verificar que `extract_current_config` (líneas 593-608) no incluya `CreatedOn`, `LastModifiedOn`, ni `AllocatedCapacity`. Si los incluye, quitarlos.
104
+
105
+ ---
106
+
107
+ ## Baja — Cobertura de `default_arguments` en diff
108
+
109
+ **Ubicación:** Fase 4, sección 4.1
110
+
111
+ **Problema:**
112
+ `default_arguments` es un Hash. La comparación `desired_config[field] != extracted[field]` en Ruby compara referencias, no contenido:
113
+
114
+ ```ruby
115
+ { "--key" => "val1" } != { "--key" => "val1" } # true (objetos distintos)
116
+ ```
117
+
118
+ Esto significa que `ensure_job` siempre vería diff en `default_arguments` aunque los valores sean iguales.
119
+
120
+ **Recomendación:** Implementar comparación de hashes recursiva o usar `==` en lugar de `!=` en `changed_fields`, o convertir a JSON string para comparación:
121
+
122
+ ```ruby
123
+ desired_config[field].to_json == extracted[field].to_json
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Plan B — Items relevantes a verificar pre-ejecución
129
+
130
+ | Item | Riesgo | Acción pre-ejecución |
131
+ |------|--------|----------------------|
132
+ | Glue Job names con `-` | Confirmed blocking | Crear `validate_glue_name!` antes de Fase 2 |
133
+ | `update_job` API shape | Medio | Test con stub que captura params antes de Fase 2 |
134
+ | `default_arguments` comparison | Bajo | Implementar comparación por JSON en `changed_fields` |
135
+ | Timestamps en job | Bajo | Verificar `extract_current_config` no los incluye |
136
+
137
+ ---
138
+
139
+ ## Orden sugerido de resolución pre-ejecución
140
+
141
+ 1. **Hoy:** Crear `validate_glue_name!` en `Validations`
142
+ 2. **Antes de Fase 2:** Escribir test rápido que verifica `update_job` API call shape
143
+ 3. **Antes de Fase 3:** Implementar `JSON.parse(JSON.dump())` comparación para `default_arguments`
144
+ 4. **Durante Fase 3:** Verificar que `extract_current_config` no extraiga timestamps