Metadata-Version: 2.4
Name: fastcorex
Version: 0.2.0
Summary: Extension en C para acelerar loops y estructuras de datos comunes
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# fastcorex

Extensión en C para acelerar loops y estructuras de datos comunes en Python. No es un reemplazo de NumPy ni Pandas — es una capa delgada que elimina las partes tediosas y repetitivas (agrupar, deduplicar, aplanar, contar, filtrar, particionar, fusionar dicts, generar slugs, acceso anidado seguro) que normalmente se reescriben a mano en cada proyecto.

## Instalación

```
pip install fastcorex
```

Requiere Python 3.9 o superior y un compilador de C (se compila al instalar, como cualquier extensión nativa).

## Uso rápido

```python
import fastcorex as fx

fx.fast_sum([1, 2, 3])                      # 6.0
fx.count_freq(["a", "b", "a"])              # {'a': 2, 'b': 1}
fx.unique([3, 1, 2, 1, 3])                  # [3, 1, 2]
fx.filter_gt([1, 5, 10, 3], 4.0)            # [5.0, 10.0]
fx.groupby(lista_de_dicts, "categoria")     # dict agrupado
fx.flatten([1, [2, [3, 4]], 5])             # [1, 2, 3, 4, 5]
fx.unique_by(lista_de_dicts, "id")          # FastList dedup por campo (encadenable)
fx.chunk([1, 2, 3, 4, 5], 2)                # [[1, 2], [3, 4], [5]]
fx.safe_get(d, "a.b.c", default=None)       # acceso anidado sin try/except
fx.clamp(15.0, 0.0, 10.0)                   # 10.0
fx.pick({"a": 1, "b": 2, "c": 3}, ["a"])    # {'a': 1}
fx.omit({"a": 1, "b": 2, "c": 3}, ["a"])    # {'b': 2, 'c': 3}
fx.deep_merge(config_base, config_local)    # dict fusionado recursivamente
fx.slugify("Título con Ñandú")              # "titulo-con-nandu"
fx.partition(numeros, lambda n: n > 0)      # (positivos, no_positivos)
fx.dedupe_consecutive([1, 1, 2, 2, 1])      # [1, 2, 1]
fx.ensure_list(5)                           # [5]
fx.first_or_default(lista, predicado)       # primer match o default
```

## Documentación de funciones

### Funciones de la versión 0.1.x

**fast_sum(lista)** — Suma todos los elementos numéricos de una lista.
```python
fx.fast_sum([1, 2, 3, 4.5])  # 10.5
```

**count_freq(lista)** — Cuenta cuántas veces aparece cada elemento. Reemplaza un loop de 4 líneas con `dict.get()` por una sola llamada.
```python
fx.count_freq(["a", "b", "a", "c", "b", "a"])  # {'a': 3, 'b': 2, 'c': 1}
```

**unique(lista)** — Elimina duplicados manteniendo el orden original. Reemplaza el patrón de `set()` + loop + `append`.
```python
fx.unique([3, 1, 2, 1, 3, 4])  # [3, 1, 2, 4]
```

**filter_gt(lista, umbral)** — Devuelve solo los elementos mayores al umbral dado.
```python
fx.filter_gt([1, 5, 10, 3, 8], 4.0)  # [5.0, 10.0, 8.0]
```

**groupby(lista_de_dicts, clave)** — Agrupa una lista de diccionarios según el valor de una clave. Devuelve un dict normal. Reemplaza el patrón de dict + `setdefault` manual.
```python
fx.groupby(ventas, "categoria")  # {'ropa': [...], 'comida': [...]}
```

**flatten(lista_anidada)** — Aplana listas anidadas de cualquier profundidad. Reemplaza una función recursiva escrita a mano.
```python
fx.flatten([1, [2, 3, [4, [5, 6]], 7], 8])  # [1, 2, 3, 4, 5, 6, 7, 8]
```

