Skip to main content

Cotejo

Detección de duplicados y búsqueda por similitud sobre datos tabulares, con las heurísticas que tú defines.

La mayoría de las herramientas de fuzzy matching saben decirte cuánto se parecen dos textos. El problema real casi nunca es ese. El problema es que dos registros muy parecidos suelen ser cosas distintas: el mismo jugo en presentación de 355 ml y de 600 ml, el mismo tornillo en dos longitudes, la misma refacción con un número de pieza diferente.

Cotejo trata ese conocimiento como configuración de primera clase. Tú declaras qué columnas comparar, con qué papel y bajo qué reglas; el motor te explica cada veredicto en lugar de entregarte un número sin origen.

Estado: las cinco fases del plan están completas — motor, aplicación de escritorio, catálogos de prueba con la respuesta conocida, documentación y aprendizaje del uso — y sobre ellas están los tres modos de trabajo: limpiar un catálogo, cruzar dos y buscar dentro de uno. El plan está en docs/arquitectura.md, y lo que cambió en cada versión, en CHANGELOG.md.


De dónde viene

De un sistema real en Excel y VBA, de unas cinco mil líneas, que detectaba materiales duplicados en el catálogo de una planta automotriz: unos 270 000 registros y una decena de heurísticas afinadas a base de revisar falsos positivos uno por uno.

Ese sistema funcionaba, pero tenía las reglas del dominio cableadas en el código y los nombres de las columnas fijos. Cotejo conserva las heurísticas —que son la parte difícil— y las convierte en configuración. El caso automotriz pasa a ser un perfil de ejemplo, no el programa.


Requisitos

Python 3.10 o superior. Nada más.

Lo único que hace falta instalar aparte son dos bibliotecas pequeñas, y pip se encarga: pydantic valida los perfiles y pyyaml los lee.

Todo lo demás ya viene dentro de Python:

Necesidad De dónde sale
Guardar el proyecto y las decisiones sqlite3incluido en Python, no se instala
Leer y escribir CSV csv — incluido
Normalizar acentos unicodedata — incluido

Los extras son opcionales y ninguno cambia los resultados:

Extra Para qué Instalar
rich Consola con formato pip install "cotejo[cli]"
rapidfuzz Distancias unas cien veces más rápidas pip install "cotejo[speed]"
openpyxl Leer archivos de Excel pip install "cotejo[excel]"
PySide6 La aplicación de escritorio pip install "cotejo[gui]"

Instalación

pip install "cotejo[gui]"    # con interfaz gráfica
pip install cotejo           # solo el motor y la consola
cotejo-gui                   # abre la ventana

O desde el repositorio, para tocar el código:

git clone https://github.com/gabinoruizramirez/cotejo
cd cotejo
pip install -e ".[dev]"

¿Sin Python? Cada publicación trae un Cotejo.exe para Windows: se descarga, se hace doble clic y se abre. No instala nada ni pide permisos de administrador. Los tres caminos, con sus pegas, están en docs/instalar.md.

Comprobar que todo está

cotejo doctor
  ok    Python                             3.13.1 en Windows
  ok    sqlite3 (guardar el proyecto)      incluido en Python · motor 3.45.1
  ok    csv (leer y escribir CSV)          incluido en Python
  ok    unicodedata (normalizar acentos)   incluido en Python
  ok    pydantic (validar perfiles)        2.13.3
  ok    yaml (leer perfiles)               6.0.3
  ok    PySide6 (interfaz gráfica)         Qt 6.11.2

  Todo lo necesario está. Puedes ejecutar el proyecto.

Si algo falta, el comando dice qué falta y cómo instalarlo, distinguiendo lo obligatorio de lo opcional.


La aplicación

cotejo-gui                 # o bien:  cotejo gui datos.csv

Seis pasos en el orden real del trabajo. Los que todavía no tienen sentido aparecen desactivados, para que la ventana enseñe el camino en vez de exigir que uno ya lo conozca.

Abrir un archivo y ya tener un perfil

Mapeo de campos

Al cargar el archivo, Cotejo mira cada columna y propone qué papel juega — y escribe por qué. No hay formulario en blanco: hay una propuesta con la que se puede discutir. Corregir un papel es cambiar un desplegable.

