DKOps 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.
Files changed (35) hide show
  1. dkops-0.1.0/LICENSE +21 -0
  2. dkops-0.1.0/PKG-INFO +406 -0
  3. dkops-0.1.0/README.md +366 -0
  4. dkops-0.1.0/pyproject.toml +100 -0
  5. dkops-0.1.0/setup.cfg +4 -0
  6. dkops-0.1.0/src/DKOps/__init__.py +0 -0
  7. dkops-0.1.0/src/DKOps/environment_config.py +385 -0
  8. dkops-0.1.0/src/DKOps/launcher.py +368 -0
  9. dkops-0.1.0/src/DKOps/logger_config.py +299 -0
  10. dkops-0.1.0/src/DKOps/table_governance/__init__.py +34 -0
  11. dkops-0.1.0/src/DKOps/table_governance/contracts/__init__.py +0 -0
  12. dkops-0.1.0/src/DKOps/table_governance/contracts/loader.py +669 -0
  13. dkops-0.1.0/src/DKOps/table_governance/contracts/validator.py +269 -0
  14. dkops-0.1.0/src/DKOps/table_governance/migrations/__init__.py +0 -0
  15. dkops-0.1.0/src/DKOps/table_governance/migrations/safe_migrator.py +270 -0
  16. dkops-0.1.0/src/DKOps/table_governance/readers/__init__.py +3 -0
  17. dkops-0.1.0/src/DKOps/table_governance/readers/table_reader.py +297 -0
  18. dkops-0.1.0/src/DKOps/table_governance/writers/__init__.py +15 -0
  19. dkops-0.1.0/src/DKOps/table_governance/writers/append_writer.py +42 -0
  20. dkops-0.1.0/src/DKOps/table_governance/writers/base_writer.py +384 -0
  21. dkops-0.1.0/src/DKOps/table_governance/writers/create_writer.py +91 -0
  22. dkops-0.1.0/src/DKOps/table_governance/writers/delete_writer.py +111 -0
  23. dkops-0.1.0/src/DKOps/table_governance/writers/partition_writer.py +75 -0
  24. dkops-0.1.0/src/DKOps/table_governance/writers/table_writer.py +162 -0
  25. dkops-0.1.0/src/DKOps/table_governance/writers/upsert_writer.py +110 -0
  26. dkops-0.1.0/src/DKOps.egg-info/PKG-INFO +406 -0
  27. dkops-0.1.0/src/DKOps.egg-info/SOURCES.txt +33 -0
  28. dkops-0.1.0/src/DKOps.egg-info/dependency_links.txt +1 -0
  29. dkops-0.1.0/src/DKOps.egg-info/requires.txt +16 -0
  30. dkops-0.1.0/src/DKOps.egg-info/top_level.txt +1 -0
  31. dkops-0.1.0/tests/test_contracts.py +280 -0
  32. dkops-0.1.0/tests/test_luncher.py +283 -0
  33. dkops-0.1.0/tests/test_safe_migrator.py +282 -0
  34. dkops-0.1.0/tests/test_table_reader.py +368 -0
  35. dkops-0.1.0/tests/test_table_writer.py +382 -0