**unique_by(lista_de_dicts, clave)** — Deduplica diccionarios según el valor de un campo específico, no el objeto completo. Devuelve un `FastList` (ver sección de encadenamiento).
```python
fx.unique_by(ventas, "id")  # FastList sin ids repetidos
```

**chunk(lista, tamaño)** — Parte una lista en sublistas de tamaño fijo; el último chunk puede quedar más corto. Reemplaza el slicing manual con `range(0, len(lista), tamaño)`.
```python
fx.chunk([1, 2, 3, 4, 5, 6, 7], 3)  # [[1, 2, 3], [4, 5, 6], [7]]
```

**safe_get(dict, "a.b.c", default=None)** — Acceso anidado seguro a diccionarios usando un path con puntos. Reemplaza el `try/except (KeyError, TypeError)` que normalmente envuelve un acceso encadenado. `default` puede pasarse posicional o como keyword.
```python
fx.safe_get({"a": {"b": {"c": 42}}}, "a.b.c")            # 42
fx.safe_get({"a": {"b": {}}}, "a.b.c", default="N/A")    # "N/A"
```
Un path vacío, o con un segmento vacío (`"a..b"`, `".a"`, `"a."`), lanza `ValueError` en vez de devolver silenciosamente el default — un path malformado suele ser un bug en quien llama, no un caso de "no encontrado".

**clamp(valor, minimo, maximo)** — Acota un número al rango `[minimo, maximo]`. Reemplaza `max(minimo, min(valor, maximo))` o un if/elif/else.
```python
fx.clamp(15.0, 0.0, 10.0)  # 10.0
fx.clamp(-5.0, 0.0, 10.0)  # 0.0
```

### Funciones nuevas (0.2.0)

**pick(dict, claves)** — Devuelve un nuevo dict con solo las claves indicadas que existan en el original; las ausentes se ignoran sin error. Reemplaza `{k: d[k] for k in claves if k in d}`.
```python
fx.pick({"nombre": "Ana", "edad": 30, "email": "a@x.com"}, ["nombre", "email"])
# {'nombre': 'Ana', 'email': 'a@x.com'}
```
Útil para serializar solo un subconjunto de campos hacia una API o un log, sin exponer el resto del objeto.

**omit(dict, claves)** — Lo inverso de `pick`: devuelve un nuevo dict sin las claves indicadas. Reemplaza `{k: v for k, v in d.items() if k not in claves}`.
```python
fx.omit({"nombre": "Ana", "password": "secreta", "edad": 30}, ["password"])
# {'nombre': 'Ana', 'edad': 30}
```
Útil para el caso contrario a `pick`: quitar uno o dos campos sensibles de un dict grande sin tener que enumerar todos los que sí quedan.

**deep_merge(base, override)** — Fusiona `override` sobre `base` recursivamente: cuando ambos tienen un dict en la misma clave, se fusionan sus contenidos en vez de que uno reemplace al otro; en cualquier otro caso, `override` gana. Ninguno de los dos argumentos originales se modifica.
```python
config_base = {"db": {"host": "localhost", "port": 5432}, "debug": False}
config_local = {"db": {"port": 5433}}
fx.deep_merge(config_base, config_local)
# {'db': {'host': 'localhost', 'port': 5433}, 'debug': False}
```
Pensado para el caso típico de fusionar un archivo de configuración base con overrides de entorno o de usuario, sin perder las claves del base que el override no toca.

**slugify(texto)** — Normaliza un string a minúsculas, sin acentos (cubre á é í ó ú ü ñ ç y sus mayúsculas), con guiones en vez de espacios o símbolos, sin guiones duplicados ni al inicio/final. Reemplaza normalizar texto a mano con `.lower()` + reemplazos de acentos + regex de limpieza.
```python
fx.slugify("Título de Sección: ¡Importante!")  # "titulo-de-seccion-importante"
fx.slugify("  Café & Té  ")                     # "cafe-te"
```
No es un normalizador Unicode completo (para eso está `unicodedata` en la librería estándar); cubre el caso real de generar slugs legibles para URLs o nombres de archivo a partir de texto en español y similares.