Ver qué le pasa a los datos, mientras se decide

Normalización con vista previa

Cada regla se activa, se desactiva y se reordena, y el resultado sobre un valor real del archivo se actualiza al instante. El asterisco marca las reglas que cambiaron algo; las demás también se aplicaron y se muestran igual, para que quede claro que nada ocurre a espaldas de nadie.

Ejecutar y ver qué descartó cada regla

Ejecución

El recuento por veto es la herramienta de calibración: si uno descarta casi todo, está mal configurado para estos datos. Sin ese número, ajustar un perfil es adivinar.

Revisar, con la explicación en castellano

Revisión

Cada grupo con su original arriba y sus posibles duplicados debajo. Al seleccionar uno, el panel responde tres preguntas en orden: en qué coinciden, en qué se diferencian y por qué el sistema concluyó lo que concluyó. Sin umbrales, sin nombres de comparadores, sin puntos.

Dos detalles que importan más de lo que parece. Si dos valores están escritos distinto pero el motor los trata como el mismo —REF-1001 y REF 1001— aparecen como coincidencia y no como diferencia: lo contrario sería contradecir a la propia herramienta. Y solo se muestran los campos que deciden; las existencias, las fechas y las tiendas nunca participaron en el juicio y solo harían ruido.

Quien esté afinando el perfil marca Ver detalle técnico y recupera la versión completa, con cada comparador, su umbral y sus puntos.

El laboratorio de reglas

Laboratorio de reglas

Dos claves y la comparación explicada. Sirve para entender el sistema, para depurar un perfil y para responder la pregunta que siempre llega: ¿por qué dice eso?

La ventana no conoce ninguna regla. Le pregunta al catálogo qué existe, qué parámetros admite cada cosa y de qué tipo son, y arma los formularios con esa respuesta. Una regla propia escrita en un archivo suelto aparece en la interfaz, con su documentación y sus campos, sin tocar una línea de código de interfaz.


El cambio central: roles de campo

El motor no conoce nombres de columnas de ningún dominio. Conoce roles, y tú asignas cada columna de tu archivo a uno:

Rol Para qué Cómo se compara
key Identificador de la fila (obligatorio) No se compara
code SKU, número de parte, ISBN, RFC Canonicalización + distancia de edición
text Descripción en lenguaje natural Tokens y similitud de conjuntos
attribute Color, talla, voltaje, marca Si coincide confirma; si difiere puede vetar
measure 355 ML, 40x20 MM, M8x40 Valor y unidad normalizados, con tolerancia
quantity Existencia, precio Informativo o desempate
date Fecha de alta Elige el registro original del grupo
partition Tienda, centro, país Restringe qué se compara con qué
display Notas, enlaces Solo se muestra

Solo key es obligatorio. Un archivo que solo tenga descripciones funciona; uno que solo tenga códigos también. Puede haber varias columnas del mismo rol: dos códigos, tres descripciones.


Un perfil mínimo

meta:
  name: mi_catalogo

fields:
  - { column: id,          role: key }
  - { column: referencia,  role: code }
  - { column: descripcion, role: text }
  - { column: presentacion, role: measure }

Con eso basta para empezar: cada rol trae una normalización y unos extractores razonables por omisión. Cuando quieras control fino, lo declaras:

fields:
  - column: descripcion
    role: text
    normalize:
      - trim
      - uppercase
      - strip_accents
      - { rule: expand_abbreviations, params: { mapping: { "P/": "PARA ", "TORN": "TORNILLO" } } }
      - contextual_dot
      - collapse_spaces

La función que hace distinto a este proyecto: explain

Una normalización agresiva es útil y peligrosa a la vez. En lugar de describirla en prosa, Cotejo la demuestra sobre tu dato:

cotejo explain industrial_catalog -c descripcion -v "TORN. CAB. P/CILINDRO NR.39 39-D-1301 135°"
columna: descripcion   rol: text

