# cache_core

Multi-backend cache abstraction layer for Python — async-first, with pluggable clients for Redis and in-memory storage.

## Installation

```bash
# Core (in-memory client — no external dependencies)
pip install cache_core

# With Redis support
pip install "cache_core[redis]"

# Or install in development mode from source
pip install -e .
```

## Quick Start

```python
import asyncio
from cache_core import CacheBuilder, create_memory_cache_client

async def main():
    cache = CacheBuilder(create_memory_cache_client())

    # Simple key-value
    await cache.key("greeting").set("hello")
    print(await cache.key("greeting").get())  # "hello"

    # Complex types auto-serialized
    await cache.key("user:1").set({"name": "Alice", "roles": ["admin"]}, ttl=3600)
    user = await cache.key("user:1").get()
    print(user["name"])  # "Alice"

asyncio.run(main())
```

## Usage Examples

### Key-Value Operations

```python
# Set and get
await cache.key("count").set(42)
count = await cache.key("count").get()          # 42 (int)

await cache.key("name").set("Alice")
name = await cache.key("name").get()            # "Alice" (str)

# TTL (time-to-live in seconds)
await cache.key("session").set("data", ttl=300)
print(await cache.key("session").ttl())         # ~300

# Existence and deletion
exists = await cache.key("count").exists()      # True
deleted = await cache.key("count").delete()     # True
deleted = await cache.key("nope").delete()      # False

# Atomic increment / decrement
await cache.key("visits").incr()                # 1
await cache.key("visits").incr(5)               # 6
await cache.key("visits").decr(2)               # 4
```

### Complex Types — Automatic JSON Serialization

Primitives (`str`, `int`, `float`, `bool`, `None`) pass through as-is. Complex types (`dict`, `list`, `set`, `tuple`) are automatically JSON-encoded on write and decoded on read.

```python
# Dicts
await cache.key("config").set({"theme": "dark", "items": [1, 2, 3]})
config = await cache.key("config").get()        # {"theme": "dark", "items": [1, 2, 3]}

# Lists
await cache.key("ids").set([10, 20, 30])
ids = await cache.key("ids").get()              # [10, 20, 30]

# Nested structures
await cache.key("nested").set({
    "users": [
        {"id": 1, "tags": ["python", "async"]},
        {"id": 2, "tags": ["rust"]},
    ]
})
```

### Namespace Scoping

Prefix keys with namespaces to organize your cache and avoid collisions.

```python
users = cache.ns("users")
posts = cache.ns("posts")

await users.key("123").set({"name": "Alice"})
await users.key("456").set({"name": "Bob"})
await posts.key("1").set({"title": "Hello World"})

# Keys: "users:123", "users:456", "posts:1"

alice = await cache.key("users:123").get()      # {"name": "Alice"}
alice = await users.key("123").get()            # same result

# Nested namespaces
blog = cache.ns("blog").ns("comments")
await blog.key("post:1").set("Great post!")      # key: "blog:comments:post:1"
```

### Lists (Arrays)

Each element is automatically serialized/deserialized individually.

```python
# Push to the right (append)
await cache.list("queue").push("task_a", "task_b", "task_c")
await cache.list("queue").push({"id": 4, "type": "email"})

# Push to the left (prepend)
await cache.list("queue").push_left("urgent")

# Pop from the right (LIFO)
item = await cache.list("queue").pop()          # {"id": 4, "type": "email"}

# Pop from the left (FIFO)
item = await cache.list("queue").pop_left()     # "urgent"

# Range and length
items = await cache.list("queue").range(0, -1)  # all items
size = await cache.list("queue").len()          # 3
```

### Sets

Each member is automatically serialized. Duplicates are ignored (deduplicated by JSON value).

```python
# Add members — duplicates are silently ignored
added = await cache.set_("tags").add(
    {"name": "python"}, {"name": "rust"}, {"name": "python"}
)
print(added)  # 2 (third was a duplicate)

# Retrieve all members
members = await cache.set_("tags").members()
# [{"name": "python"}, {"name": "rust"}]

# Membership check
has_python = await cache.set_("tags").contains({"name": "python"})  # True

# Remove members
removed = await cache.set_("tags").remove({"name": "rust"})         # 1

# Cardinality
size = await cache.set_("tags").size()          # 1
```

### Hashes (Maps)

Store field-value pairs under a single key. Each value is auto-serialized.

