beanis


Namebeanis JSON
Version 0.2.0 PyPI version JSON
download
home_pageNone
SummaryAsynchronous Python ODM for Redis
upload_time2025-10-24 11:06:47
maintainerNone
docs_urlNone
authorNone
requires_python<4.0,>=3.9
licenseNone
keywords redis odm orm pydantic async python
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Beanis - Redis ODM for Humans

[![Beanis](https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/logo-no-background.svg)](https://github.com/andreim14/beanis)

<div align="center">
  <a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/v/beanis?color=blue" alt="PyPI version"></a>
  <a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/dm/beanis?color=blue" alt="Downloads"></a>
  <a href="https://pypi.python.org/pypi/beanis"><img src="https://img.shields.io/pypi/pyversions/beanis?color=blue" alt="Python versions"></a>
  <a href="https://github.com/andreim14/beanis/blob/main/LICENSE"><img src="https://img.shields.io/github/license/andreim14/beanis?color=blue" alt="License"></a>
  <a href="https://github.com/andreim14/beanis"><img src="https://img.shields.io/github/stars/andreim14/beanis?style=social" alt="GitHub stars"></a>
</div>

<div align="center">
  <a href="https://github.com/andreim14/beanis/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/andreim14/beanis/test.yml?branch=main&label=tests" alt="Tests"></a>
  <a href="https://github.com/andreim14/beanis"><img src="https://img.shields.io/badge/coverage-56%25-yellow" alt="Coverage"></a>
  <a href="https://github.com/andreim14/beanis"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code style: black"></a>
  <a href="https://pydantic.dev"><img src="https://img.shields.io/badge/Pydantic-2.x-E92063?logo=pydantic" alt="Pydantic"></a>
  <a href="https://redis.io"><img src="https://img.shields.io/badge/Redis-7.x-DC382D?logo=redis&logoColor=white" alt="Redis"></a>
</div>

<div align="center">
  <b>
    📚 <a href="https://andreim14.github.io/beanis/">Documentation</a> |
    🚀 <a href="https://andreim14.github.io/beanis/getting-started/">Getting Started</a> |
    📖 <a href="https://andreim14.github.io/beanis/tutorial/">Tutorial</a> |
    🔧 <a href="https://andreim14.github.io/beanis/api-documentation/document/">API Reference</a>
  </b>
</div>

---

**Stop writing boilerplate Redis code. Focus on your application logic.**

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

## Why Beanis?

### The Problem with Vanilla Redis

❌ **Manual serialization** - You write `json.dumps()` and `json.loads()` everywhere
❌ **Type conversions** - Strings from Redis need manual `float()`, `int()` conversions
❌ **Key management** - You track `"Product:123"`, `"all:Product"` keys manually
❌ **No validation** - Bad data silently corrupts your Redis database
❌ **Boilerplate code** - 15-20 lines for simple CRUD operations

### The Solution: Beanis

✅ **Automatic serialization** - Nested objects, lists, custom types - all handled
✅ **Type safety** - Full Pydantic validation + IDE autocomplete
✅ **Smart key management** - Focus on your data, not Redis internals
✅ **Data validation** - Catch errors before they hit Redis
✅ **Write 70% less code** - 5-7 lines for the same operations

**AND it's fast:** Only 8% overhead vs vanilla Redis

### Who Should Use Beanis?

✅ You're building a **production app** that needs Redis but not the boilerplate
✅ You want **type safety and validation** without sacrificing performance
✅ You're using **vanilla Redis** (no RedisJSON/RediSearch modules)
✅ You like **Beanie's MongoDB API** and want the same for Redis
✅ You're storing **complex data** (nested objects, NumPy arrays, etc.)

### When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py)
❌ You need RedisJSON/RediSearch features (use Redis OM)
❌ You're only storing simple key-value pairs (use raw redis-py)

## Show Me The Code

### Basic CRUD Operation

<table>
<tr>
<th>Vanilla Redis (20 lines)</th>
<th>Beanis (7 lines)</th>
</tr>
<tr>
<td>

```python
import json
import time
from redis.asyncio import Redis

redis = Redis(decode_responses=True)

# Insert
product_data = {
    "name": "Tony's Chocolonely",
    "price": "5.95",
    "category": json.dumps({
        "name": "Chocolate",
        "description": "Roasted cacao"
    })
}
await redis.hset("Product:prod_123",
                 mapping=product_data)
await redis.zadd("all:Product",
                 {"prod_123": time.time()})

# Retrieve
raw = await redis.hgetall("Product:prod_123")
product = {
    "name": raw["name"],
    "price": float(raw["price"]),
    "category": json.loads(raw["category"])
}
```

</td>
<td>

```python
from beanis import Document
from pydantic import BaseModel

class Category(BaseModel):
    name: str
    description: str

class Product(Document):
    name: str
    price: float
    category: Category

# Insert
product = Product(
    name="Tony's Chocolonely",
    price=5.95,
    category=Category(
        name="Chocolate",
        description="Roasted cacao"
    )
)
await product.insert()

# Retrieve
found = await Product.get(product.id)
```

</td>
</tr>
</table>

**Result:** Type-safe, validated, **65% less code**

### Search/Query Operation

<table>
<tr>
<th>Vanilla Redis (25 lines)</th>
<th>Beanis (4 lines)</th>
</tr>
<tr>
<td>

```python
# Find products between $10-50
keys = await redis.zrangebyscore(
    "idx:Product:price",
    min=10.0,
    max=50.0
)

# Fetch each product using pipeline
pipe = redis.pipeline()
for key in keys:
    pipe.hgetall(f"Product:{key}")
results = await pipe.execute()

# Parse manually
products = []
for data in results:
    if data:
        products.append({
            "name": data["name"],
            "price": float(data["price"]),
            "stock": int(data["stock"]),
            "category": json.loads(
                data.get("category", "{}")
            )
        })
```

</td>
<td>

```python
# Find products between $10-50
products = await Product.find(
    Product.price >= 10.0,
    Product.price <= 50.0
).to_list()
```

</td>
</tr>
</table>

**Result:** **84% less code**, fully typed results

### Update Operation

<table>
<tr>
<th>Vanilla Redis (10 lines)</th>
<th>Beanis (6 lines)</th>
</tr>
<tr>
<td>

```python
# Update price and stock
await redis.hset("Product:123", mapping={
    "price": "6.95",
    "stock": "150"
})

# Atomic increment
new_stock = await redis.hincrby(
    "Product:123",
    "stock",
    -1
)
```

</td>
<td>

```python
# Update fields
await product.update(
    price=6.95,
    stock=150
)

# Atomic increment
new_stock = await product.increment_field(
    "stock", -1
)
```

</td>
</tr>
</table>

**Result:** Same functionality, cleaner API, type-safe

### Batch Operations

<table>
<tr>
<th>Vanilla Redis (14 lines)</th>
<th>Beanis (9 lines)</th>
</tr>
<tr>
<td>

```python
# Insert 100 products
pipe = redis.pipeline()
for i in range(100):
    product_id = f"prod_{i}"
    data = {
        "name": f"Product {i}",
        "price": str(i * 10),
        "stock": "100",
        "category": json.dumps({
            "name": "Category"
        })
    }
    pipe.hset(f"Product:{product_id}",
              mapping=data)
    pipe.zadd("all:Product",
              {product_id: time.time()})
await pipe.execute()
```

</td>
<td>

```python
# Insert 100 products
products = [
    Product(
        name=f"Product {i}",
        price=i * 10,
        stock=100,
        category=Category(name="Category")
    )
    for i in range(100)
]
await Product.insert_many(products)
```

</td>
</tr>
</table>

**Result:** **35% less code**, no manual key management

## Installation

### PIP

```shell
pip install beanis
```

### Poetry

```shell
poetry add beanis
```

## Quick Start

```python
import asyncio
from typing import Optional
from redis.asyncio import Redis
from pydantic import BaseModel
from beanis import Document, init_beanis, Indexed


class Category(BaseModel):
    name: str
    description: str


class Product(Document):
    name: str
    description: Optional[str] = None
    price: Indexed(float)  # Indexed for range queries
    category: Category
    stock: int = 0

    class Settings:
        name = "products"


async def main():
    # Initialize Redis client
    client = Redis(host="localhost", port=6379, db=0, decode_responses=True)

    # Initialize Beanis
    await init_beanis(database=client, document_models=[Product])

    # Create a product
    chocolate = Category(
        name="Chocolate",
        description="A preparation of roasted and ground cacao seeds."
    )

    product = Product(
        name="Tony's Chocolonely",
        price=5.95,
        category=chocolate,
        stock=100
    )

    # Insert into Redis
    await product.insert()

    # Retrieve by ID
    found = await Product.get(product.id)
    print(f"Found: {found.name} - ${found.price}")

    # Query by price range
    affordable = await Product.find(
        Product.price < 10.0
    ).to_list()
    print(f"Affordable products: {len(affordable)}")

    # Update specific fields
    await product.update(price=6.95, stock=150)

    # Atomic increment
    new_stock = await product.increment_field("stock", -1)
    print(f"Stock after sale: {new_stock}")

    # Get all products
    all_products = await Product.all()
    print(f"Total products: {len(all_products)}")

    # Delete
    await product.delete_self()

    await client.close()


if __name__ == "__main__":
    asyncio.run(main())
```

## Core Features

### 🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

```python
class Product(Document):
    name: str
    price: float  # Must be a number
    stock: int    # Must be an integer
    category: Category  # Must be a valid Category object

# This will raise a validation error BEFORE hitting Redis
product = Product(
    name="Invalid",
    price="not a number",  # ❌ ValidationError!
    stock=100
)
```

### ⚡ High Performance

Beanis is optimized for speed with minimal overhead:

- **8% overhead** vs vanilla Redis (benchmarked)
- Uses `msgspec` for ultra-fast JSON parsing (2x faster than orjson)
- Skips Pydantic validation on reads by default (data from Redis is trusted)
- Efficient pipeline usage for batch operations

**Benchmark Results** (Get by ID):
- Vanilla Redis: 1.00x (baseline)
- Beanis: 1.08x (only 8% slower)
- Redis OM: 1.20x (20% slower)


### 🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

```python
# Query with Pythonic operators
products = await Product.find(
    Product.price >= 10.0,
    Product.price <= 50.0,
    Product.stock > 0
).to_list()

# Chaining operations
expensive = await Product.find(
    Product.price > 100
).sort(Product.price).limit(10).to_list()

# Batch operations
await Product.insert_many([product1, product2, product3])
products = await Product.get_many([id1, id2, id3])
await Product.delete_many([id1, id2])
```

### 📦 Store Anything

Beanis handles complex types automatically:

**Built-in support:**
- Nested Pydantic models
- Lists, dicts, tuples, sets
- Decimal, UUID, Enum
- datetime, date, time, timedelta

**Custom types via encoders:**
```python
from beanis import Document, register_type
import numpy as np

# NumPy arrays work automatically (auto-registered)
class MLModel(Document):
    name: str
    weights: Any  # Stores np.ndarray!

model = MLModel(name="v1", weights=np.random.rand(100, 100))
await model.insert()  # Just works!

# Custom types
register_type(
    MyCustomType,
    encoder=lambda obj: str(obj),
    decoder=lambda s: MyCustomType.from_string(s)
)
```

See tutorial on how to create them!

### 🔧 Production Ready Features

**TTL Support:**
```python
# Insert with TTL
await product.insert(ttl=3600)  # Expires in 1 hour

# Set TTL on existing document
await product.set_ttl(7200)
ttl = await product.get_ttl()
await product.persist()  # Remove TTL
```

**Event Hooks:**
```python
from beanis import before_event, after_event, Insert, Update

class Product(Document):
    name: str
    price: float

    @before_event(Insert)
    async def validate_price(self):
        if self.price < 0:
            raise ValueError("Price cannot be negative")

    @after_event(Insert)
    async def log_creation(self):
        print(f"Created product: {self.name}")
```

**Field-Level Operations:**
```python
# Get/set single field without loading entire document
price = await product.get_field("price")
await product.set_field("stock", 200)

# Atomic increment
new_stock = await product.increment_field("stock", 5)
```

**Document Tracking:**
```python
# Get all documents (sorted by insertion time)
all_products = await Product.all()

# Pagination
page1 = await Product.all(limit=10)
page2 = await Product.all(skip=10, limit=10)

# Count and delete all
count = await Product.count()
await Product.delete_all()
```

## Comparison

| Feature | Vanilla Redis | Beanis | Redis OM |
|---------|--------------|---------|----------|
| **Code volume** | 100% | **30%** ⭐ | 50% |
| **Type safety** | Manual | **Automatic** ⭐ | Automatic |
| **Performance** | **100%** ⭐ | 108% | 120% |
| **Vanilla Redis** | ✅ | **✅** ⭐ | ❌ Requires modules |
| **Validation** | Manual | **Automatic** ⭐ | Automatic |
| **API Style** | Redis commands | **Pythonic** ⭐ | Redis OM |
| **Learning curve** | Medium | **Easy** ⭐ | Medium |
| **Nested objects** | Manual | **Automatic** ⭐ | Automatic |
| **Custom types** | Manual | **Easy** ⭐ | Limited |
| **Event hooks** | ❌ | **✅** ⭐ | ❌ |
| **All DBs (0-15)** | ✅ | **✅** ⭐ | ❌ DB 0 only |

## Choosing the Right Tool

### Choose Vanilla Redis when:
- Every microsecond matters (high-frequency trading, etc.)
- Simple key-value storage
- You're a Redis expert and don't need abstractions

### Choose Beanis when: ⭐
- **Building production applications** with complex data models
- Want **type safety + performance** (8% overhead is acceptable)
- Using **vanilla Redis** (no RedisJSON/RediSearch modules)
- Need to store **nested objects, custom types, NumPy arrays**, etc.
- Coming from **MongoDB/Beanie** and want familiar API
- Want **event hooks** for validation and lifecycle management

### Choose Redis OM when:
- You need **RedisJSON/RediSearch** features
- Don't mind installing Redis modules
- Want Redis Stack integration
- Need advanced full-text search

## Requirements

- Python 3.8+
- Redis 5.0+
- Pydantic 1.10+ or 2.0+

## Testing

```bash
# Run tests
pytest

# Run with coverage
pytest --cov=beanis

# Run specific test
pytest tests/test_core.py::test_insert_and_get
```

## Credits

Beanis is a fork of [Beanie](https://github.com/BeanieODM/beanie) - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original [Beanie](https://github.com/BeanieODM/beanie) - it's awesome!

**Special thanks to:**
- Roman Right and the Beanie community for creating the foundation
- All Beanie contributors whose code inspired this project
- The Redis and Pydantic teams for their excellent libraries

[![Beanie](https://raw.githubusercontent.com/roman-right/beanie/main/assets/logo/white_bg.svg)](https://github.com/BeanieODM/beanie)

## License

Apache License 2.0


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "beanis",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.9",
    "maintainer_email": null,
    "keywords": "redis, odm, orm, pydantic, async, python",
    "author": null,
    "author_email": "Andrei Stefan Bejgu <stefan.bejgu@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/20/5f/d979084124a83d464d8bfd914195fd61651af0a9924be6b4ade63f6ddd35/beanis-0.2.0.tar.gz",
    "platform": null,
    "description": "# Beanis - Redis ODM for Humans\n\n[![Beanis](https://raw.githubusercontent.com/andreim14/beanis/main/assets/logo/logo-no-background.svg)](https://github.com/andreim14/beanis)\n\n<div align=\"center\">\n  <a href=\"https://pypi.python.org/pypi/beanis\"><img src=\"https://img.shields.io/pypi/v/beanis?color=blue\" alt=\"PyPI version\"></a>\n  <a href=\"https://pypi.python.org/pypi/beanis\"><img src=\"https://img.shields.io/pypi/dm/beanis?color=blue\" alt=\"Downloads\"></a>\n  <a href=\"https://pypi.python.org/pypi/beanis\"><img src=\"https://img.shields.io/pypi/pyversions/beanis?color=blue\" alt=\"Python versions\"></a>\n  <a href=\"https://github.com/andreim14/beanis/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/andreim14/beanis?color=blue\" alt=\"License\"></a>\n  <a href=\"https://github.com/andreim14/beanis\"><img src=\"https://img.shields.io/github/stars/andreim14/beanis?style=social\" alt=\"GitHub stars\"></a>\n</div>\n\n<div align=\"center\">\n  <a href=\"https://github.com/andreim14/beanis/actions/workflows/test.yml\"><img src=\"https://img.shields.io/github/actions/workflow/status/andreim14/beanis/test.yml?branch=main&label=tests\" alt=\"Tests\"></a>\n  <a href=\"https://github.com/andreim14/beanis\"><img src=\"https://img.shields.io/badge/coverage-56%25-yellow\" alt=\"Coverage\"></a>\n  <a href=\"https://github.com/andreim14/beanis\"><img src=\"https://img.shields.io/badge/code%20style-black-000000.svg\" alt=\"Code style: black\"></a>\n  <a href=\"https://pydantic.dev\"><img src=\"https://img.shields.io/badge/Pydantic-2.x-E92063?logo=pydantic\" alt=\"Pydantic\"></a>\n  <a href=\"https://redis.io\"><img src=\"https://img.shields.io/badge/Redis-7.x-DC382D?logo=redis&logoColor=white\" alt=\"Redis\"></a>\n</div>\n\n<div align=\"center\">\n  <b>\n    \ud83d\udcda <a href=\"https://andreim14.github.io/beanis/\">Documentation</a> |\n    \ud83d\ude80 <a href=\"https://andreim14.github.io/beanis/getting-started/\">Getting Started</a> |\n    \ud83d\udcd6 <a href=\"https://andreim14.github.io/beanis/tutorial/\">Tutorial</a> |\n    \ud83d\udd27 <a href=\"https://andreim14.github.io/beanis/api-documentation/document/\">API Reference</a>\n  </b>\n</div>\n\n---\n\n**Stop writing boilerplate Redis code. Focus on your application logic.**\n\nBeanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.\n\n## Why Beanis?\n\n### The Problem with Vanilla Redis\n\n\u274c **Manual serialization** - You write `json.dumps()` and `json.loads()` everywhere\n\u274c **Type conversions** - Strings from Redis need manual `float()`, `int()` conversions\n\u274c **Key management** - You track `\"Product:123\"`, `\"all:Product\"` keys manually\n\u274c **No validation** - Bad data silently corrupts your Redis database\n\u274c **Boilerplate code** - 15-20 lines for simple CRUD operations\n\n### The Solution: Beanis\n\n\u2705 **Automatic serialization** - Nested objects, lists, custom types - all handled\n\u2705 **Type safety** - Full Pydantic validation + IDE autocomplete\n\u2705 **Smart key management** - Focus on your data, not Redis internals\n\u2705 **Data validation** - Catch errors before they hit Redis\n\u2705 **Write 70% less code** - 5-7 lines for the same operations\n\n**AND it's fast:** Only 8% overhead vs vanilla Redis\n\n### Who Should Use Beanis?\n\n\u2705 You're building a **production app** that needs Redis but not the boilerplate\n\u2705 You want **type safety and validation** without sacrificing performance\n\u2705 You're using **vanilla Redis** (no RedisJSON/RediSearch modules)\n\u2705 You like **Beanie's MongoDB API** and want the same for Redis\n\u2705 You're storing **complex data** (nested objects, NumPy arrays, etc.)\n\n### When NOT to Use Beanis?\n\n\u274c You need every microsecond of performance (use raw redis-py)\n\u274c You need RedisJSON/RediSearch features (use Redis OM)\n\u274c You're only storing simple key-value pairs (use raw redis-py)\n\n## Show Me The Code\n\n### Basic CRUD Operation\n\n<table>\n<tr>\n<th>Vanilla Redis (20 lines)</th>\n<th>Beanis (7 lines)</th>\n</tr>\n<tr>\n<td>\n\n```python\nimport json\nimport time\nfrom redis.asyncio import Redis\n\nredis = Redis(decode_responses=True)\n\n# Insert\nproduct_data = {\n    \"name\": \"Tony's Chocolonely\",\n    \"price\": \"5.95\",\n    \"category\": json.dumps({\n        \"name\": \"Chocolate\",\n        \"description\": \"Roasted cacao\"\n    })\n}\nawait redis.hset(\"Product:prod_123\",\n                 mapping=product_data)\nawait redis.zadd(\"all:Product\",\n                 {\"prod_123\": time.time()})\n\n# Retrieve\nraw = await redis.hgetall(\"Product:prod_123\")\nproduct = {\n    \"name\": raw[\"name\"],\n    \"price\": float(raw[\"price\"]),\n    \"category\": json.loads(raw[\"category\"])\n}\n```\n\n</td>\n<td>\n\n```python\nfrom beanis import Document\nfrom pydantic import BaseModel\n\nclass Category(BaseModel):\n    name: str\n    description: str\n\nclass Product(Document):\n    name: str\n    price: float\n    category: Category\n\n# Insert\nproduct = Product(\n    name=\"Tony's Chocolonely\",\n    price=5.95,\n    category=Category(\n        name=\"Chocolate\",\n        description=\"Roasted cacao\"\n    )\n)\nawait product.insert()\n\n# Retrieve\nfound = await Product.get(product.id)\n```\n\n</td>\n</tr>\n</table>\n\n**Result:** Type-safe, validated, **65% less code**\n\n### Search/Query Operation\n\n<table>\n<tr>\n<th>Vanilla Redis (25 lines)</th>\n<th>Beanis (4 lines)</th>\n</tr>\n<tr>\n<td>\n\n```python\n# Find products between $10-50\nkeys = await redis.zrangebyscore(\n    \"idx:Product:price\",\n    min=10.0,\n    max=50.0\n)\n\n# Fetch each product using pipeline\npipe = redis.pipeline()\nfor key in keys:\n    pipe.hgetall(f\"Product:{key}\")\nresults = await pipe.execute()\n\n# Parse manually\nproducts = []\nfor data in results:\n    if data:\n        products.append({\n            \"name\": data[\"name\"],\n            \"price\": float(data[\"price\"]),\n            \"stock\": int(data[\"stock\"]),\n            \"category\": json.loads(\n                data.get(\"category\", \"{}\")\n            )\n        })\n```\n\n</td>\n<td>\n\n```python\n# Find products between $10-50\nproducts = await Product.find(\n    Product.price >= 10.0,\n    Product.price <= 50.0\n).to_list()\n```\n\n</td>\n</tr>\n</table>\n\n**Result:** **84% less code**, fully typed results\n\n### Update Operation\n\n<table>\n<tr>\n<th>Vanilla Redis (10 lines)</th>\n<th>Beanis (6 lines)</th>\n</tr>\n<tr>\n<td>\n\n```python\n# Update price and stock\nawait redis.hset(\"Product:123\", mapping={\n    \"price\": \"6.95\",\n    \"stock\": \"150\"\n})\n\n# Atomic increment\nnew_stock = await redis.hincrby(\n    \"Product:123\",\n    \"stock\",\n    -1\n)\n```\n\n</td>\n<td>\n\n```python\n# Update fields\nawait product.update(\n    price=6.95,\n    stock=150\n)\n\n# Atomic increment\nnew_stock = await product.increment_field(\n    \"stock\", -1\n)\n```\n\n</td>\n</tr>\n</table>\n\n**Result:** Same functionality, cleaner API, type-safe\n\n### Batch Operations\n\n<table>\n<tr>\n<th>Vanilla Redis (14 lines)</th>\n<th>Beanis (9 lines)</th>\n</tr>\n<tr>\n<td>\n\n```python\n# Insert 100 products\npipe = redis.pipeline()\nfor i in range(100):\n    product_id = f\"prod_{i}\"\n    data = {\n        \"name\": f\"Product {i}\",\n        \"price\": str(i * 10),\n        \"stock\": \"100\",\n        \"category\": json.dumps({\n            \"name\": \"Category\"\n        })\n    }\n    pipe.hset(f\"Product:{product_id}\",\n              mapping=data)\n    pipe.zadd(\"all:Product\",\n              {product_id: time.time()})\nawait pipe.execute()\n```\n\n</td>\n<td>\n\n```python\n# Insert 100 products\nproducts = [\n    Product(\n        name=f\"Product {i}\",\n        price=i * 10,\n        stock=100,\n        category=Category(name=\"Category\")\n    )\n    for i in range(100)\n]\nawait Product.insert_many(products)\n```\n\n</td>\n</tr>\n</table>\n\n**Result:** **35% less code**, no manual key management\n\n## Installation\n\n### PIP\n\n```shell\npip install beanis\n```\n\n### Poetry\n\n```shell\npoetry add beanis\n```\n\n## Quick Start\n\n```python\nimport asyncio\nfrom typing import Optional\nfrom redis.asyncio import Redis\nfrom pydantic import BaseModel\nfrom beanis import Document, init_beanis, Indexed\n\n\nclass Category(BaseModel):\n    name: str\n    description: str\n\n\nclass Product(Document):\n    name: str\n    description: Optional[str] = None\n    price: Indexed(float)  # Indexed for range queries\n    category: Category\n    stock: int = 0\n\n    class Settings:\n        name = \"products\"\n\n\nasync def main():\n    # Initialize Redis client\n    client = Redis(host=\"localhost\", port=6379, db=0, decode_responses=True)\n\n    # Initialize Beanis\n    await init_beanis(database=client, document_models=[Product])\n\n    # Create a product\n    chocolate = Category(\n        name=\"Chocolate\",\n        description=\"A preparation of roasted and ground cacao seeds.\"\n    )\n\n    product = Product(\n        name=\"Tony's Chocolonely\",\n        price=5.95,\n        category=chocolate,\n        stock=100\n    )\n\n    # Insert into Redis\n    await product.insert()\n\n    # Retrieve by ID\n    found = await Product.get(product.id)\n    print(f\"Found: {found.name} - ${found.price}\")\n\n    # Query by price range\n    affordable = await Product.find(\n        Product.price < 10.0\n    ).to_list()\n    print(f\"Affordable products: {len(affordable)}\")\n\n    # Update specific fields\n    await product.update(price=6.95, stock=150)\n\n    # Atomic increment\n    new_stock = await product.increment_field(\"stock\", -1)\n    print(f\"Stock after sale: {new_stock}\")\n\n    # Get all products\n    all_products = await Product.all()\n    print(f\"Total products: {len(all_products)}\")\n\n    # Delete\n    await product.delete_self()\n\n    await client.close()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Core Features\n\n### \ud83d\ude80 Type Safety & Validation\n\nBeanis uses Pydantic models, giving you automatic validation and type checking:\n\n```python\nclass Product(Document):\n    name: str\n    price: float  # Must be a number\n    stock: int    # Must be an integer\n    category: Category  # Must be a valid Category object\n\n# This will raise a validation error BEFORE hitting Redis\nproduct = Product(\n    name=\"Invalid\",\n    price=\"not a number\",  # \u274c ValidationError!\n    stock=100\n)\n```\n\n### \u26a1 High Performance\n\nBeanis is optimized for speed with minimal overhead:\n\n- **8% overhead** vs vanilla Redis (benchmarked)\n- Uses `msgspec` for ultra-fast JSON parsing (2x faster than orjson)\n- Skips Pydantic validation on reads by default (data from Redis is trusted)\n- Efficient pipeline usage for batch operations\n\n**Benchmark Results** (Get by ID):\n- Vanilla Redis: 1.00x (baseline)\n- Beanis: 1.08x (only 8% slower)\n- Redis OM: 1.20x (20% slower)\n\n\n### \ud83c\udfaf Pythonic API\n\nFamiliar Beanie-style interface for MongoDB developers:\n\n```python\n# Query with Pythonic operators\nproducts = await Product.find(\n    Product.price >= 10.0,\n    Product.price <= 50.0,\n    Product.stock > 0\n).to_list()\n\n# Chaining operations\nexpensive = await Product.find(\n    Product.price > 100\n).sort(Product.price).limit(10).to_list()\n\n# Batch operations\nawait Product.insert_many([product1, product2, product3])\nproducts = await Product.get_many([id1, id2, id3])\nawait Product.delete_many([id1, id2])\n```\n\n### \ud83d\udce6 Store Anything\n\nBeanis handles complex types automatically:\n\n**Built-in support:**\n- Nested Pydantic models\n- Lists, dicts, tuples, sets\n- Decimal, UUID, Enum\n- datetime, date, time, timedelta\n\n**Custom types via encoders:**\n```python\nfrom beanis import Document, register_type\nimport numpy as np\n\n# NumPy arrays work automatically (auto-registered)\nclass MLModel(Document):\n    name: str\n    weights: Any  # Stores np.ndarray!\n\nmodel = MLModel(name=\"v1\", weights=np.random.rand(100, 100))\nawait model.insert()  # Just works!\n\n# Custom types\nregister_type(\n    MyCustomType,\n    encoder=lambda obj: str(obj),\n    decoder=lambda s: MyCustomType.from_string(s)\n)\n```\n\nSee tutorial on how to create them!\n\n### \ud83d\udd27 Production Ready Features\n\n**TTL Support:**\n```python\n# Insert with TTL\nawait product.insert(ttl=3600)  # Expires in 1 hour\n\n# Set TTL on existing document\nawait product.set_ttl(7200)\nttl = await product.get_ttl()\nawait product.persist()  # Remove TTL\n```\n\n**Event Hooks:**\n```python\nfrom beanis import before_event, after_event, Insert, Update\n\nclass Product(Document):\n    name: str\n    price: float\n\n    @before_event(Insert)\n    async def validate_price(self):\n        if self.price < 0:\n            raise ValueError(\"Price cannot be negative\")\n\n    @after_event(Insert)\n    async def log_creation(self):\n        print(f\"Created product: {self.name}\")\n```\n\n**Field-Level Operations:**\n```python\n# Get/set single field without loading entire document\nprice = await product.get_field(\"price\")\nawait product.set_field(\"stock\", 200)\n\n# Atomic increment\nnew_stock = await product.increment_field(\"stock\", 5)\n```\n\n**Document Tracking:**\n```python\n# Get all documents (sorted by insertion time)\nall_products = await Product.all()\n\n# Pagination\npage1 = await Product.all(limit=10)\npage2 = await Product.all(skip=10, limit=10)\n\n# Count and delete all\ncount = await Product.count()\nawait Product.delete_all()\n```\n\n## Comparison\n\n| Feature | Vanilla Redis | Beanis | Redis OM |\n|---------|--------------|---------|----------|\n| **Code volume** | 100% | **30%** \u2b50 | 50% |\n| **Type safety** | Manual | **Automatic** \u2b50 | Automatic |\n| **Performance** | **100%** \u2b50 | 108% | 120% |\n| **Vanilla Redis** | \u2705 | **\u2705** \u2b50 | \u274c Requires modules |\n| **Validation** | Manual | **Automatic** \u2b50 | Automatic |\n| **API Style** | Redis commands | **Pythonic** \u2b50 | Redis OM |\n| **Learning curve** | Medium | **Easy** \u2b50 | Medium |\n| **Nested objects** | Manual | **Automatic** \u2b50 | Automatic |\n| **Custom types** | Manual | **Easy** \u2b50 | Limited |\n| **Event hooks** | \u274c | **\u2705** \u2b50 | \u274c |\n| **All DBs (0-15)** | \u2705 | **\u2705** \u2b50 | \u274c DB 0 only |\n\n## Choosing the Right Tool\n\n### Choose Vanilla Redis when:\n- Every microsecond matters (high-frequency trading, etc.)\n- Simple key-value storage\n- You're a Redis expert and don't need abstractions\n\n### Choose Beanis when: \u2b50\n- **Building production applications** with complex data models\n- Want **type safety + performance** (8% overhead is acceptable)\n- Using **vanilla Redis** (no RedisJSON/RediSearch modules)\n- Need to store **nested objects, custom types, NumPy arrays**, etc.\n- Coming from **MongoDB/Beanie** and want familiar API\n- Want **event hooks** for validation and lifecycle management\n\n### Choose Redis OM when:\n- You need **RedisJSON/RediSearch** features\n- Don't mind installing Redis modules\n- Want Redis Stack integration\n- Need advanced full-text search\n\n## Requirements\n\n- Python 3.8+\n- Redis 5.0+\n- Pydantic 1.10+ or 2.0+\n\n## Testing\n\n```bash\n# Run tests\npytest\n\n# Run with coverage\npytest --cov=beanis\n\n# Run specific test\npytest tests/test_core.py::test_insert_and_get\n```\n\n## Credits\n\nBeanis is a fork of [Beanie](https://github.com/BeanieODM/beanie) - the amazing MongoDB ODM created by Roman Right and contributors.\n\nWe took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original [Beanie](https://github.com/BeanieODM/beanie) - it's awesome!\n\n**Special thanks to:**\n- Roman Right and the Beanie community for creating the foundation\n- All Beanie contributors whose code inspired this project\n- The Redis and Pydantic teams for their excellent libraries\n\n[![Beanie](https://raw.githubusercontent.com/roman-right/beanie/main/assets/logo/white_bg.svg)](https://github.com/BeanieODM/beanie)\n\n## License\n\nApache License 2.0\n\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Asynchronous Python ODM for Redis",
    "version": "0.2.0",
    "project_urls": {
        "homepage": "https://github.com/andreim14/beanis",
        "repository": "https://github.com/andreim14/beanis"
    },
    "split_keywords": [
        "redis",
        " odm",
        " orm",
        " pydantic",
        " async",
        " python"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b1a63058d3683344e7a148cf59cd6bd54715605f6c470c3fff74a55edeedb726",
                "md5": "3d307c32c76e67ff349b340de331a8cd",
                "sha256": "e57df8c9ac3ae5080ec4e1ca5235a43a85856346944d4bb090e2a3cb04ad5b64"
            },
            "downloads": -1,
            "filename": "beanis-0.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3d307c32c76e67ff349b340de331a8cd",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.9",
            "size": 47902,
            "upload_time": "2025-10-24T11:06:46",
            "upload_time_iso_8601": "2025-10-24T11:06:46.444667Z",
            "url": "https://files.pythonhosted.org/packages/b1/a6/3058d3683344e7a148cf59cd6bd54715605f6c470c3fff74a55edeedb726/beanis-0.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "205fd979084124a83d464d8bfd914195fd61651af0a9924be6b4ade63f6ddd35",
                "md5": "bb7abb58a4afea5ba863e53437d408f9",
                "sha256": "4a223462a93d05ca52f04aaa0423938d425e72d00f74e7e00029bdfabbf3bde5"
            },
            "downloads": -1,
            "filename": "beanis-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "bb7abb58a4afea5ba863e53437d408f9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.9",
            "size": 43748,
            "upload_time": "2025-10-24T11:06:47",
            "upload_time_iso_8601": "2025-10-24T11:06:47.742118Z",
            "url": "https://files.pythonhosted.org/packages/20/5f/d979084124a83d464d8bfd914195fd61651af0a9924be6b4ade63f6ddd35/beanis-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-24 11:06:47",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "andreim14",
    "github_project": "beanis",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "beanis"
}
        
Elapsed time: 2.77891s