normalización
  entrada  "TORN. CAB. P/CILINDRO NR.39 39-D-1301 135°"
    01 trim  ->  (sin cambio)
    02 strip_invisible  ->  (sin cambio)
    03 unify_dashes  ->  (sin cambio)
    04 unify_quotes  ->  (sin cambio)
    05 uppercase  ->  (sin cambio)
    06 strip_accents  ->  (sin cambio)
  * 07 expand_abbreviations  ->  "TORNILLO. CABEZA. PARA CILINDRO NR.39 39-D-1301 135°"
  * 08 contextual_dot  ->  "TORNILLO  CABEZA  PARA CILINDRO NR 39 39-D-1301 135°"
  * 09 merge_alphanumeric_segments  ->  "TORNILLO  CABEZA  PARA CILINDRO NR 39 39D1301 135°"
  * 10 strip_degree_sign  ->  "TORNILLO  CABEZA  PARA CILINDRO NR 39 39D1301 135 "
  * 11 collapse_spaces  ->  "TORNILLO CABEZA PARA CILINDRO NR 39 39D1301 135"
  salida   "TORNILLO CABEZA PARA CILINDRO NR 39 39D1301 135"

rasgos extraídos
  tokens          [TORNILLO, CABEZA, CILINDRO, 39D1301, 135]
  numeric_tokens  [39, 39D1301, 135]
  head_word       [TORNILLO]

El asterisco marca las reglas que cambiaron algo. Las demás también se aplicaron: se muestran para que quede claro que nada ocurre a tus espaldas.


Detectar duplicados

cotejo run retail_inventory examples/abarrotes.csv
  registros            7
  bloqueo              10 pares candidatos de 21 posibles (52.38% evitados)
  pares evaluados      10
  descartados por veto
      measure_differs                    8
  pares reportados     2
      duplicado_seguro                   2
  agrupamiento         2 grupos · 0 familias descartadas · 0 solitarios descartados

De siete artículos, dos parejas son el mismo producto capturado dos veces y ocho pares se descartaron por diferencia de presentación. El sistema dice cuántos descartó y por qué regla, que es lo que permite calibrar el perfil en lugar de adivinar.

Y de cualquier par se puede pedir la cuenta completa:

cotejo pair retail_inventory examples/abarrotes.csv 1 3
1 y 3: no son el mismo

Coinciden en
    referencia   REF-1001
    marca        Del Valle

Se diferencian en
    descripcion    Jugo del Valle Naranja   frente a   Jugo del Valle Naranja 600
    presentacion   355 ml   frente a   600 ml

Por qué se descartó
    · La medida de «presentacion» no es la misma: 355 ML contra 600 ML.

Misma referencia, misma marca, casi la misma descripción — y aun así no son el mismo artículo. Ese es exactamente el caso que una herramienta de similitud textual resuelve mal.

Con -t se obtiene el detalle técnico: cada comparador, su umbral y los puntos que aportó.


Tres preguntas, un mismo motor

Detectar duplicados es una de las tres cosas que se le piden a un sistema como este. Las otras dos son cruzar dos archivos y buscar dentro de un catálogo, y en Cotejo no son otro programa: son el mismo perfil, las mismas reglas y los mismos vetos, con otro modo.

Pregunta Comando Respuesta
¿Qué filas de este archivo son la misma cosa? cotejo run Grupos: un original y sus duplicados
¿A qué corresponde cada fila de este archivo en aquel otro? cotejo link Por cada fila, sus candidatos del otro archivo
¿Esto que voy a dar de alta ya existe? cotejo search Por cada consulta, lo que se encontró — o nada
cotejo search retail_inventory examples/lista_proveedor.csv examples/abarrotes.csv --show 5
consulta P-02  "REF-1001"  "Jugo del Valle Naranja 600"  "Del Valle"
  1. 100%  3  "REF-1001"  "Jugo del Valle Naranja 600"  "Del Valle"
        duplicado_seguro · same_canonical_code, reference_similarity, description_similarity, same_brand

consulta P-04  "REF-2010"  "Cafe soluble Legal 200 g"  "Legal"
     sin coincidencias en el catálogo

El artículo del proveedor de 600 ml encontró solo el de 600 ml del catálogo, aunque el de 355 ml comparte referencia, marca y casi la descripción: el veto measure_differs trabaja igual al buscar que al limpiar. Y el café no está — que es justo lo que se fue a averiguar, así que aparece en pantalla y en el informe con su propio renglón.

