Changelog¶
All notable changes to DKOps are documented here.
Format: Keep a Changelog · Versioning: Semantic Versioning.
[0.3.6] — 2026-09-17¶
Release de documentación y limpieza del repositorio. Lo único que cambia en
src/ son docstrings, así que actualizar desde la 0.3.5 no altera el
comportamiento de ningún pipeline.
Removed¶
databricks.yml. Era un Databricks Asset Bundle de otro proyecto (proyecto_aeronautica) con hosts de workspace, cuentas de storage, scopes de Key Vault y centros de coste codificados. El framework se configura conconfig.jsony nada en el repositorio lo usaba- Diagramas generados por
pyreverseypydepsque estaban versionados en el root:classes_DKOps.doty.png,packages_DKOps.doty.png,DKOps_deps.svgydependencies, más las dos copias dedocs/assets/que ninguna página referenciaba. Se regeneran cuando se necesiten src/DKOps.egg-info/deja de estar versionado. Es un artefacto de build que el.gitignoreya excluía, pero se había commiteado antes de esa regla
Fixed¶
- Nombres de infraestructura real en docstrings y plantillas. Los docstrings
de
environment_config.pyyloader.pyusaban workspace IDs, hosts de Azure, cuentas de storage y catálogos reales como ejemplo, y mkdocstrings los publica en la referencia API del sitio. Igual ocurría enconfig/config-copy.json. Todos pasan a valores de ejemplo. No hay cambios de comportamiento -
.gitignoreignora ahoraderby.log, el directoriofile:/que crea Spark cuando una ruta llega con ese esquema, y los diagramas generados -
Tres afirmaciones de la documentación que no coincidían con el código.
TableWriter.delete(preview=True)se documentaba como una previsualización que no borraba, cuando en realidad muestra las filas afectadas y las borra (para verlas sin borrar hay que combinarlo condry_run=True).incremental_replacese describía como un upsert pormerge_keys, pero reemplaza la partición más reciente conoverwrite_partition. Y la guía de migraciones seguía usando la firma antiguaSafeMigrator(spark, contract, env), que dejó de existir cuando el migrador pasó a resolver Spark desde elLauncheractivo - Los enlaces a
LICENSEyCHANGELOG.mddel README eran rutas relativas del repositorio. GitHub las resuelve, pero PyPI no: en la página del paquete daban - Ahora son URL absolutas
Added¶
- Documentación de
TableReader, que no tenía ninguna página propia pese a ser parte de la API pública desde la 0.3.0:read()con sus tres atajos,read_partition(),read_stream()yread_cdf()con sus requisitos - Páginas nuevas de conceptos: los dos tipos de contrato y cómo se referencian entre sí, los flujos internos de una escritura y de una promoción, y qué cambia exactamente entre local y Databricks
- Guía de operación separada en cuatro páginas: logger de aplicación, logs en almacenamiento cloud, registro operativo y consultas SQL de monitoreo
- Índice de la referencia API y páginas de entrada por sección
Changed¶
- La web pasa de siete páginas largas a unas treinta páginas cortas organizadas en pestañas, con navegación Anterior y Siguiente al pie. Antes la guía de ingesta sola ocupaba 350 líneas y la de logging 320
- Tema visual propio: cabecera sobria, paleta verde azulado, tipografía Inter y JetBrains Mono, tablas y avisos más ligeros, y modo claro y oscuro consistentes
- Portada nueva con las cuatro capas Medallion y acceso directo a cada sección
- El changelog se publica también en la web
- Redacción revisada en toda la documentación
- El workflow de la web fija
mkdocs>=1.6,<2. Material for MkDocs advierte de que MkDocs 2.0 elimina el sistema de plugins, así que sin la restricción el despliegue podía romperse en cualquier momento al instalar sin pinear
[0.3.5] — 2026-09-06¶
Fixed¶
- El log en almacenamiento cloud podía quedar a 0 bytes (#30). El handler de
dbutils.fs.putacumulaba el contenido completo en memoria y reescribía el fichero entero conoverwrite=Trueen cada sincronización. Eseputtrunca el destino antes de volcar y devuelve el control —imprimiendoWrote N bytes.— antes de que el blob esté confirmado, así que un proceso que muriera dentro de esa ventana no perdía el último tramo: perdía todo el histórico. Intermitente por naturaleza: de 13 ficheros de log de un proyecto, 5 quedaron vacíos - El sink del puente JVM tenía un
except Exception: passque descartaba en silencio los errores de escritura. Ahora los cuenta y los reporta (#30)
Changed¶
- La escritura cloud pasa a hacerse por tramos. Cada sincronización escribe solo el contenido nuevo en un objeto propio —
<nombre>.<run>.0001.log,.0002.log…— que no se vuelve a tocar nunca. No asume nada del almacenamiento: ni escritura atómica, ni renombrado atómico, que en ADLS solo lo es con espacio de nombres jerárquico. Un fallo pierde como mucho el último tramo (#30) - El token de ejecución en el nombre evita además que dos procesos que escriban el mismo log se sobrescriban (#30)
- Un tramo que falla vuelve a la cola y se reintenta en la sincronización siguiente, en lugar de perderse (#30)
- Los fallos de escritura se reportan por
stdout, nostderr: es lo que Databricks captura y lo que sigue vivo durante el apagado del intérprete. Al terminar se distingue entre un log realmente incompleto y uno que tuvo fallos pero los recuperó (#30)
Added¶
AppLogger.flush()— vuelca lo pendiente con el proceso todavía vivo. El volcado poratexitsigue existiendo, pero en el apagado un fallo ya no tiene quién lo recoja (#30)AppLogger.read_cloud_log()— reconstruye un log escrito por tramos, concatenándolo en orden. Admite filtrar por ejecución (#30)tests/test_cloud_log_handler.py— 10 tests con undbutilsfalso que registra cada escritura, para poder aseverar que ningún objeto se toca dos veces. No existía ninguna prueba de este handler
Notes¶
- El volcado del apagado ya no reescribe nada si no hay contenido nuevo. En el caso reportado, la tarea afectada tenía su último mensaje justo en un múltiplo de la frecuencia de sincronización, de modo que el
atexitrepetía unputcompleto redundante en el momento de mayor riesgo (#30) - Los tramos de ejecuciones anteriores no se ven afectados: el formato nuevo convive con los ficheros que ya hubiera en el directorio
[0.3.4] — 2026-09-01¶
Fixed¶
IngestionOpsLoggersolo registrabaSTARTED(#28).started_atestaba declaradonullable=False, perolog_success()ylog_failure()no lo pasan —un cierre no reabre el inicio—, así quecreateDataFramerechazaba toda fila de cierre conPySparkValueError: [CANNOT_BE_NONE]. La excepción la absorbía unexceptque solo emitía un warning, de modo que la ingesta terminaba en verde con la tabla de control incompleta. Afectaba por igual aBronzeIngestory aSilverPromoter: ninguna ingesta ni promoción registraba cierre- El
exceptde_write_row()registra ahora el fallo comoERROR, con el tipo de excepción, elstatusy elrun_id. Sigue sin relanzar: tumbar una ingesta que fue bien porque no se pudo anotar el cierre sería peor que el problema (#28)
Changed¶
IngestionOpsLoggerrecuerda elstarted_atde cadarun_idy lo repite en la fila de cierre, de modo que la duración sale de una resta y no de un self-join. Si el proceso se reinicia entre apertura y cierre, la fila se escribe igualmente constarted_ataNULLen lugar de perderse (#28)
Added¶
- Guía Logging y registro operativo — distingue el logger de aplicación (
LoggableMixin, consola y archivo) del registro operativo (IngestionOpsLogger, tabla Delta consultable), con el esquema de la tabla, consultas de tasa de éxito, duración y ejecuciones sin cerrar, y cómo usar el registro fuera del engine tests/integration/test_ops_logger_spark.py— 10 tests con Spark y Delta reales sobre el ciclo completo del registro operativo
Notes¶
- Los dos tests que existían del
OpsLoggerno probaban nada.test_log_start_returns_run_idnunca llamaba alog_start(): obtenía la función con.__func__sin invocarla y luego aseveraba queuuid.uuid4()recortado a 8 mide 8.test_ops_schema_has_required_fieldsleía el texto fuente del módulo coninspect.getsource()y buscaba substrings, sin construir jamás un DataFrame. Ambos en verde sobre el único componente roto. Reescritos para aseverar sobre elStructTypereal y sobre las filas que arma cada método (#28) - Las tablas de control creadas por versiones anteriores tienen
started_atcomoNOT NULLen la metadata de Delta. Las filas de cierre normales llevan valor y se escriben sin problema; solo el caso de reinicio del proceso necesitaría recrear la tabla (#28)
[0.3.3] — 2026-08-30¶
Fixed¶
cdc_mergepropagabais_deleted = NULLa Silver (#25). El default solo se aplicaba cuando la columna faltaba en el DataFrame; bastaba con que llegara desde Bronze —por ejemplo porque Auto Loader la incorporó al esquema de la landing— para que dejara de aplicarse. El daño no está en Silver sino aguas abajo: el filtro natural esWHERE NOT is_deleted, y con lógica ternariaNOT NULLno esTRUE, así que las filas vigentes desaparecen del resultado sin error alguno. Nuevo helper_ensure_is_deleted(), que rellena en vez de comprobar presencia- El contrato no gobernaba la ubicación de la tabla (#26). Solo
CreateWriteremitíaLOCATION; los demás caminos creaban la tabla desde el esquema del DataFrame consaveAsTable()otoTable(), que ignorantype: EXTERNALylocationy la dejan MANAGED en el almacenamiento interno de Unity Catalog._write_df()y_write_stream()pasan ahorapathcuando el contrato es externo AppendWriterno aplicaba la metadata del contrato (#26). Era el único camino de creación que quedó sin cubrir al corregir #18 env0.3.1: las tablas que creaba nacían sin comentarios en el catálogo
Notes¶
- La opción
pathse pasa solo si la tabla no existe todavía. Sobre una tabla ya creada, Spark rechaza la escritura cuando la ubicación no coincide, y eso rompería a quien ya venía escribiendo contra una tabla MANAGED creada por este mismo fallo. Convertir esas tablas a EXTERNAL sigue siendo una decisión explícita (#26) - En los eventos I/U se respeta el
is_deletedque traiga el origen y solo se rellenan los nulos; en los eventos D la baja se marca siempre (#25) SchemaValidatorno validanullablehoy. La sugerencia del #25 de avisar cuando una columna no nula va a escribirse con nulos exigiría escanear el DataFrame antes de cada escritura —imposible en streaming— así que queda fuera de esta versión
[0.3.2] — 2026-08-24¶
Fixed¶
license = "MIT"impedía instalar el paquete desde git en Databricks (#23). La forma corta de PEP 639 solo la entiendesetuptools >= 77, perobuild-system.requiresdeclara>= 68. En cualquier entorno que resuelva un setuptools anterior, la generación de metadatos falla coninvalid pyproject.toml config: project.licensey el cluster aborta conERROR_WHEEL_BUILD, de modo que ninguna tarea llega a ejecutarse. Vuelve a la forma de tablalicense = { text = "MIT" }, válida en todas las versiones, y restaura el classifierLicense :: OSI Approved :: MIT License- El wheel publicado en PyPI no estaba afectado: se distribuye ya construido y no vuelve a generar metadatos al instalarse. El fallo solo se daba instalando desde el repositorio
Added¶
- Workflow
build-min-setuptools.yml— construye el paquete con exactamente el setuptools mínimo declarado enbuild-system.requires, sin aislamiento de build. El mínimo declarado y el real pueden separarse sin que nadie lo note, porque pip aísla la construcción y descarga el setuptools más reciente; este job lo habría detectado antes de publicar el tag (#23)
Notes¶
- El tag
v0.3.0no exhibía este fallo porque es anterior al commit203af74, que introdujo la forma corta. Al corregir el desfase del tag en #20,v0.3.1pasó a empaquetar elpyproject.tomlactual y el problema quedó al descubierto
[0.3.1] — 2026-08-23¶
Added¶
TableWriter.apply_contract_metadata()— aplica comentario de tabla, comentarios de columna, masks y permisos del contrato de forma idempotente y sin reescribir datos. Sirve tanto para los caminos de escritura que no pasan porCreateWritercomo para reparar tablas ya creadas (#18)insert_only_columnsenTableWriter.upsert()yUpsertWriter.write()— columnas que el MERGE inserta pero nunca actualiza (#19)_silver_created_aten la promoción Bronze → Silver — las cuatro estrategias añaden ahora las dos columnas queadd_silver_timestampspromete (#19)- Verificación en el workflow de publicación: el tag debe coincidir con
versiondepyproject.tomlo el build falla (#20)
Fixed¶
- La carga inicial de
UpsertWriter(tabla inexistente) dejaba la tabla sin comentarios en el catálogo (#18) BronzeIngestor._write_stream()creaba la tabla viawriteStream.toTable()sin aplicar la metadata delTableContract(#18)add_silver_timestampsproducía dos columnas enMetadataEnricherpero solo_silver_modified_aten las estrategias de promoción, de modo que un contrato Silver que declarara ambas fallaba la validación (#19)- El MERGE de
UpsertWriteractualizaba todas las columnas no-key, lo que habría sobrescrito_silver_created_aten cada ejecución (#19) IngestionEngine.promote_silver()omitía silenciosamente todas las promociones. El engine resolvía elTableContractBronze de cada contrato Silver desde susource_contracty luego descartaba el resultado; la búsqueda efectiva se hacía por nombre contra_bronze_tablesy no encontraba nada salvo que el contrato de ingesta batch, la tabla Bronze y el contrato Silver se llamaran igual. Los cinco demos salían conFaltan contratos src/dst — omitidoy Silver quedaba vacío. El mensaje de WARNING ahora dice cuál de los dos contratos no se pudo resolver
Tests¶
- Nueva suite
tests/integration/con Spark y Delta reales — verifica que_silver_created_atsobrevive a un segundo MERGE y queapply_contract_metadata()deja los comentarios visibles enDESCRIBE TABLE. Excluida de la suite por defecto: debe correr en su propio proceso (pytest tests/integration) - 18 tests nuevos de mocks sobre
apply_contract_metadata,insert_only_columnsy las columnas técnicas de Silver
Notes¶
- El tag
v0.3.0empaqueta unpyproject.tomlque declara0.2.4: se creó antes del commit de bump. Solo afecta a la instalación desde git — el0.3.0de PyPI se publicó víaworkflow_dispatchdesdemain, que ya tenía la versión subida, y está correctamente etiquetado. Se corrige publicando0.3.1; el tagv0.3.0se deja intacto para no romper instalaciones existentes (#20) - La verificación tag ↔ versión solo aplica al evento
release. Una publicación lanzada conworkflow_dispatchno tiene tag contra el que comparar y sigue publicando lo que haya en la rama por defecto (#20)
[0.3.0] — 2026-05-23¶
Added¶
IngestionEngine— orquestador principal:ingest_bronze(),run_streaming(),promote_silver(),status()BronzeIngestor— ingesta Landing → Bronze con partition overwrite idempotente por_ingested_dateSilverPromoter— aplica estrategias declarativas desde contratos JSON- Estrategia
full_merge— MERGE INTO con dedup por watermark (SCD Type 1) - Estrategia
cdc_merge— CDC I/U/D con soft delete viais_deleted - Estrategia
incremental_replace— upsert de la partición más reciente por watermark - Estrategia
append_dedup— anti-join append para eventos y clickstream FileStreamReader— lectura streaming con auto-inferencia de schema desde archivos existentesLoadType.STREAMING— tipo de carga semántico para contratos streaming- Tabla de control operativo — registro por dataset de filas, estado, timestamps y run_id
- 5 demos end-to-end verificados — Aeronáutica, Manufactura, E-commerce, Retail, Marketplace
- Documentación completa — diagramas Mermaid, guía de ingesta, quickstart actualizado, 5 páginas de demos
Fixed¶
CdcMergeStrategy._apply_deletes()— añade_silver_modified_aty aplica_select_for_silver()en soft deletesCdcMergeStrategy— añadeis_deleted=Falseen upserts cuando la columna está en el contrato SilverAppendDedupStrategy— añade_silver_modified_atantes de_select_for_silver()IncrementalReplaceStrategy— añade_silver_modified_atantes de_select_for_silver()FileStreamReader—readStreamahora infiere schema desde archivos estáticos existentes (evitaAnalysisException)- Contratos demo_2/demo_5 — tipos de columna alineados con lo que Spark
inferSchemaproduce (STRING vs DATE/TIMESTAMP)
Changed¶
- Versión de desarrollo Alpha → Beta (
Development Status :: 4 - Beta) - Descripción del paquete actualizada para reflejar IngestionEngine y arquitectura Medallion
- URLs del proyecto apuntan a GitHub Pages en lugar del repositorio raw
Removed¶
- Scripts obsoletos
pipeline_aeronautica.py,pipeline_manufactura.py,pipeline_ecommerce.py,pipeline_lectura.py data_generator.py×4 (reemplazados por directoriosdatagen/por demo)- Directorio
build/(artefactos compilados)
[0.2.4] — anterior¶
TableWriter— API unificada:overwrite,append,upsert,overwrite_partition,deleteTableReader—read(),read_partition(),read_stream(),read_cdf()SafeMigrator— comparación contrato vs estado real con planALTER TABLEContractLoader— carga y resolución de placeholders en contratos JSONSchemaValidator— validación de tipos y nulabilidad pre-escritura- Runtime detector local / Databricks — mismo código sin cambios