**partition(lista, predicado)** — Recorre la lista una sola vez y la separa en `(cumplen, no_cumplen)` según `predicado(item)`, en vez de hacer dos pasadas (`filter` + `filter` con la condición negada). Devuelve una tupla de dos `FastList`.
```python
pares, impares = fx.partition(range(10), lambda n: n % 2 == 0)
# ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
```

**dedupe_consecutive(lista)** — Colapsa elementos repetidos que aparecen uno justo después del otro. A diferencia de `unique()`, no deduplica globalmente: `dedupe_consecutive([1, 2, 1])` deja `[1, 2, 1]` intacto, porque el segundo `1` no es consecutivo con el primero. Pensado para logs, lecturas de sensores o streams donde importa el valor inmediatamente anterior, no todo el historial.
```python
fx.dedupe_consecutive([1, 1, 2, 2, 2, 1, 3, 3])  # [1, 2, 1, 3]
fx.dedupe_consecutive(["OK", "OK", "ERROR", "OK"])  # ['OK', 'ERROR', 'OK']
```

### Utilidades en Python puro (0.2.0)

Estas dos funciones se implementaron directamente en Python, no en C, porque su costo ya es mínimo en el intérprete (una rama de `isinstance` en un caso, delegar a `next()`/`iter()` en el otro) y una extensión de C no traería ninguna ganancia medible — solo complejidad extra de mantenimiento.

**ensure_list(valor)** — Envuelve `valor` en una lista si no es ya una lista o tupla. Reemplaza el patrón de normalizar un parámetro que a veces llega como valor único y a veces como colección.
```python
fx.ensure_list(5)          # [5]
fx.ensure_list([1, 2, 3])  # [1, 2, 3]
fx.ensure_list(None)       # [None]
```

**first_or_default(iterable, predicado=None, default=None)** — Devuelve el primer elemento que cumple `predicado`, o `default` si ninguno cumple (o el iterable está vacío), sin lanzar `StopIteration`. Funciona con cualquier iterable, incluidos generadores, y se detiene apenas encuentra la primera coincidencia.
```python
fx.first_or_default([1, 2, 3, 4], lambda x: x > 2)  # 3
fx.first_or_default([], default="vacío")             # "vacío"
```

## FastList: encadenamiento de métodos

`unique_by()`, `partition()` y algunas otras funciones devuelven un `FastList`: un subtipo de `list` que se comporta como una lista normal (indexable, iterable, con `len()`, slicing, etc.) pero además expone métodos propios para seguir encadenando sin volver a pasar por funciones sueltas del módulo.

Métodos disponibles en `FastList`:

- **`.groupby(clave)`** — igual que `fx.groupby()`, pero devuelve otro `FastList` (de pares `[clave, sublista]`) en vez de un dict, para poder seguir encadenando.
- **`.sum(campo=None)`** — sin argumento, suma los elementos como números. Con argumento, asume que el `FastList` viene de un `.groupby()` y devuelve un dict `{clave: suma_del_campo}`.
- **`.count()`** — asume que el `FastList` viene de un `.groupby()` y devuelve un dict `{clave: cantidad}`.
- **`.filter(predicado)`** — devuelve un `FastList` con los elementos donde `predicado(item)` es verdadero.
- **`.map(función)`** — devuelve un `FastList` con `función(item)` aplicada a cada elemento.
- **`.unique_by(clave)`** — igual que `fx.unique_by()`, encadenable.
- **`.flatten()`** — igual que `fx.flatten()`, encadenable.
- **`.chunk(tamaño)`** — igual que `fx.chunk()`, encadenable.
- **`.partition(predicado)`** — igual que `fx.partition()`, devuelve una tupla de dos `FastList`.
- **`.dedupe_consecutive()`** — igual que `fx.dedupe_consecutive()`, encadenable.

