Metadata-Version: 2.4
Name: compickle
Version: 1.0.3
Summary: Fast binary serializer for Python with optional C acceleration.
Author: Luis Fernando Montaño Hernandez
License-Expression: MIT
Keywords: serializer,pickle,binary,performance,python,serialization
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Archiving
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<div align="center">

<img src="https://github.com/lmontanohernandez8-png/Micronnx/blob/main/logo2.png" alt="compickle logo" width="180"/>

# 🥒 compickle

**Serialización binaria para Python con motor en C — rápido, compacto y sin dependencias.**

[![Python](https://img.shields.io/badge/Python-3.9%2B-blue?style=flat-square&logo=python&logoColor=white)](https://www.python.org/)
[![Motor C](https://img.shields.io/badge/Motor-C%20nativo-orange?style=flat-square&logo=c&logoColor=white)]()
[![Versión](https://img.shields.io/badge/versión-1.0.3-blue?style=flat-square)]()
[![Licencia](https://img.shields.io/badge/Licencia-MIT-green?style=flat-square)]()
[![Compilación](https://img.shields.io/badge/Compilado--O3%20--march%3Dnative-red?style=flat-square)]()

</div>

---

## ¿Qué es compickle?

`compickle` es un serializador binario escrito principalmente en **C** y expuesto como extensión nativa de Python (`_compickle`). Está diseñado para ser **simple de usar** y **más rápido que `pickle`** en la mayoría de cargas de trabajo, gracias a:

- Un **buffer de salida dinámico** (`Buf`) que crece en potencias de 2 desde 256 bytes inicial.
- Una **tabla de deduplicación FNV-1a** dinámica (crece en potencias de 2 desde 8 192 entradas iniciales) con 16 384 buckets hash — O(1) amortizado, con colisiones resueltas por lista enlazada.
- **Compilación nativa** con `-O3 -march=native -mtune=native` aplicada automáticamente en `setup.py`.
- Un **fallback puro en Python** (`compickle.py`) que implementa exactamente el mismo protocolo binario y se activa si la extensión C no está disponible.
- **Lazy loading** en `__init__.py`: la extensión C se importa solo en la primera llamada real a `dump`, `load`, `dumps`, `loads`, `dedup_reset` o `backend()`.

> **Versión actual:** `1.0.3` (definida en `__init__.py`)

---

## ⚙️ Instalación

```bash
pip install .
```

Para desarrollo (compilación en el directorio actual):

```bash
python setup.py build_ext --inplace
```

`setup.py` detecta la arquitectura vía `platform.machine()` y añade `-march=native -mtune=native` automáticamente en GCC/Clang. La extensión nativa se construye como `_compickle` (con guión bajo) y el módulo público `compickle` la importa internamente.

---

## 🚀 Uso rápido

```python
import compickle

datos = {
    "nombre": "Rex",
    "edad": 5,
    "activo": True,
    "coordenadas": (4.0, 2.0),
    "etiquetas": {"perro", "mascota"},
}

# Serializar a archivo
compickle.dump(datos, "datos.cpkl")

# Deserializar desde archivo
copia = compickle.load("datos.cpkl")

# Serializar/deserializar en memoria (bytes)
raw = compickle.dumps(datos)
copia2 = compickle.loads(raw)

# Ver qué motor está activo
print(compickle.backend())  # → 'c' o 'python'
```

---

## 📖 API completa

### `compickle.dump(obj, path)`

Serializa `obj` y escribe el resultado binario en `path`.

- Internamente llama a `dumps(obj)` y abre el archivo en modo `'wb'`.
- Resetea la tabla de deduplicación antes de cada serialización (vía `_c_dedup_reset()` en C o `_reset_write_state()` en Python).

```python
compickle.dump(mi_objeto, "salida.cpkl")
```

---

### `compickle.dumps(obj) → bytes`

Serializa `obj` y devuelve los bytes resultantes directamente.

```python
raw: bytes = compickle.dumps(mi_objeto)
```

---

### `compickle.load(path) → object`

Lee el archivo binario en `path` y reconstruye el objeto original.

```python
obj = compickle.load("salida.cpkl")
```

---

### `compickle.loads(data: bytes) → object`

Deserializa directamente desde un objeto `bytes`.

```python
obj = compickle.loads(raw_bytes)
```

---

### `compickle.dedup_reset()`

Resetea manualmente todas las tablas internas de deduplicación — tanto la del motor C como la del fallback Python — y también limpia el `source_cache` y `exec_cache`.

```python
compickle.dedup_reset()
```

Útil en procesos de larga duración que serializan muchos objetos distintos en sesiones separadas, para evitar que la tabla FNV-1a se llene (capacidad máxima: 8 192 entradas).

---

### `compickle.backend() → str`

Devuelve el motor activo. Dispara el lazy loading si aún no se había importado la extensión.

```python
compickle.backend()  # → 'c'      (extensión _compickle compilada)
                     # → 'python' (fallback puro Python)
```

---

## 🧩 Tipos soportados

| Tipo Python              | Tag    | Deduplicado | Notas                                                    |
|--------------------------|--------|:-----------:|----------------------------------------------------------|
| `None`                   | `0x00` | —           | 1 byte                                                   |
| `False`                  | `0x80` | —           | 1 byte                                                   |
| `True`                   | `0x81` | —           | 1 byte                                                   |
| `int` (0–58)             | `0xC0–0xFA` | —      | 1 byte: opcode directo `0xC0 + valor`                    |
| `int` (59–65535)         | `0x0F` | —           | 3 bytes: tag + `uint16` big-endian                       |
| `int` (arbitrario)       | `0x02` | —           | signo + longitud variable + bytes big-endian             |
| `float`                  | `0x03` | —           | IEEE 754 doble precisión, 8 bytes big-endian             |
| `complex`                | `0x04` | —           | Dos `float64` big-endian (16 bytes total)                |
| `str`                    | `0x05` | ✅           | UTF-8 + dedup FNV-1a                                     |
| `bytes`                  | `0x06` | ✅           | Deduplicado por contenido                                |
| `bytearray`              | `0x07` | ✅           | Deduplicado por contenido                                |
| `list`                   | `0x08` | —           | Elementos serializados recursivamente                    |
| `tuple`                  | `0x09` | —           | Elementos serializados recursivamente                    |
| `set`                    | `0x0A` | —           | Ordenado por `repr()` (Python) / `PySequence_List` (C)   |
| `frozenset`              | `0x0B` | —           | Ordenado por `repr()` (Python) / `PySequence_List` (C)   |
| `dict`                   | `0x0C` | —           | Claves y valores serializados recursivamente             |
| `function` (simple)      | `0x0D` | ✅ (source)  | Nombre UTF-8 + código fuente vía `inspect.getsource`     |
| `function` (closure)     | `0x1E` | ✅ (source)  | Fuente + dict de variables capturadas (`co_freevars`)    |
| `type` (clase)           | `0x12` | ✅           | Nombre + módulo + fuente (`inspect.getsource`)           |
| Instancia con `__dict__` | `0x1C` | ✅ (fuente)  | Encabezado de clase + `__dict__` serializado             |
| Instancia con `__slots__`| `0x1E` | ✅ (fuente)  | Recorre MRO completo para capturar todos los slots       |
| Objeto con `__reduce__`  | `0x1F` | ✅           | Prioridad máxima; soporta tuple de 2 o 3 elementos       |

> **Prioridad de serialización de instancias (C y Python coinciden):**
> 1. `__reduce__` personalizado en la clase → tag `0x1F`
> 2. `__dict__` disponible → tag `0x1C`
> 3. `__slots__` sin `__dict__` → tag `0x1E`

---

## 🔬 Cómo funciona internamente

### Motor C (`compickle.c`, ~898 líneas)

#### Buffer de salida (`Buf`)

```c
typedef struct { uint8_t *buf; Py_ssize_t len, cap; } Buf;
```

El buffer comienza con capacidad 256 bytes y se duplica vía `realloc` cada vez que el espacio disponible es insuficiente. Las funciones primitivas son:

```
buf_grow  → realloc en potencias de 2
buf_u8    → escribe 1 byte
buf_u16be → escribe uint16 big-endian (2 bytes)
buf_u64be → escribe uint64 big-endian (8 bytes)
buf_raw   → escribe N bytes con memcpy
```

#### Tabla de deduplicación

```c
#define DEDUP_CAP    8192
#define HASH_BUCKETS 16384

typedef struct { uint8_t tag; uint8_t *data; Py_ssize_t len; int next; } DEntry;
static DEntry dedup_table[DEDUP_CAP];
static int    hash_buckets[HASH_BUCKETS];
```

El hash usado es **FNV-1a** de 32 bits, aplicado sobre el `tag` de tipo seguido de los bytes del dato. Las colisiones se resuelven con listas enlazadas dentro de `DEntry.next`. La función `dedup_reset()` libera la memoria de cada entrada y hace `memset(-1)` sobre los buckets.

Cuando se registra un dato nuevo:

- Si `len ≤ 63` → se emite `0x0E + longitud_1byte + datos` (opcode de string corta, 2 bytes de cabecera).
- Si `len > 63` → se emite `0xFB + type_tag + write_len(n) + datos`.

Cuando se detecta duplicado → se emite una referencia compacta:

```
idx ≤ 0xFF   → 0xFE + 1 byte de índice  (2 bytes total)
idx ≤ 0xFFFF → 0xFD + 2 bytes big-endian (3 bytes total)
idx > 0xFFFF → 0xFC + 4 bytes big-endian (5 bytes total)
```

#### Codificación de longitudes variable

```
n ≤ 0x3F    → 1 byte (n directo)
n ≤ 0x3FFF  → 2 bytes (0x40 | n>>8 , n & 0xFF)
n > 0x3FFF  → 5 bytes (0xFF + uint32 big-endian)
```

#### Caché de fuentes y ejecución

- `source_cache` (dict): mapea `id(objeto)` → `bytes` del source obtenido con `inspect.getsource`. Evita llamar a `inspect` múltiples veces para el mismo objeto.
- `exec_cache` (dict): mapea `src_bytes` → `namespace dict` resultante de `PyRun_String`. Evita re-ejecutar el mismo source al deserializar.

#### Detección de `__reduce__` personalizado

La función `has_custom_reduce()` recorre el MRO del tipo del objeto, saltando `object` (PyBaseObject_Type), y busca `__reduce__` directamente en el `__dict__` de cada clase base. Esto evita falsos positivos por el `__reduce__` heredado de `object`.

#### Enteros arbitrarios — compatibilidad 3.13+

La API privada `_PyLong_NumBits` fue eliminada en Python 3.13. El motor C usa `bit_length()` vía `PyObject_CallMethod` para calcular el número de bytes necesarios, manteniendo compatibilidad con Python 3.9–3.13+.

---

### Fallback Python (`compickle.py`, ~390 líneas)

Implementa exactamente el mismo protocolo binario en Python puro. Las diferencias internas son:

- La deduplicación usa un `dict` Python `{(type_tag, raw_bytes): idx}` en lugar de la tabla C con FNV-1a.
- La serialización acumula en un `bytearray` y retorna `bytes(buf)`.
- La deserialización usa `memoryview` para acceso sin copia.
- Los closures se reconstruyen con `ctypes` y `types.FunctionType`, replicando las celdas de cierre.

El fallback Python también expone `dumps()` y `loads()` directamente, además de `dump()` y `load()` con archivo.

---

### Lazy loading (`__init__.py`, ~96 líneas)

```python
def _ensure_loaded():
    global _USE_C, _serialize_fast, _deserialize_fast, _c_dedup_reset
    if _USE_C is not None:
        return
    try:
        from ._compickle import serialize_fast, deserialize_fast, dedup_reset
        _USE_C = True
    except ImportError:
        _USE_C = False
```

La función `_ensure_loaded()` se llama en la primera invocación de cualquier función pública. Esto significa que un `import compickle` nunca falla aunque la extensión C no esté compilada — el fallback Python se activa transparentemente.

El módulo `__init__.py` también define `__getattr__` para resolver `_USE_C`, `serialize_fast` y `deserialize_fast` bajo demanda.

---

## 🏗️ Estructura del proyecto

```
compickle/
├── compickle.c     # Motor principal en C (~898 líneas) — extensión CPython _compickle
├── compickle.py    # Implementación de referencia en Python puro (~390 líneas)
├── __init__.py     # API pública con lazy loading (~96 líneas), versión 1.0.3
└── setup.py        # Build con -O3 -march=native -mtune=native (~20 líneas)
```

---

## 🧪 Ejemplos avanzados

### Clase con `__dict__`

```python
import compickle

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Punto(3.0, 7.5)
compickle.dump(p, "punto.cpkl")
p2 = compickle.load("punto.cpkl")
print(p2.x, p2.y)  # → 3.0  7.5
```

La clase se serializa incluyendo su código fuente completo (vía `inspect.getsource`). Al deserializar, el source se ejecuta en un namespace aislado y se recupera la clase por nombre.

---

### Clase con `__slots__`

```python
class Vector:
    __slots__ = ("x", "y", "z")
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z

v = Vector(1, 2, 3)
compickle.dump(v, "vector.cpkl")
v2 = compickle.load("vector.cpkl")
print(v2.x, v2.y, v2.z)  # → 1  2  3
```

El motor recorre el MRO completo para capturar todos los slots, incluyendo los heredados. Solo se incluyen los slots con valor asignado.

---

### Objeto con `__reduce__`

```python
class Color:
    def __init__(self, r, g, b):
        self.r, self.g, self.b = r, g, b

    def __reduce__(self):
        return (Color, (self.r, self.g, self.b))

c = Color(255, 128, 0)
compickle.dump(c, "color.cpkl")
c2 = compickle.load("color.cpkl")
print(c2.r, c2.g, c2.b)  # → 255  128  0
```

`__reduce__` puede devolver una tupla de 2 elementos `(callable, args)` o de 3 `(callable, args, state)`. El estado opcional se deserializa y aplica vía `__dict__.update()` o `setattr()`.

---

### Serialización en memoria (`dumps` / `loads`)

```python
import compickle

datos = [1, "hola", {"a": True}, (3.14,)]
raw = compickle.dumps(datos)
print(type(raw))         # → <class 'bytes'>
print(len(raw))          # tamaño compacto en bytes

restaurado = compickle.loads(raw)
assert restaurado == datos
```

---

### Uso de deduplicación manual

```python
import compickle

# Resetear antes de una sesión nueva
compickle.dedup_reset()

# Serializar múltiples objetos que comparten strings
for i in range(100):
    compickle.dumps({"tipo": "evento", "id": i, "fuente": "sensor_A"})

# Las strings "tipo", "evento", "fuente", "sensor_A" se deduplicarán
# en la primera aparición y se referenciarán con 2–5 bytes en las siguientes.
```

---

## 📦 Formato binario — tabla completa de opcodes

```
Opcode      Tipo / Significado
─────────────────────────────────────────────────────────────────────
0x00        None
0x02        int arbitrario: signo(1) + write_len + bytes big-endian
0x03        float: 8 bytes IEEE 754 big-endian
0x04        complex: 2 × float64 big-endian (16 bytes)
0x05        str: tag + write_dedup(UTF-8)
0x06        bytes: tag + write_dedup(contenido)
0x07        bytearray: tag + write_dedup(contenido)
0x08        list: write_len(n) + n × serialize(item)
0x09        tuple: write_len(n) + n × serialize(item)
0x0A        set: write_len(n) + n × serialize(item)
0x0B        frozenset: write_len(n) + n × serialize(item)
0x0C        dict: write_len(n) + n × (serialize(k) + serialize(v))
0x0D        function: 0x05+nombre + write_dedup(source)
0x0E        dedup short: longitud(1 byte) + datos (≤ 63 bytes, nueva entrada)
0x0F        int 16 bits: 2 bytes uint16 big-endian (rango 59–65535)
0x12        class: 0x05+nombre + 0x05+módulo + write_dedup(source)
0x1C        instancia __dict__: class_header + 0x0C + dict
0x1D        (interno) tag de source en tabla de dedup
0x1E        instancia __slots__ / closure: class_header + 0x0C + dict de slots
0x1F        instancia __reduce__: callable_ref + args(0x09) + flags(1) + [state] + [list_items] + [dict_items]
0x80        False
0x81        True
0xC0–0xFA   int pequeño (valor = opcode − 0xC0, rango 0–58)
0xFB        dedup long: type_tag(1) + write_len + datos (nueva entrada, > 63 bytes)
0xFC        referencia dedup: 4 bytes big-endian de índice
0xFD        referencia dedup: 2 bytes big-endian de índice
0xFE        referencia dedup: 1 byte de índice (índice ≤ 255)
```

---

## ⚠️ Limitaciones conocidas

- **Funciones y clases requieren source:** `inspect.getsource()` debe poder acceder al código fuente en tiempo de serialización. No funciona con lambdas anónimas, funciones definidas en el REPL interactivo o código generado dinámicamente con `exec()`. El callable usado en `__reduce__` sí tiene fallback a builtins si no se encuentra su fuente.
- **No compatible con `pickle`:** el formato binario es propio y no intercambiable con `pickle`, `marshal` u otros serializadores estándar de Python.
- **`__reduce__` requiere callable con `__name__`:** el callable devuelto por `__reduce__` debe tener atributo `__name__` accesible. Callables arbitrarios sin nombre (por ejemplo, instancias de clases con `__call__`) no están soportados.

---

## 📄 Licencia

MIT — úsalo como quieras.
