#!/usr/bin/env python3 """Indexa las 'awesome lists' clonadas en ~/COFRE/CODERS como catalogo del RAG. ./awesome.py di cuantas entradas saldrian, por fuente ./awesome.py --volcado imprime unas cuantas entradas de muestra Las awesome lists son catalogos curados: "herramienta -> que hace + categoria". No son comandos ni metodologia; son el "que existe para hacer X". Aqui se parsean —markdown y reStructuredText— a entradas limpias, agrupadas por categoria, para que busca.py las recupere igual que todo lo demas. Cada fuente lleva su LICENCIA. Solo las redistribuibles (CC0, CC-BY-SA) entran en el indice PUBLICO que se sube al repo; el resto se indexa solo en la maquina del usuario (indexado personal = uso legitimo). indexa.py respeta esa marca. """ import argparse import os import re import sys CODERS = os.path.expanduser("~/COFRE/CODERS") MAX_BYTES = 4 * 1024 * 1024 # una lista no pasa de esto; guarda la swap chica MAX_FRAGMENTO = 1100 # como el resto del indice # Secciones que no son catalogo, se saltan vengan como vengan. RUIDO = re.compile(r"^(table of contents|contents|contributing|license|" r"list of licenses|anti.?features|external links|related|" r"footnotes|backers|sponsors|thanks|credits|acknowledge|" r"see also|index|legend|notes?)\b", re.I) # (carpeta en CODERS, fichero, formato, etiqueta de fuente, perfil, licencia, publico) CATALOGOS = [ ("awesome-selfhosted", "README.md", "md", "awesome-selfhosted", "AUTOHOSPEDADO", "CC-BY-SA-3.0", True), ("awesome", "readme.md", "md", "awesome (sindresorhus)", "LISTAS", "CC0-1.0", True), ("awesome-hacking", os.path.join("_pages", "index.rst"), "rst", "awesome-hacking (jekil)", "PENTESTERS", "", False), ] # markdown: - [Nombre](http...) - Descripcion ... `Lic` `Lenguaje` IT_MD = re.compile(r"^\s*[-*]\s+\[([^\]]+)\]\((https?://[^)]+)\)\s*[-–—:]?\s*(.*)$") # rst: - `Nombre `_ - Descripcion IT_RST = re.compile(r"^\s*[-*]\s+`([^<]+?)\s*<([^>]+)>`_+\s*[-–—:]?\s*(.*)$") # parentesis de enlaces auxiliares: ([Source Code](...), [Demo](...)) PARENS = re.compile(r"\s*\((?:\[[^\]]+\]\([^)]*\)[,\s]*)+\)") TAGS = re.compile(r"`([^`]+)`") # `MIT` `Docker` -> MIT, Docker def _limpia_desc(desc): desc = PARENS.sub("", desc) # fuera ([Source Code]...) etc. tags = TAGS.findall(desc) # licencia/lenguaje entre backticks desc = TAGS.sub("", desc).strip(" .-–—") if tags: # se conservan como pista (lenguaje, lic) desc = f"{desc} [{', '.join(t.strip() for t in tags)}]" return re.sub(r"\s+", " ", desc).strip() def _lee(ruta): if not os.path.exists(ruta) or os.path.getsize(ruta) > MAX_BYTES: return None with open(ruta, encoding="utf-8", errors="ignore") as f: return f.read() def _secciones_md(texto): """Cede (categoria, [(nombre, desc)...]) por cada seccion ## / ###.""" categoria, items = "", [] for linea in texto.splitlines(): h = re.match(r"^#{2,4}\s+(.+?)\s*$", linea) if h: if items: yield categoria, items categoria, items = re.sub(r"[#*`\[\]]", "", h.group(1)).strip(), [] continue m = IT_MD.match(linea) if m and m.group(2).startswith("http"): # ignora enlaces de TOC (#ancla) desc = _limpia_desc(m.group(3)) items.append((m.group(1).strip(), desc)) if items: yield categoria, items def _secciones_rst(texto): """En RST el titulo es una linea con otra de === o --- debajo.""" lineas = texto.splitlines() categoria, items = "", [] for i, linea in enumerate(lineas): sig = lineas[i + 1] if i + 1 < len(lineas) else "" if linea.strip() and re.match(r"^[=~^\"'`#*+.-]{3,}\s*$", sig) \ and len(sig.strip()) >= len(linea.strip()) - 2 \ and not linea.startswith(("-", "*", " ")): if items: yield categoria, items categoria, items = linea.strip(), [] continue m = IT_RST.match(linea) if m: items.append((m.group(1).strip(), _limpia_desc(m.group(3)))) if items: yield categoria, items def _fragmenta(fuente, perfil, licencia, publico, secciones): """Agrupa los items de cada categoria en fragmentos con tope de tamaño.""" for categoria, items in secciones: if not categoria or RUIDO.match(categoria): continue cabecera = f"Catalogo {fuente} / {categoria}:" buffer, n = cabecera, 0 for nombre, desc in items: linea = f" {nombre}" + (f" — {desc}." if desc else ".") if len(buffer) + len(linea) > MAX_FRAGMENTO and n: yield _entrada(fuente, perfil, licencia, publico, categoria, buffer) buffer, n = cabecera, 0 buffer += linea n += 1 if n: yield _entrada(fuente, perfil, licencia, publico, categoria, buffer) def _entrada(fuente, perfil, licencia, publico, categoria, texto): return { "tipo": "catalogo", "herramienta": categoria, "perfil": perfil, "fuente": fuente, "licencia": licencia, "publico": publico, "tema": categoria.lower(), "fichero": f"awesome: {fuente} / {categoria}", "texto": texto, } def entradas(solo_publico=False): """Todas las entradas de catalogo de las awesome lists disponibles.""" for carpeta, fichero, formato, fuente, perfil, lic, publico in CATALOGOS: if solo_publico and not publico: continue texto = _lee(os.path.join(CODERS, carpeta, fichero)) if not texto: continue secciones = _secciones_rst(texto) if formato == "rst" else _secciones_md(texto) yield from _fragmenta(fuente, perfil, lic, publico, secciones) def main(): p = argparse.ArgumentParser() p.add_argument("--volcado", action="store_true", help="imprime entradas de muestra") a = p.parse_args() porfuente = {} muestras = [] for e in entradas(): porfuente[e["fuente"]] = porfuente.get(e["fuente"], 0) + 1 if len(muestras) < 6: muestras.append(e) total = sum(porfuente.values()) for fuente, n in porfuente.items(): pub = next(c[5] or "sin licencia" for c in CATALOGOS if c[3] == fuente) print(f" {fuente:26s} {n:5d} fragmentos [{pub}]") print(f" total: {total} fragmentos de catalogo") if a.volcado: print("\n--- muestras ---") for e in muestras: print(f"\n[{e['fuente']} / {e['herramienta']}]\n {e['texto'][:300]}") return 0 if __name__ == "__main__": sys.exit(main())