```python
ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")
# {'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}

conteos = ventas_unicas.groupby("categoria").count()
# {'ropa': 2, 'comida': 1, 'tech': 1}
```

También se puede construir un `FastList` directamente para tener esta API encadenada desde el principio, combinando cualquier secuencia de métodos:

```python
resultado = (
    fx.FastList(eventos)
    .filter(lambda e: e["nivel"] == "ERROR")
    .unique_by("id")
    .groupby("servicio")
    .count()
)
```

```python
# Limpieza de datos: normalizar, quitar vacíos, colapsar repetidos consecutivos
resultado = (
    fx.FastList(lecturas_sensor)
    .map(str.lower)
    .filter(lambda s: s != "")
    .dedupe_consecutive()
)
```

Nota: `.sum(campo)` y `.count()` esperan específicamente el formato que produce `.groupby()` (una lista de pares `[clave, sublista]`); si se les pasa otra cosa, lanzan `TypeError` con un mensaje explicando qué esperaban.

## Ejemplo real combinando varias funciones

Sin fastcorex (19 líneas): un loop para deduplicar por id con `set()` + seen, otro loop para agrupar por categoría con dict + `setdefault`, y un tercer loop anidado para sumar montos por grupo.

Con fastcorex, dos formas equivalentes:

Con funciones sueltas (4 líneas):
```python
ventas_unicas = fx.unique_by(ventas, "id")
grupos = fx.groupby(ventas_unicas, "categoria")
totales = {cat: fx.fast_sum([i["monto"] for i in items]) for cat, items in grupos.items()}
```

Con encadenamiento de FastList (2 líneas):
```python
ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")
```

Ambas versiones dan el mismo resultado: `{'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}`

## Resultados de benchmark

Medido con listas de 200,000 y 2,000,000 de elementos, mejor tiempo de 5 corridas (ver `benchmark.py` en el repositorio para reproducirlo). Datos generados aleatoriamente por función (números, strings de un alfabeto de 50 valores, dicts con ~50% de ids duplicados para forzar deduplicación real en `unique_by`, texto con acentos para `slugify`, corridas de repetidos para `dedupe_consecutive`).

**Con 200,000 elementos:** fast_sum 2.6x más rápido, count_freq 1.5x, unique 1.7x, filter_gt 2.1x, groupby 2.4x, flatten 11.0x, unique_by 1.2x, chunk 1.3x, safe_get 1.1x, clamp 1.9x, pick 1.8x, omit 5.8x, deep_merge 13.9x, slugify 34.6x, dedupe_consecutive 2.2x, pipeline encadenado completo (unique_by → groupby → sum) 1.7x. **partition: 0.53x — más lento que Python puro.**

**Con 2,000,000 elementos:** fast_sum 1.8x, count_freq 1.4x, unique 1.5x, filter_gt 1.6x, groupby 1.9x, flatten 8.4x, unique_by 1.3x, chunk 1.1x, safe_get 1.1x, clamp 2.0x, pick 1.6x, omit 5.9x, deep_merge 14.2x, slugify 34.9x, dedupe_consecutive 1.9x, pipeline encadenado 1.7x. **partition: 0.56x — más lento que Python puro.**

`deep_merge` y `slugify` tienen las mejores ganancias entre las funciones nuevas porque hacen trabajo genuinamente pesado en C sin depender de callbacks a Python: `deep_merge` evita el `copy.deepcopy` completo de Python (que recorre y copia recursivamente *todo* el árbol) y solo copia los dicts que realmente cambian; `slugify` reemplaza un `.translate()` + comprehension + limpieza de guiones por un único paso sobre los bytes, sin crear objetos string intermedios por cada substitución. `omit` gana bien porque evita construir un `dict.items()` view y el filtrado elemento por elemento en bytecode de Python. `pick` gana menos que `omit` porque su costo ya está dominado por las búsquedas en el dict origen (`PyDict_GetItemWithError`), que en ambas versiones usan la misma implementación en C de CPython por debajo.