```python
# Bulk set fields
await cache.hash("users").set({
    "alice": '{"name":"Alice","age":30}',
    "bob": '{"name":"Bob","age":25}',
})

# Get all fields
all_users = await cache.hash("users").getall()
# {"alice": {"name": "Alice", "age": 30}, "bob": {"name": "Bob", "age": 25}}

# Single field operations
await cache.hash("users").field("alice").set('{"name":"Alice","age":31}')
user = await cache.hash("users").field("alice").get()
# {"name": "Alice", "age": 31}

exists = await cache.hash("users").field("bob").exists()   # True
deleted = await cache.hash("users").field("bob").delete()   # 1

# List fields
fields = await cache.hash("users").keys()      # ["alice"]
count = await cache.hash("users").len()         # 1
```

### Sorted Sets

Members ordered by score.

```python
# Add members with scores
await cache.sorted_set("leaderboard").add({
    "alice": 1500.0,
    "bob": 1200.0,
    "carol": 1800.0,
})

# Range by score (ascending)
top = await cache.sorted_set("leaderboard").range(0, -1)
# ["bob", "alice", "carol"]

# With scores
top_scored = await cache.sorted_set("leaderboard").range(0, -1, withscores=True)
# [("bob", 1200.0), ("alice", 1500.0), ("carol", 1800.0)]

# Remove and size
await cache.sorted_set("leaderboard").remove("bob")
size = await cache.sorted_set("leaderboard").size()   # 2
```

### Pattern-Based Batch Operations

Delete or list keys matching a glob pattern.

```python
# Seed some keys
await cache.key("session:a").set("x")
await cache.key("session:b").set("y")
await cache.key("cache:config").set("z")

# Count matching keys
count = await cache.pattern("session:*").count()   # 2

# List matching keys
keys = await cache.pattern("session:*").keys()     # ["session:a", "session:b"]

# Batch delete
deleted = await cache.pattern("session:*").delete()  # 2
```

### Async Context Manager

```python
from cache_core import CacheBuilder, create_memory_cache_client

async with CacheBuilder(create_memory_cache_client()) as cache:
    await cache.key("temp").set("value")
    # ... do work ...
# Automatically disconnects on exit
```

## Complete Usage Example

```python
import asyncio
from cache_core import CacheBuilder, create_memory_cache_client


async def main():
    # Create client and builder (in-memory for testing; swap for Redis in production)
    cache = CacheBuilder(create_memory_cache_client())

    # 1. Cache a user profile with TTL
    user = {"id": 1, "name": "Alice", "email": "alice@example.com", "roles": ["admin"]}
    await cache.key("user:1").set(user, ttl=3600)

    # 2. Maintain a task queue
    tasks = cache.list("queue:pending")
    await tasks.push(
        {"task_id": "t1", "action": "send_email", "to": "alice@example.com"},
        {"task_id": "t2", "action": "generate_report", "user_id": 1},
    )

    # 3. Track unique tags for a post
    post_tags = cache.set_("post:42:tags")
    await post_tags.add("python", "async", "caching")
    await post_tags.add("python")  # duplicate — ignored

    # 4. Store configuration as a hash
    config = cache.hash("app:config")
    await config.set({
        "theme": "dark",
        "language": "en",
        "features": '{"search":true,"chat":false}',
    })

    # 5. Leaderboard with scores
    lb = cache.sorted_set("game:scores")
    await lb.add({"alice": 9500, "bob": 8200, "carol": 9900})

    # --- Read back ---
    print("User:", await cache.key("user:1").get())
    # User: {'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'roles': ['admin']}

    print("Next task:", await tasks.pop())
    # Next task: {'task_id': 't2', 'action': 'generate_report', 'user_id': 1}

    print("Tags:", await post_tags.members())
    # Tags: ['python', 'async', 'caching']

    print("Config:", await config.getall())
    # Config: {'theme': 'dark', 'language': 'en', 'features': {'search': True, 'chat': False}}

    print("Leaderboard:", await lb.range(0, -1, withscores=True))
    # Leaderboard: [('bob', 8200.0), ('alice', 9500.0), ('carol', 9900.0)]

    # 6. Cleanup session keys
    print("Session keys cleaned:", await cache.pattern("session:*").delete())

    # 7. Flush everything
    await cache.flush()


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

## Redis Backend

```python
import os
from cache_core import CacheBuilder, create_redis_cache_client

# From URL
client = create_redis_cache_client(url="redis://localhost:6379/0")

# From environment variables: REDIS_URL, REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_PASSWORD
client = create_redis_cache_client()