dkops-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Roberto Sanchez Figueroa
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.
dkops-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,406 @@
1
+ Metadata-Version: 2.4
2
+ Name: DKOps
3
+ Version: 0.1.0
4
+ Summary: Lakehouse automation framework for DataOps, DevOps, QA, and Medallion architecture on Databricks
5
+ Author-email: Roberto <brrsanchezfi@unal.edu.co>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/brrsanchezfi/DKOps
8
+ Project-URL: Documentation, https://github.com/brrsanchezfi/DKOps
9
+ Project-URL: Repository, https://github.com/brrsanchezfi/DKOps
10
+ Keywords: databricks,spark,delta,lakehouse,dataops,medallion
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Operating System :: OS Independent
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: loguru>=0.7.0
27
+ Provides-Extra: local
28
+ Requires-Dist: pyspark==3.5.3; extra == "local"
29
+ Requires-Dist: delta-spark==3.2.0; extra == "local"
30
+ Provides-Extra: databricks-connect
31
+ Requires-Dist: databricks-connect<16.0,>=14.0; extra == "databricks-connect"
32
+ Requires-Dist: zstandard>=0.21.0; extra == "databricks-connect"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
36
+ Requires-Dist: black>=23.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
+ Requires-Dist: mypy>=1.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ <div align="center">
42
+
43
+ # DKOps
44
+
45
+ **Framework de gobierno de tablas Delta y orquestación de pipelines Spark para entornos híbridos local ↔ Databricks.**
46
+
47
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
48
+ [![PySpark](https://img.shields.io/badge/pyspark-3.5+-orange.svg)](https://spark.apache.org/)
49
+ [![Delta Lake](https://img.shields.io/badge/delta--lake-3.2+-00ADD4.svg)](https://delta.io/)
50
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
51
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#-contribuir)
52
+
53
+ *El mismo código corre en tu PC y en Databricks — sin cambios.*
54
+
55
+ </div>
56
+
57
+ ---
58
+
59
+ ## ¿Qué es DKOps?
60
+
61
+ DKOps es un framework Python que **profesionaliza la construcción de pipelines de datos** sobre Spark + Delta Lake. Resuelve los problemas que aparecen cuando un equipo crece más allá de "scripts sueltos":
62
+
63
+ - **Contratos de tabla** — el schema, los permisos, el particionado y los metadatos viven en JSON versionado, no enterrados en código.
64
+ - **`TableWriter`** — API unificada: `overwrite`, `append`, `upsert`, `overwrite_partition`, `delete`. Un solo objeto, sin ruido.
65
+ - **merge_schema** — declara `"merge_schema": true` en el contrato y Delta añade columnas nuevas automáticamente al hacer append, sin recrear la tabla.
66
+ - **Enmascaramiento de columnas** — declara `"mask": "security.fn"` en una columna y el framework aplica `ALTER TABLE … SET MASK` post-escritura en Unity Catalog.
67
+ - **Migraciones seguras** — `SafeMigrator` compara contrato vs estado real y genera un plan de cambios sin pérdida de datos.
68
+ - **Runtime-agnóstico** — el mismo pipeline corre en local PC (Spark + Delta) y en Databricks (Connect o cluster nativo). El framework detecta el entorno y se adapta.
69
+ - **Configuración por entorno** — placeholders `{catalog.bronze}`, `{path.silver}` se resuelven contra `dev`/`prod` desde un único `config.json`.
70
+
71
+ ```python
72
+ from DKOps.launcher import Launcher
73
+ from DKOps.table_governance import load_contract, TableWriter
74
+
75
+ launcher = Launcher("config/config.json")
76
+ contract = load_contract("tables/fact_ventas.json")
77
+
78
+ TableWriter(contract).overwrite(df) # full load
79
+ TableWriter(contract).upsert(df_nuevo, keys=["venta_id"]) # SCD1
80
+ TableWriter(contract).append(df_evolucionado) # schema evolution automática
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Tabla de contenidos
86
+
87
+ - [Arquitectura](#-arquitectura)
88
+ - [Instalación](#-instalación)
89
+ - [Requisitos](#requisitos)
90
+ - [Entorno local PC (`.venv-local`)](#entorno-local-pc-venv-local)
91
+ - [Entorno Databricks Connect (`.venv-databricks`)](#entorno-databricks-connect-venv-databricks)
92
+ - [Configuración](#-configuración)
93
+ - [Quickstart](#-quickstart)
94
+ - [Demos](#-demos)
95
+ - [Build](#-build)
96
+ - [Estado del proyecto](#-estado-del-proyecto)
97
+ - [Contribuir](#-contribuir)
98
+ - [Licencia](#-licencia)
99
+
100
+ ---
101
+
102
+ ## 🏗️ Arquitectura
103
+
104
+ ```
105
+ DKOps/
106
+ ├── launcher.py # punto de entrada — detecta runtime y crea SparkSession
107
+ ├── environment_config.py # resuelve catalogs/paths/secrets según workspace activo
108
+ ├── logger_config.py # logging estructurado (loguru) con contexto
109
+ └── table_governance/
110
+ ├── contracts/
111
+ │ ├── loader.py # carga JSON → TableContract tipado (merge_schema, mask)
112
+ │ └── validator.py # valida DataFrame contra contrato (tipos, nulls)
113
+ ├── writers/
114
+ │ ├── table_writer.py # ★ fachada pública: overwrite/append/upsert/delete
115
+ │ ├── base_writer.py # bridge local PC ↔ Databricks + merge_schema + masks
116
+ │ ├── create_writer.py # CREATE OR REPLACE TABLE
117
+ │ ├── append_writer.py # INSERT INTO
118
+ │ ├── upsert_writer.py # MERGE INTO (SCD1)
119
+ │ ├── partition_writer.py # overwrite de partición específica
120
+ │ └── delete_writer.py # DELETE WHERE
121
+ └── migrations/
122
+ └── safe_migrator.py # compara contrato vs tabla real → plan de migración
123
+ ```
124
+
125
+ **Filosofía:** pasar `spark` y `env` a cada componente es ruido. El `Launcher` se auto-registra como singleton del proceso; los writers, loaders y migrator obtienen lo que necesitan vía `Launcher.current()`. La API queda mínima: `TableWriter(contract).overwrite(df)`.
126
+
127
+ ---
128
+
129
+ ## 📦 Instalación
130
+
131
+ ### Requisitos
132
+
133
+ - **Python 3.10+** (3.11 recomendado)
134
+ - **Java 11 o 17** (requerido por Spark)
135
+ - **Git**
136
+
137
+ DKOps se distribuye con `pyproject.toml`. Recomendamos dos virtual environments separados — uno para correr localmente con Spark, otro para Databricks Connect — porque tienen dependencias incompatibles entre sí (PySpark vanilla vs `databricks-connect`).
138
+
139
+ ### Entorno local PC (`.venv-local`)
140
+
141
+ Para desarrollo y tests en tu máquina con Spark + Delta Lake configurados desde cero.
142
+
143
+ ```bash
144
+ # 1. Clonar el repo
145
+ git clone https://github.com/brrsanchezfi/BigDataFrameworkSpark.git
146
+ cd <NOMBRE_REPO>
147
+
148
+ # 2. Crear el venv local
149
+ python3 -m venv .venv-local
150
+ source .venv-local/bin/activate # Linux/Mac/WSL
151
+ # .venv-local\Scripts\activate # Windows PowerShell
152
+
153
+ # 3. Instalar el framework + dependencias locales
154
+ pip install --upgrade pip
155
+ pip install -e ".[local]"
156
+ ```
157
+
158
+ Esto instala:
159
+ - `pyspark` 3.5.x (con Delta Lake configurado vía JARs en runtime)
160
+ - `loguru` para logging estructurado
161
+ - `pytest` para tests
162
+ - DKOps en modo editable (`-e`) — los cambios al código se reflejan al instante
163
+
164
+ **Verificación:**
165
+
166
+ ```bash
167
+ python -c "from DKOps.launcher import Launcher; print('OK')"
168
+ ```
169
+
170
+ ### Entorno Databricks Connect (`.venv-databricks`)
171
+
172
+ Para conectarte desde tu máquina a un cluster Databricks remoto. **No mezcles este venv con el local** — las versiones de PySpark son incompatibles.
173
+
174
+ ```bash
175
+ # 1. Crear el venv (asegúrate de NO tener el local activo)
176
+ deactivate 2>/dev/null
177
+ python3 -m venv .venv-databricks
178
+ source .venv-databricks/bin/activate
179
+
180
+ # 2. Instalar el framework + extras de Databricks
181
+ pip install --upgrade pip
182
+ pip install -e ".[databricks]"
183
+ ```
184
+
185
+ Esto instala:
186
+ - `databricks-connect` (versión que coincida con el runtime de tu cluster)
187
+ - `databricks-sdk`
188
+ - `loguru`, `pytest`
189
+ - DKOps en modo editable
190
+
191
+ **Configurar credenciales** (PAT o OAuth):
192
+
193
+ ```bash
194
+ # Opción A: Personal Access Token (rápido para desarrollo)
195
+ export DATABRICKS_HOST="https://<workspace>.azuredatabricks.net"
196
+ export DATABRICKS_TOKEN="<tu-pat>"
197
+
198
+ # Opción B: OAuth via Databricks CLI (recomendado para uso prolongado)
199
+ databricks auth login
200
+ ```
201
+
202
+ Luego edita tu `config.json`:
203
+
204
+ ```json
205
+ {
206
+ "EXECUTION_ENVIRONMENT": "databricks",
207
+ "CLUSTER_ID": "<tu-cluster-id>"
208
+ }
209
+ ```
210
+
211
+ **Verificación:**
212
+
213
+ ```bash
214
+ python -c "from databricks.connect import DatabricksSession; \
215
+ DatabricksSession.builder.getOrCreate().sql('SELECT 1').show()"
216
+ ```
217
+
218
+ ### Cuál venv activar
219
+
220
+ | Estás haciendo... | Activa |
221
+ |---|---|
222
+ | Desarrollo del framework, tests unitarios, demos en local | `.venv-local` |
223
+ | Ejecutar contra un cluster Databricks remoto desde la PC | `.venv-databricks` |
224
+ | Notebook dentro del workspace Databricks | Ninguno — usa el del cluster |
225
+
226
+ ---
227
+
228
+ ## ⚙️ Configuración
229
+
230
+ DKOps lee un `config.json` que define:
231
+ - El runtime (`local` o `databricks`).
232
+ - Los **environments** del proyecto (`dev`, `prod`) con sus catálogos, paths y secrets scopes.
233
+ - Configuración de logging.
234
+
235
+ Estructura mínima:
236
+
237
+ ```json
238
+ {
239
+ "EXECUTION_ENVIRONMENT": "local",
240
+ "SPARK_APP_NAME": "miPipeline",
241
+ "SPARK_WAREHOUSE_DIR": "/tmp/spark-warehouse",
242
+ "DELTA_VERSION": "3.2.0",
243
+
244
+ "environments": {
245
+ "<workspace_id>": {
246
+ "env": "dev",
247
+ "env_short": "d",
248
+ "catalogs": {
249
+ "bronze": "bronze_dev",
250
+ "silver": "silver_dev",
251
+ "gold": "gold_dev"
252
+ },
253
+ "paths": {
254
+ "bronze": "abfss://bronze@<storage>.dfs.core.windows.net",
255
+ "silver": "abfss://silver@<storage>.dfs.core.windows.net"
256
+ }
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ DKOps busca el config en este orden:
263
+ 1. Argumento explícito: `Launcher("ruta/config.json")`
264
+ 2. Variable de entorno: `PATH_CONFIG_LAUNCHER=ruta/config.json`
265
+
266
+ ---
267
+
268
+ ## 🚀 Quickstart
269
+
270
+ ```python
271
+ from DKOps.launcher import Launcher
272
+ from DKOps.table_governance import load_contract, TableWriter
273
+
274
+ # 1. Inicializa el Launcher (auto-detecta runtime, crea SparkSession)
275
+ launcher = Launcher("config/config.json")
276
+
277
+ # 2. Carga un contrato JSON — los placeholders {catalog.silver} se resuelven solos
278
+ contract = load_contract("tables/fact_ventas.json")
279
+
280
+ # 3. Construye tu DataFrame (de un source, una transformación, lo que sea)
281
+ df = launcher.spark.read.parquet("source/ventas.parquet")
282
+
283
+ # 4. Escribe — full load inicial
284
+ TableWriter(contract).overwrite(df)
285
+
286
+ # 5. Día siguiente — solo añadir lo nuevo
287
+ TableWriter(contract).upsert(df_delta, keys=["venta_id", "fecha"])
288
+
289
+ # 6. Schema evolution — el contrato tiene merge_schema: true
290
+ df_nuevo_campo = df_delta.withColumn("canal", lit("web"))
291
+ TableWriter(contract).append(df_nuevo_campo) # Delta añade la columna automáticamente
292
+ ```
293
+
294
+ Para ejemplos completos con varias capas y tests, ver la carpeta [`demos/`](demos/).
295
+
296
+ ---
297
+
298
+ ## 📚 Demos
299
+
300
+ Cada demo es **independiente y autocontenido**, pensado como referencia de uso.
301
+
302
+ | Demo | Tema | Qué demuestra |
303
+ |---|---|---|
304
+ | [`demos/demo_1`](demos/demo_1) | Contratos y writers gobernados | Bootstrap, append, upsert, partition overwrite, delete y migración con `SafeMigrator`. Dominio: aeronáutica. |
305
+ | [`demos/demo_2`](demos/demo_2) | Transformaciones testeables y Data Quality | Pipeline bronze → silver → gold con funciones puras de transformación, tests `pytest` y motor de DQ declarativo. Dominio: manufactura de aseo. |
306
+ | [`demos/demo_3`](demos/demo_3) | merge_schema y enmascaramiento | Schema evolution con `merge_schema: true` y column masking con `mask` en contratos. Dominio: e-commerce. |
307
+
308
+ Para correr un demo:
309
+
310
+ ```bash
311
+ source .venv-local/bin/activate
312
+ cd demos/demo_1
313
+ python pipeline_aeronautica.py
314
+ ```
315
+
316
+ ---
317
+
318
+ ## 🔨 Build
319
+
320
+ DKOps usa `pyproject.toml` (PEP 517/621). Para construir el wheel distribuible:
321
+
322
+ ```bash
323
+ source .venv-local/bin/activate
324
+ pip install --upgrade build
325
+ python -m build
326
+ ```
327
+
328
+ Esto genera en `dist/`:
329
+ - `dkops-X.Y.Z-py3-none-any.whl` — wheel para instalar en Databricks o cualquier entorno
330
+ - `dkops-X.Y.Z.tar.gz` — sdist
331
+
332
+ **Subir a Databricks** como librería del cluster:
333
+
334
+ ```bash
335
+ databricks libraries install --cluster-id <id> --whl dist/dkops-X.Y.Z-py3-none-any.whl
336
+ ```
337
+
338
+ **Versionado:** DKOps sigue [Semantic Versioning](https://semver.org/). La versión vive en `pyproject.toml`.
339
+
340
+ ---
341
+
342
+ ## 📊 Estado del proyecto
343
+
344
+ | Componente | Estado |
345
+ |---|---|
346
+ | `Launcher` (multi-runtime) | ✅ Estable |
347
+ | Contratos + `ContractLoader` | ✅ Estable |
348
+ | `TableWriter` (fachada unificada) | ✅ Estable |
349
+ | Writers individuales (`Create`, `Append`, `Upsert`, `Partition`, `Delete`) | ✅ Estables |
350
+ | `merge_schema` (schema evolution) | ✅ Disponible |
351
+ | Enmascaramiento de columnas (`mask`) | ✅ Disponible (Databricks / Unity Catalog) |
352
+ | `SafeMigrator` (esquema seguro) | ✅ Estable |
353
+ | Demos (1, 2, 3) | ✅ Disponibles |
354
+ | Tests del framework (36 tests) | ✅ Disponibles |
355
+ | Documentación de API | 🚧 En desarrollo |
356
+ | Soporte SCD2 | 📋 Backlog |
357
+ | Módulo de Data Quality nativo | 📋 Backlog (existe prototipo en `demo_2`) |
358
+
359
+ ---
360
+
361
+ <div align="center">
362
+
363
+ ## 🤝 Contribuir
364
+
365
+ **¿Te interesa lo que estamos construyendo? Las contribuciones son bienvenidas y muy apreciadas.**
366
+
367
+ [![Issues abiertos](https://img.shields.io/github/issues/brrsanchezfi/BigDataFrameworkSpark)](https://github.com/brrsanchezfi/BigDataFrameworkSpark/issues)
368
+ [![PRs abiertos](https://img.shields.io/github/issues-pr/brrsanchezfi/BigDataFrameworkSpark)](https://github.com/brrsanchezfi/BigDataFrameworkSpark/pulls)
369
+ [![Last commit](https://img.shields.io/github/last-commit/brrsanchezfi/BigDataFrameworkSpark)](https://github.com/brrsanchezfi/BigDataFrameworkSpark/commits)
370
+
371
+ </div>
372
+
373
+ Áreas donde nos vendría especialmente bien ayuda:
374
+
375
+ - 🧪 **Más tests** — la suite cubre contratos, writers y migrator; faltan tests de integración y cobertura de casos extremos.
376
+ - 📖 **Documentación** — guías de uso, referencia de API, casos reales.
377
+ - 🎨 **Más demos** — dominios distintos, patrones distintos.
378
+ - 🐛 **Reportar bugs** — abre un issue con un caso reproducible.
379
+ - 💡 **Discutir ideas** — el módulo de Data Quality, soporte SCD2, integración con Great Expectations son temas abiertos.
380
+
381
+ ### Cómo contribuir
382
+
383
+ 1. **Haz fork** del repo y crea una rama: `git checkout -b feature/mi-mejora`
384
+ 2. Activa el venv local: `source .venv-local/bin/activate`
385
+ 3. Haz tus cambios siguiendo el estilo del código existente.
386
+ 4. Si añades funcionalidad, **añade un test o un demo** que la demuestre.
387
+ 5. Verifica que los demos siguen pasando: `cd demos/demo_2 && pytest`
388
+ 6. Abre un Pull Request describiendo el cambio y por qué es útil.
389
+
390
+ ¿Primera vez contribuyendo a un proyecto open source? Consulta [esta guía de GitHub](https://docs.github.com/es/get-started/quickstart/contributing-to-projects).
391
+
392
+ ---
393
+
394
+ ## 📄 Licencia
395
+
396
+ DKOps se distribuye bajo licencia MIT. Ver [`LICENSE`](LICENSE) para los términos completos.
397
+
398
+ ---
399
+
400
+ <div align="center">
401
+
402
+ **Hecho con ☕ y ❤️ por el equipo de Data Engineering.**
403
+
404
+ Si DKOps te resulta útil, considera darle una ⭐ al repo — ayuda a que otros lo encuentren.
405
+
406
+ </div>