**partition es la excepción real, igual que `safe_get` lo era en 0.1.1 antes de esta versión: es consistentemente ~45-47% más lento que el equivalente en Python.** La razón no es un descuido de implementación sino estructural: `partition` invoca el predicado de Python una vez por elemento vía `PyObject_CallFunctionObjArgs`, y ese cruce Python→C→Python en cada iteración cuesta más de lo que se ahorra teniendo el loop externo en C. Se confirmó directamente: usando una función *builtin* de C como predicado (`bool`) en vez de una `lambda` de Python, `partition` iguala la velocidad de `filter_gt` (que no usa callback); el cuello de botella está en el lado Python del callback, no en el loop de C. Lo mismo aplica a `.filter()` y `.map()` de `FastList`, y por eso el pipeline nuevo `filter → map → dedupe_consecutive` también da un ratio menor a 1x (0.73x-0.74x) cuando se lo compara contra su equivalente en Python puro: dos de sus tres pasos pagan ese mismo costo de callback. Si la velocidad de estos métodos importa en un caso puntual, conviene o bien usar `filter_gt`/`dedupe_consecutive` solos (que no necesitan callback), o aceptar que la ganancia real de `partition`/`filter`/`map` no es de velocidad sino de expresividad y de mantener todo en una sola cadena legible.

flatten sigue teniendo una de las mayores ganancias porque en Python puro depende de recursión con overhead de llamadas a función, que en C es casi gratis. groupby gana bien porque evita el overhead del bytecode en el loop principal. Las funciones que ya dependen de dict/set de Python (count_freq, unique, unique_by, fast_sum) ganan menos, porque esas estructuras ya están optimizadas en C por debajo del intérprete — el beneficio ahí es sobre todo la reducción de líneas de código, no la velocidad. chunk gana relativamente poco porque el slicing de listas en Python ya es una operación en C bien optimizada.

**safe_get ya no es una excepción negativa.** En la versión 0.1.1 era ~25% más lento que Python puro porque hacía `strdup()` + `strtok()` sobre el path en cada llamada (con su malloc/free correspondiente) y usaba `PyDict_GetItemString`, que construye un objeto string de Python por cada segmento. La implementación 0.2.0 elimina el `strdup` inicial y recorre el path original con punteros crudos, sin duplicarlo; sigue creando un `PyUnicode` por segmento porque `PyDict_GetItem` necesita un objeto Python como clave, pero ya no hay malloc/free adicional para el path completo. El resultado pasó de 0.75x-0.77x (más lento) a un consistente ~1.1x-1.4x más rápido que Python puro, dependiendo del tamaño del path y de cuánto se repita la llamada.

## Licencia

MIT License. Ver el archivo LICENSE para el texto completo.

## Estado del proyecto

Versión 0.2.0. Cubre patrones comunes de listas, diccionarios y strings (agrupar, deduplicar, aplanar, contar, filtrar, particionar, fusionar, generar slugs, acceso anidado, acotar rangos) más un tipo `FastList` que permite encadenar diez operaciones distintas (`groupby`, `sum`, `count`, `filter`, `map`, `unique_by`, `flatten`, `chunk`, `partition`, `dedupe_consecutive`) sin pasar por dicts intermedios. No pretende reemplazar NumPy para cómputo numérico ni Pandas para análisis de datos tabulares — está enfocado en el trabajo genérico con listas, dicts y strings que esas librerías no cubren directamente.

Cubierto por pruebas automatizadas (`tests/`, ejecutables con `python -m unittest discover -s tests` o con `pytest tests/`): funciones originales de 0.1.1 (regresión), las seis utilidades nuevas de 0.2.0, y encadenamientos largos de `FastList` combinando `filter`, `map`, `unique_by`, `groupby`, `dedupe_consecutive` y `flatten` en una sola expresión.
