Metadata-Version: 2.2
Name: ichido
Version: 0.1.0
Summary: Stripe-inspired idempotency key middleware for FastAPI — exactly-once request processing.
Author: Akhil
License: MIT
Project-URL: Homepage, https://github.com/akhil/ichido
Project-URL: Repository, https://github.com/akhil/ichido
Project-URL: Issues, https://github.com/akhil/ichido/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.100.0
Requires-Dist: redis>=5.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: fakeredis>=2.21; extra == "dev"
Requires-Dist: uvicorn>=0.30; extra == "dev"

# Ichido (一度) — "One Time"

[![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688.svg)](https://fastapi.tiangolo.com)
[![Redis](https://img.shields.io/badge/Redis-5.0%2B-red.svg)](https://redis.io)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Ichido** is a drop-in, production-grade idempotency key middleware for **FastAPI**, backed by **Redis**. It guarantees exactly-once processing on unsafe HTTP endpoints (like payments, charge actions, or refunds) by ensuring retry requests with the same idempotency key are safely replayed or gracefully throttled.

It implements the core idempotency specifications used by leading API platforms like **Stripe** and **Razorpay**.

---

## 💡 Why Ichido? (Motivation & Business Value)

In distributed systems, the network is fundamentally unreliable. When a client makes an API request (e.g., to charge a credit card) and the network drops before the server can return a response, the client is left in an ambiguous state: **Did the payment succeed or fail?**

To recover, the client must retry the request. However, naive retries on non-idempotent endpoints (like `POST /charges`) lead to the **double-charging problem** (charging a user twice for a single order).

```
[ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment successfully)
[ Client ] ◄─── (Network drops / Timeout) ─── [ Server ] (Response lost)
                  
            *Client retries the request*
            
[ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment AGAIN - Double Charge!)
```

### The Solution: Exactly-Once Semantics
**Ichido** intercepts retry requests before they execute any business logic. By deduplicating requests on the server side using a unique client-generated header token (`Idempotency-Key`), it achieves **effectively-exactly-once execution**:

- **Card Charge Protection**: Prevents duplicate financial transactions, keeping disputes and chargebacks low.
- **Resource Protection**: Stops redundant database writes and expensive third-party API executions (e.g., mail sending, billing endpoints).
- **Robust Client Retry Loops**: Allows client software to safely retry requests aggressively with exponential backoff, without developers having to implement complex transaction checks manually at the application layer.

---

## 🏗️ Architecture & State Machine

```
              Client Request (Idempotency-Key)
                            │
                            ▼
              Atomically claim key (SET NX EX)
               /                            \
        [Lock Acquired]               [Key Already Exists]
             /                                \
            ▼                                  ▼
      State: PROCESSING                 Get stored record
            │                          /                 \
    Run endpoint logic        State: PROCESSING     State: COMPLETED
            │                        │                        │
       [Completion]           Return 409 Conflict    Compare fingerprint
      /            \          (Retry-After: 1)       /                 \
  Success         Failure                          Match             Mismatch
    /                \                              /                     \
Update to          DELETE key                      ▼                       ▼
COMPLETED        (Allow retry)               Replay cached         Return 422 Error
with response                                  response            (Key reused for
                                                                   different body)
```

### Redis Record JSON Schema
Each record is serialized to JSON and stored with a namespace prefix (`ichido:`). When in the `completed` state, the response body is stored using base64 encoding to support binary and text payloads cleanly.

```json
{
  "status": "processing" | "completed",
  "fingerprint": "sha256_hash_value",
  "response_status": 200,
  "response_headers": {
    "content-type": "application/json",
    "x-custom-header": "value"
  },
  "response_body": "eyJhIjoxLCJiIjoyfQ==" // Base64 encoded payload
}
```

---

## 🛠️ File Structure

```
Ichido-Idempotency/
├── pyproject.toml              # Package config and metadata
├── LICENSE                     # MIT license
├── README.md                   # You are here
├── src/
│   └── ichido/
│       ├── __init__.py         # Public exports (Middleware, Config, Store)
│       ├── config.py           # Frozen configuration dataclass
│       ├── exceptions.py       # Library internal exceptions
│       ├── fingerprint.py      # Request body canonicalizer & SHA256 hasher
│       ├── middleware.py       # Pure ASGI middleware handling request/response flows
│       └── storage.py          # Redis connection client wrapper & record schema
├── tests/
│   ├── conftest.py             # pytest setup with mock FastAPI & fakeredis client
│   ├── test_concurrency.py      # Multi-task concurrent execution tests
│   ├── test_fingerprint.py      # JSON sorting & fingerprint verification
│   ├── test_middleware.py       # Standard request lifecycle tests
│   └── test_storage.py          # Redis store serialization tests
└── examples/
    ├── demo_app.py             # Runnable local FastAPI server with fallback support
    └── demo_concurrent.py      # Client script firing concurrent/sequential request scenarios
```

---

## ⚙️ Configuration Reference

Use `IchidoConfig` to customize the middleware's behavior:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `header_name` | `str` | `"Idempotency-Key"` | The HTTP request header checked by the middleware. |
| `ttl_seconds` | `int` | `86400` (24 Hours) | Time-to-Live in seconds for the cached responses in Redis. |
| `enforce_methods` | `tuple[str, ...]` | `("POST", "PATCH", "PUT")` | HTTP methods that will run through the idempotency filter. GET/DELETE are naturally idempotent. |
| `key_prefix` | `str` | `"ichido:"` | Namespace prefix prepended to all Redis keys to avoid collisions. |

---

## 🚀 Quick Start

### 1. Installation

Install the package directly in editable development mode:

```bash
pip install -e ".[dev]"
```

### 2. Basic Integration

```python
import redis.asyncio as aioredis
from fastapi import FastAPI
from ichido import IchidoMiddleware, RedisIdempotencyStore, IchidoConfig

app = FastAPI()

# 1. Initialize Redis connection pool
redis_client = aioredis.from_url("redis://localhost:6379/0", decode_responses=True)

# 2. Setup the idempotency storage backend
store = RedisIdempotencyStore(redis_client=redis_client, ttl=86400)

# 3. Add the middleware to FastAPI
app.add_middleware(
    IchidoMiddleware,
    store=store,
    config=IchidoConfig(
        header_name="Idempotency-Key",
        ttl_seconds=86400 # 24 Hours
    )
)

@app.post("/payments/charge")
async def process_payment(amount: float):
    # This logic is guaranteed to execute exactly once for any unique Idempotency-Key header.
    return {"status": "success", "charge_id": "ch_12345"}
```

---

## 🧪 Testing & Live Demo

Ichido includes an automated suite and an end-to-end sandbox application.

### Running Unit Tests

To run the unit tests (which automatically mock Redis using `fakeredis`):

```bash
python -m pytest tests/ -v
```

---

### Running the Live Sandbox Demo

You can run the live demo either locally (via a mock store) or connected to a **free Cloud Redis** provider (like Upstash).

#### Option A: Zero Configuration (In-Memory Mock)
If you don't have Redis installed, the demo app will automatically fall back to an in-memory `fakeredis` database.

1. **Start the API Server**:
   ```bash
   uvicorn examples.demo_app:app --port 8000 --reload
   ```
2. **Execute the Simulator Client** in another terminal:
   ```bash
   python examples.demo_concurrent.py
   ```

#### Option B: Connected to a Cloud Redis Server (e.g. Upstash)
1. Go to [Upstash](https://console.upstash.com/) and create a free serverless database.
2. Copy the connection string under **Redis URL** (`rediss://default:...`).
3. **Start the API Server** with the environment variable set:
   ```bash
   # Windows PowerShell
   $env:REDIS_URL="rediss://default:yourpassword@your-endpoint.upstash.io:6379"
   uvicorn examples.demo_app:app --port 8000 --reload
   ```
4. **Execute the Simulator Client**:
   ```bash
   python examples.demo_concurrent.py
   ```

---

### Expected Console Output Trace

When executing `demo_concurrent.py`, the client output demonstrates the state transitions:

```text
======================================================================
DEMO 1: CONCURRENT DUPLICATE REQUESTS (State: PROCESSING)
Idempotency Key: 2391c028-804f-460f-b1f2-1e3e4110b127
Firing 2 requests simultaneously. The server takes ~2.5s to process...
======================================================================

Response #1:
  Status Code: 200
  Headers:
    X-Ichido-Status: executed
    Retry-After: None
  Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}

Response #2:
  Status Code: 409
  Headers:
    X-Ichido-Status: None
    Retry-After: 1
  Body: {"error":"request_in_progress","message":"A request with idempotency key '2391c028-804f-460f-b1f2-1e3e4110b127' is currently being processed.Please retry later."}

======================================================================
DEMO 2: REPLAYED REQUEST (State: COMPLETED)
Firing a 3rd request with the SAME key and SAME body...
======================================================================

Response #3 (Replayed):
  Status Code: 200
  Headers:
    X-Ichido-Status: replayed
  Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}

======================================================================
DEMO 3: REQUEST PARAMETER MISMATCH (State: COMPLETED, Different Body)
Firing a 4th request with the SAME key but a DIFFERENT amount ($99.99)...
======================================================================

Response #4 (Mismatch):
  Status Code: 422
  Body: {"error":"fingerprint_mismatch","message":"Idempotency key '2391c028-804f-460f-b1f2-1e3e4110b127' was already used with different request parameters. Each key must be used with identical requests."}
```

---

## 💡 Interview Q&A / Architecture Defense

Be ready to explain these core distributed systems design decisions:

### 1. Why Redis instead of PostgreSQL for Idempotency?
- **Speed**: Idempotency checks sit in the critical hot path of payments APIs. Redis is an in-memory datastore that offers sub-millisecond lookups. 
- **Auto Expiration**: Redis provides built-in Time-to-Live (TTL) key expiration natively. Relational databases require custom cron tasks, worker scripts, or database trigger cleanups to delete outdated idempotency keys.
- **Atomic Operations**: Redis executes commands single-threaded, allowing us to perform atomic `SET NX EX` locks in a single roundtrip.
- **Tradeoff**: Redis is not durable. If a node crashes, in-flight keys are lost. In a massive scale production payments app, you would use Redis for the fast concurrency lock and short-term caching, backed by a persistent relational database (Postgres) as the source-of-truth for completed transactions.

### 2. How is the concurrent duplicate request problem solved?
- When two identical requests with the same key arrive simultaneously, both attempt a Redis `SET key value NX EX TTL`.
- Exactly one request succeeds (receiving the lock) and proceeds to run the downstream endpoint logic, setting the state to `processing`.
- The second request fails to write the key, reads the current state as `processing`, and immediately aborts, returning `409 Conflict` with a `Retry-After: 1` header.
- **Tradeoff vs. Polling**: We return `409` instead of holding the second connection open and polling. Holding connections open consumes web server threads/workers. Under load, this could exhaust the worker pool and lead to a server-wide denial of service. Pushing the retry logic to the client is significantly more resilient.

### 3. Why did you choose pure ASGI middleware instead of Starlette's `BaseHTTPMiddleware`?
- Starlette's `BaseHTTPMiddleware` wraps requests in separate background threads using `anyio`. This adds substantial latency overhead, triggers memory leaks, and causes socket disconnect errors under high-concurrency request streams.
- Pure ASGI middleware directly implements `__call__(scope, receive, send)`. This gives us raw access to ASGI request streams and response chunks without threading overhead, which is critical for high-throughput gateway services.

### 4. What is At-Least-Once vs. Exactly-Once processing?
- **At-Least-Once**: The network is unreliable. If a request times out, the client retries. Without server-side deduplication, this results in duplicate side effects (e.g. charging a card twice).
- **Exactly-Once**: Combining client-side retries (At-Least-Once delivery) with server-side deduplication (using idempotency keys) achieves **Exactly-Once** execution. The action occurs exactly once, even if the request was sent multiple times.

---

## 📄 License

Distributed under the MIT License. See `LICENSE` for details.