Los detalles, en docs/buscar-en-otro-catalogo.md.


Los vetos

Un veto descarta un par sin importar cuánto puntaje haya acumulado. Aquí es donde vive el conocimiento del dominio, y por eso son configuración y no código.

Veto Descarta cuando Ejemplo
exclusive_numeric_token Cada lado tiene un número que el otro no 397374 contra 397373
segment_numeric_divergence Misma estructura, un tramo numérico distinto N230-026-04 contra N230-026-13
sequential_suffix Solo cambia un dígito final T1 contra T10
attribute_differs Un atributo existe en ambos y no coincide dos marcas distintas
measure_differs Ambos declaran medida y no es la misma 355 ML contra 600 ML
unit_differs Misma cifra, otra unidad 500 G contra 500 KG
blocked_words Alguno trae una palabra que invalida SEGÚN DIBUJO
protected_tokens Un token del vocabulario propio no coincide paquete de 6 contra de 8
partition_mismatch Los registros no deben cruzarse dos tiendas distintas
regex_veto Lo que tu dominio necesite JR contra SR

Todos siguen el mismo criterio de prudencia: solo actúan si ambos registros traen el dato. Si a uno le falta, no hay divergencia que declarar, y descartar por falta de información escondería duplicados reales.

Y cuando un veto se equivoca, un rescate lo neutraliza:

rescues:
  - when_signals: [same_canonical_code]
    neutralizes: [exclusive_numeric_token]
    reason: >
      Los códigos canónicos coinciden: la divergencia numérica viene de que cada
      registro capturó el mismo código con separadores distintos.

Comandos

cotejo rules                                  # catálogo de reglas disponibles
cotejo profiles                               # perfiles incluidos
cotejo validate mi_perfil.yaml                # valida y resume un perfil
cotejo explain PERFIL -c COLUMNA -v "VALOR"   # normalización paso a paso
cotejo preview PERFIL datos.csv -n 5          # cómo queda cada registro
cotejo prepare PERFIL datos.csv -o salida.csv # normaliza y extrae rasgos
cotejo run     PERFIL datos.csv               # detecta duplicados de punta a punta
cotejo link    PERFIL propio.csv otro.csv     # cruza dos archivos
cotejo search  PERFIL consultas.csv base.csv  # ¿ya existe esto en el catálogo?
cotejo pair    PERFIL datos.csv A B           # por qué dos registros son o no el mismo
cotejo gui     [datos.csv]                    # abre la aplicación de escritorio
cotejo sample  DOMINIO -n 2000                # genera un catálogo con la respuesta conocida
cotejo evaluate PERFIL datos.csv              # mide contra esa respuesta
cotejo project datos.cotejo                   # qué se ha revisado y qué conviene ajustar
cotejo doctor                                 # comprueba que el entorno esté completo

run produce tres archivos: los pares con su explicación desglosada (auditoría), los grupos con el original arriba y los duplicados debajo con su porcentaje (revisión), y un resumen de la ejecución. link y search producen los mismos, cambiando los grupos por una hoja de coincidencias con una fila por consulta —incluidas las que no encontraron nada.


¿Y funciona? Se puede medir

La pregunta incómoda de cualquier herramienta de este tipo es cómo saber si acierta. Cotejo trae generadores de catálogos sintéticos que siembran la respuesta: duplicados con las deformaciones de captura que aparecen en datos reales, y trampas que se les parecen muchísimo y no son el mismo elemento.

cotejo sample retail -n 2000 -o prueba.csv
cotejo evaluate retail_inventory prueba.csv
  duplicados sembrados      172
    encontrados             172  (100%)
    perdidos                0
  trampas sembradas         170
    cayó en                 0  (0%)
  precisión                 100%
  parejas agrupadas         172
    de ellas, no sembradas  0

Las dos cifras hay que leerlas juntas. Recuperación dice cuántos duplicados encontró; trampas dice en cuántas parejas se equivocó. Un sistema que dice que sí a todo tiene recuperación perfecta y cae en todas las trampas — por eso el generador siembra las dos cosas.

Sobre 2 000 registros de cada dominio, con los perfiles incluidos:

Perfil Recuperación Trampas caídas Precisión
retail_inventory 100 % 0 % 100 %
industrial_catalog 100 % 0 % 100 %
crm_contacts 100 % 0 % 100 %
bibliography 100 % 0 % 100 %

Con honestidad: estas cifras dicen que el sistema entiende las deformaciones que el generador produce, y generador y motor comparten supuestos. Un 100 % aquí no es un 100 % sobre un catálogo real. Lo que sí garantiza es que un cambio en las reglas no rompa en silencio algo que antes funcionaba — y eso, en un sistema de heurísticas acumuladas, es exactamente el riesgo.

La medición sobre datos reales es otra: la pantalla de revisión calcula el porcentaje de aciertos a partir de lo que una persona confirma o rechaza.

Veinte mil registros tardan unos diez segundos en una laptop común, y el bloqueo evita el 99.9 % de las comparaciones posibles.


Aprende de lo que revisas

Cada vez que confirmas o rechazas un par dejas un dato etiquetado, y eso se guarda en el proyecto —un archivo .cotejo que aparece junto a tus datos, sin que haya que acordarse de guardar nada.

Mejora

Con unas cuantas decisiones el sistema puede responder dos preguntas que antes solo tenían respuesta por intuición.

¿Dónde conviene poner el corte? Con los puntajes y los veredictos a la vista se calcula qué precisión y qué cobertura daría cada umbral, en lugar de moverlo a ojo.

¿Qué regla se equivoca? Un veto que descarta pares que después confirmas está mal calibrado para estos datos. Una señal que se activa sobre todo en los pares que rechazas está sumando puntos donde no hay evidencia.

Para que un veto mal calibrado se pueda descubrir, la pantalla de revisión deja ver lo que cada veto descartó. Sin eso, un veto demasiado agresivo trabaja en silencio y nadie se entera nunca.

Nada se aplica solo: son observaciones sobre lo que ya decidiste, y el botón lo pulsas tú.

Desde la consola, lo mismo:

cotejo project datos.cotejo

Y el trabajo hecho no se repite

El mismo archivo guarda los registros ya normalizados. Al volver a abrir el catálogo mañana, si ni los datos ni la normalización cambiaron, ese trabajo no se rehace:

Sobre 20 000 filas Sin caché Con caché
Preparar los registros 2.6 s 0.6 s

La huella que decide si sigue valiendo se calcula sobre el contenido del archivo y sobre la parte del perfil que interviene en preparar registros. Mover un umbral —que es lo que uno hace todo el rato al calibrar— no invalida nada; cambiar una regla de normalización, sí. Si algo no cuadra, la caché sencillamente no se encuentra y se recalcula: nunca se contesta con algo viejo.

cotejo project datos.cotejo --forget-cache    # por si quieres empezar de cero

Perfiles incluidos

Cuatro dominios muy distintos resueltos con el mismo motor. Esa es la prueba de que la arquitectura es genérica de verdad y no solo de nombre.

Perfil Dominio Qué demuestra Datos
industrial_catalog Refacciones de planta Códigos compuestos, identificadores fuertes, dimensiones, números de pieza industrial.csv
retail_inventory Abarrotes La referencia se repite entre presentaciones: el tamaño es el discriminador retail.csv
crm_contacts Clientes y proveedores Normalización conservadora, la eñe se conserva, bloqueo fonético contacts.csv
bibliography Libros ISBN canónico y título ponderado por rareza de término bibliography.csv

Cada archivo de ejemplo viene con su _verdad.csv al lado, así que cualquiera puede reproducir las cifras de arriba.


Reglas propias, sin bifurcar el paquete

El conocimiento específico de un dominio vive en tu proyecto, no en el nuestro. Escribe un archivo suelto:

from cotejo import veto, VetoResult

@veto("talla_diferente")
def talla_diferente(left, right, column="talla"):
    """Descarta el par cuando ambos registros traen talla y no coinciden."""
    a, b = left.norm(column), right.norm(column)
    if a and b and a != b:
        return VetoResult(True, f"talla {a} distinta de {b}")
    return VetoResult(False)

Y decláralo en el perfil:

plugins: [reglas_propias.py]
vetoes:
  - { rule: talla_diferente, params: { column: talla } }

La regla queda en el mismo catálogo que las incluidas, con su documentación y sus parámetros. Ejemplo completo en examples/.


Uso como librería

from cotejo import Engine, load_profile, read_table, render_pair

profile = load_profile("mi_perfil.yaml")
result = Engine(profile).run(read_table("catalogo.csv"))

print("\n".join(result.stats.lines()))

for group in result.groups:
    print(group.canonical, "->", group.members)

for pair in result.pairs[:5]:
    print(render_pair(pair, result.by_key))

El motor informa su avance llamando a una función que se le pasa, sin saber nada de la interfaz que la implemente:

Engine(profile, progress=lambda fase, hechos, total: print(fase, hechos, total))

Documentación

La segunda no se escribe a mano: tools/build_docs.py la deriva de los docstrings y de los casos golden, y la integración continua falla si queda desactualizada. Cada ejemplo que aparece ahí está verificado por la suite.


Pruebas

pytest -q

La interfaz se prueba entera sin monitor: Qt dibuja en un búfer en memoria, así que construir la ventana, cargar datos, ejecutar el motor y comprobar el resultado funciona igual en una laptop que en integración continua.

Los casos golden son el corazón de la suite: cada uno documenta por qué su resultado es el correcto. Hay tres conjuntos —normalización, extracción y vetos— y la suite comprueba que ningún veto se quede sin al menos un caso que no deba dispararlo, porque un veto probado solo cuando acierta no está realmente probado.

- rule: merge_alphanumeric_segments
  cases:
    - { input: "39-D-1301",   expected: "39D1301",     why: "todos los tramos son código: es un identificador partido" }
    - { input: "30-MM",       expected: "30-MM",       why: "MM son dos letras sin dígito: es una unidad, no código" }
    - { input: "MOTOR-BOMBA", expected: "MOTOR-BOMBA", why: "palabra compuesta real: no se toca" }

Cuando un caso falla, el mensaje muestra esa razón: quien lo repare sabe qué se estaba protegiendo.


Hoja de ruta

Fase Contenido Estado
1 Núcleo, catálogo de reglas, perfiles, normalización con explain, extractores listo
2 Bloqueo, comparadores, vetos, puntaje, agrupamiento, CLI de punta a punta listo
3 Interfaz gráfica (PySide6) con laboratorio de reglas listo
4 Catálogos de prueba con la respuesta conocida, medición y documentación listo
5 Revisión persistida, métricas acumuladas y ajuste asistido de umbrales listo
6 Los tres modos: limpiar un catálogo, cruzar dos archivos y buscar dentro de uno listo
7 Caché de registros normalizados, ejecutable de Windows y publicación en PyPI listo

Licencia

MIT — ver LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cotejo-0.7.0.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cotejo-0.7.0-py3-none-any.whl (177.7 kB view details)

Uploaded Python 3

File details

Details for the file cotejo-0.7.0.tar.gz.

File metadata

  • Download URL: cotejo-0.7.0.tar.gz
  • Upload date:
  • Size: 1.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cotejo-0.7.0.tar.gz
Algorithm Hash digest
SHA256 cfffcd5efc231ad16697e29846177d0292466ad3a9465ab6f821cab40352891a
MD5 3dc90f19e1927ae24587f5b0b2fa9365
BLAKE2b-256 c30c19f97d14e0dd4a7801b6692d5f9a3924d829902ced8ffa460da8e7a3c086

See more details on using hashes here.

Provenance

The following attestation bundles were made for cotejo-0.7.0.tar.gz:

Publisher: release.yml on gaabinrr/cotejo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cotejo-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: cotejo-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 177.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cotejo-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f5e2d3be5ba015774f50bde0b0514e9e75b4a6d7bf96b58d68827bd5acca5686
MD5 96a62733cf9c05c3ed81b13af9ff0450
BLAKE2b-256 94a66c9c1b8251089495254a9e7889a2d6a2b58fce064b43a1be326f28b77ff0

See more details on using hashes here.

Provenance

The following attestation bundles were made for cotejo-0.7.0-py3-none-any.whl:

Publisher: release.yml on gaabinrr/cotejo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page