cache = CacheBuilder(client)
await cache.key("hello").set("world")
```

## Custom Serializer

```python
from cache_core import CacheBuilder, PickleSerializer, PassthroughSerializer, create_memory_cache_client

# Pickle — preserves full Python objects (Python-only)
cache = CacheBuilder(create_memory_cache_client(), serializer=PickleSerializer())

# Pass-through — no serialization, caller handles it
cache = CacheBuilder(create_memory_cache_client(), serializer=PassthroughSerializer())
```

## API Reference

### `CacheBuilder`

| Method             | Returns              | Description                         |
| ------------------ | -------------------- | ----------------------------------- |
| `key(name)`        | `KeyOperation`       | Target a single key                 |
| `hash(name)`       | `HashOperation`      | Target a hash (map)                 |
| `list(name)`       | `ListOperation`      | Target a list (array)               |
| `set_(name)`       | `SetOperation`       | Target a set                        |
| `sorted_set(name)` | `SortedSetOperation` | Target a sorted set                 |
| `pattern(glob)`    | `PatternOperation`   | Target keys matching a glob pattern |
| `ns(namespace)`    | `CacheBuilder`       | Scope to a namespace prefix         |
| `connect()`        | `None`               | Open backend connections            |
| `disconnect()`     | `None`               | Close backend connections           |
| `ping()`           | `bool`               | Health check                        |
| `flush()`          | `None`               | Remove all keys                     |

### `KeyOperation`

| Method                 | Returns | Description                     |
| ---------------------- | ------- | ------------------------------- |
| `get()`                | `Any`   | Get the value                   |
| `set(value, ttl=None)` | `None`  | Set the value with optional TTL |
| `delete()`             | `bool`  | Delete the key                  |
| `exists()`             | `bool`  | Check if key exists             |
| `expire(ttl)`          | `bool`  | Set TTL on existing key         |
| `ttl()`                | `int`   | Get remaining TTL (-1 if none)  |
| `incr(amount=1)`       | `int`   | Increment integer value         |
| `decr(amount=1)`       | `int`   | Decrement integer value         |

### `ListOperation`

| Method               | Returns | Description                 |
| -------------------- | ------- | --------------------------- |
| `push(*values)`      | `int`   | Append to right (tail)      |
| `push_left(*values)` | `int`   | Prepend to left (head)      |
| `pop()`              | `Any`   | Remove and return rightmost |
| `pop_left()`         | `Any`   | Remove and return leftmost  |
| `range(start, stop)` | `list`  | Slice of the list           |
| `len()`              | `int`   | Number of elements          |

### `SetOperation`

| Method             | Returns | Description                           |
| ------------------ | ------- | ------------------------------------- |
| `add(*members)`    | `int`   | Add members, returns count added      |
| `remove(*members)` | `int`   | Remove members, returns count removed |
| `members()`        | `list`  | All members                           |
| `contains(member)` | `bool`  | Membership test                       |
| `size()`           | `int`   | Cardinality                           |

### `HashOperation`

| Method            | Returns              | Description             |
| ----------------- | -------------------- | ----------------------- |
| `field(name)`     | `HashFieldOperation` | Target a specific field |
| `set(mapping)`    | `int`                | Bulk set fields         |
| `getall()`        | `dict`               | All fields and values   |
| `delete(*fields)` | `int`                | Delete fields           |
| `keys()`          | `list`               | All field names         |
| `len()`           | `int`                | Number of fields        |

### `HashFieldOperation`

| Method       | Returns | Description            |
| ------------ | ------- | ---------------------- |
| `get()`      | `Any`   | Get the field value    |
| `set(value)` | `int`   | Set the field value    |
| `getall()`   | `dict`  | All fields in the hash |
| `delete()`   | `int`   | Delete this field      |
| `exists()`   | `bool`  | Check if field exists  |

### `SortedSetOperation`

| Method                                 | Returns | Description             |
| -------------------------------------- | ------- | ----------------------- |
| `add(mapping)`                         | `int`   | Add members with scores |
| `remove(*members)`                     | `int`   | Remove members          |
| `range(start, stop, withscores=False)` | `list`  | Members by score        |
| `size()`                               | `int`   | Cardinality             |

### `PatternOperation`

| Method     | Returns | Description              |
| ---------- | ------- | ------------------------ |
| `delete()` | `int`   | Delete all matching keys |
| `keys()`   | `list`  | List matching keys       |
| `count()`  | `int`   | Count matching keys      |
