ichido 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ichido-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akhil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ichido-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.2
2
+ Name: ichido
3
+ Version: 0.1.0
4
+ Summary: Stripe-inspired idempotency key middleware for FastAPI β€” exactly-once request processing.
5
+ Author: Akhil
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/akhil/ichido
8
+ Project-URL: Repository, https://github.com/akhil/ichido
9
+ Project-URL: Issues, https://github.com/akhil/ichido/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: fastapi>=0.100.0
23
+ Requires-Dist: redis>=5.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
27
+ Requires-Dist: httpx>=0.27; extra == "dev"
28
+ Requires-Dist: fakeredis>=2.21; extra == "dev"
29
+ Requires-Dist: uvicorn>=0.30; extra == "dev"
30
+
31
+ # Ichido (δΈ€εΊ¦) β€” "One Time"
32
+
33
+ [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
34
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688.svg)](https://fastapi.tiangolo.com)
35
+ [![Redis](https://img.shields.io/badge/Redis-5.0%2B-red.svg)](https://redis.io)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+
38
+ **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.
39
+
40
+ It implements the core idempotency specifications used by leading API platforms like **Stripe** and **Razorpay**.
41
+
42
+ ---
43
+
44
+ ## πŸ’‘ Why Ichido? (Motivation & Business Value)
45
+
46
+ 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?**
47
+
48
+ 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).
49
+
50
+ ```
51
+ [ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment successfully)
52
+ [ Client ] ◄─── (Network drops / Timeout) ─── [ Server ] (Response lost)
53
+
54
+ *Client retries the request*
55
+
56
+ [ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment AGAIN - Double Charge!)
57
+ ```
58
+
59
+ ### The Solution: Exactly-Once Semantics
60
+ **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**:
61
+
62
+ - **Card Charge Protection**: Prevents duplicate financial transactions, keeping disputes and chargebacks low.
63
+ - **Resource Protection**: Stops redundant database writes and expensive third-party API executions (e.g., mail sending, billing endpoints).
64
+ - **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.
65
+
66
+ ---
67
+
68
+ ## πŸ—οΈ Architecture & State Machine
69
+
70
+ ```
71
+ Client Request (Idempotency-Key)
72
+ β”‚
73
+ β–Ό
74
+ Atomically claim key (SET NX EX)
75
+ / \
76
+ [Lock Acquired] [Key Already Exists]
77
+ / \
78
+ β–Ό β–Ό
79
+ State: PROCESSING Get stored record
80
+ β”‚ / \
81
+ Run endpoint logic State: PROCESSING State: COMPLETED
82
+ β”‚ β”‚ β”‚
83
+ [Completion] Return 409 Conflict Compare fingerprint
84
+ / \ (Retry-After: 1) / \
85
+ Success Failure Match Mismatch
86
+ / \ / \
87
+ Update to DELETE key β–Ό β–Ό
88
+ COMPLETED (Allow retry) Replay cached Return 422 Error
89
+ with response response (Key reused for
90
+ different body)
91
+ ```
92
+
93
+ ### Redis Record JSON Schema
94
+ 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.
95
+
96
+ ```json
97
+ {
98
+ "status": "processing" | "completed",
99
+ "fingerprint": "sha256_hash_value",
100
+ "response_status": 200,
101
+ "response_headers": {
102
+ "content-type": "application/json",
103
+ "x-custom-header": "value"
104
+ },
105
+ "response_body": "eyJhIjoxLCJiIjoyfQ==" // Base64 encoded payload
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## πŸ› οΈ File Structure
112
+
113
+ ```
114
+ Ichido-Idempotency/
115
+ β”œβ”€β”€ pyproject.toml # Package config and metadata
116
+ β”œβ”€β”€ LICENSE # MIT license
117
+ β”œβ”€β”€ README.md # You are here
118
+ β”œβ”€β”€ src/
119
+ β”‚ └── ichido/
120
+ β”‚ β”œβ”€β”€ __init__.py # Public exports (Middleware, Config, Store)
121
+ β”‚ β”œβ”€β”€ config.py # Frozen configuration dataclass
122
+ β”‚ β”œβ”€β”€ exceptions.py # Library internal exceptions
123
+ β”‚ β”œβ”€β”€ fingerprint.py # Request body canonicalizer & SHA256 hasher
124
+ β”‚ β”œβ”€β”€ middleware.py # Pure ASGI middleware handling request/response flows
125
+ β”‚ └── storage.py # Redis connection client wrapper & record schema
126
+ β”œβ”€β”€ tests/
127
+ β”‚ β”œβ”€β”€ conftest.py # pytest setup with mock FastAPI & fakeredis client
128
+ β”‚ β”œβ”€β”€ test_concurrency.py # Multi-task concurrent execution tests
129
+ β”‚ β”œβ”€β”€ test_fingerprint.py # JSON sorting & fingerprint verification
130
+ β”‚ β”œβ”€β”€ test_middleware.py # Standard request lifecycle tests
131
+ β”‚ └── test_storage.py # Redis store serialization tests
132
+ └── examples/
133
+ β”œβ”€β”€ demo_app.py # Runnable local FastAPI server with fallback support
134
+ └── demo_concurrent.py # Client script firing concurrent/sequential request scenarios
135
+ ```
136
+
137
+ ---
138
+
139
+ ## βš™οΈ Configuration Reference
140
+
141
+ Use `IchidoConfig` to customize the middleware's behavior:
142
+
143
+ | Parameter | Type | Default | Description |
144
+ |---|---|---|---|
145
+ | `header_name` | `str` | `"Idempotency-Key"` | The HTTP request header checked by the middleware. |
146
+ | `ttl_seconds` | `int` | `86400` (24 Hours) | Time-to-Live in seconds for the cached responses in Redis. |
147
+ | `enforce_methods` | `tuple[str, ...]` | `("POST", "PATCH", "PUT")` | HTTP methods that will run through the idempotency filter. GET/DELETE are naturally idempotent. |
148
+ | `key_prefix` | `str` | `"ichido:"` | Namespace prefix prepended to all Redis keys to avoid collisions. |
149
+
150
+ ---
151
+
152
+ ## πŸš€ Quick Start
153
+
154
+ ### 1. Installation
155
+
156
+ Install the package directly in editable development mode:
157
+
158
+ ```bash
159
+ pip install -e ".[dev]"
160
+ ```
161
+
162
+ ### 2. Basic Integration
163
+
164
+ ```python
165
+ import redis.asyncio as aioredis
166
+ from fastapi import FastAPI
167
+ from ichido import IchidoMiddleware, RedisIdempotencyStore, IchidoConfig
168
+
169
+ app = FastAPI()
170
+
171
+ # 1. Initialize Redis connection pool
172
+ redis_client = aioredis.from_url("redis://localhost:6379/0", decode_responses=True)
173
+
174
+ # 2. Setup the idempotency storage backend
175
+ store = RedisIdempotencyStore(redis_client=redis_client, ttl=86400)
176
+
177
+ # 3. Add the middleware to FastAPI
178
+ app.add_middleware(
179
+ IchidoMiddleware,
180
+ store=store,
181
+ config=IchidoConfig(
182
+ header_name="Idempotency-Key",
183
+ ttl_seconds=86400 # 24 Hours
184
+ )
185
+ )
186
+
187
+ @app.post("/payments/charge")
188
+ async def process_payment(amount: float):
189
+ # This logic is guaranteed to execute exactly once for any unique Idempotency-Key header.
190
+ return {"status": "success", "charge_id": "ch_12345"}
191
+ ```
192
+
193
+ ---
194
+
195
+ ## πŸ§ͺ Testing & Live Demo
196
+
197
+ Ichido includes an automated suite and an end-to-end sandbox application.
198
+
199
+ ### Running Unit Tests
200
+
201
+ To run the unit tests (which automatically mock Redis using `fakeredis`):
202
+
203
+ ```bash
204
+ python -m pytest tests/ -v
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Running the Live Sandbox Demo
210
+
211
+ You can run the live demo either locally (via a mock store) or connected to a **free Cloud Redis** provider (like Upstash).
212
+
213
+ #### Option A: Zero Configuration (In-Memory Mock)
214
+ If you don't have Redis installed, the demo app will automatically fall back to an in-memory `fakeredis` database.
215
+
216
+ 1. **Start the API Server**:
217
+ ```bash
218
+ uvicorn examples.demo_app:app --port 8000 --reload
219
+ ```
220
+ 2. **Execute the Simulator Client** in another terminal:
221
+ ```bash
222
+ python examples.demo_concurrent.py
223
+ ```
224
+
225
+ #### Option B: Connected to a Cloud Redis Server (e.g. Upstash)
226
+ 1. Go to [Upstash](https://console.upstash.com/) and create a free serverless database.
227
+ 2. Copy the connection string under **Redis URL** (`rediss://default:...`).
228
+ 3. **Start the API Server** with the environment variable set:
229
+ ```bash
230
+ # Windows PowerShell
231
+ $env:REDIS_URL="rediss://default:yourpassword@your-endpoint.upstash.io:6379"
232
+ uvicorn examples.demo_app:app --port 8000 --reload
233
+ ```
234
+ 4. **Execute the Simulator Client**:
235
+ ```bash
236
+ python examples.demo_concurrent.py
237
+ ```
238
+
239
+ ---
240
+
241
+ ### Expected Console Output Trace
242
+
243
+ When executing `demo_concurrent.py`, the client output demonstrates the state transitions:
244
+
245
+ ```text
246
+ ======================================================================
247
+ DEMO 1: CONCURRENT DUPLICATE REQUESTS (State: PROCESSING)
248
+ Idempotency Key: 2391c028-804f-460f-b1f2-1e3e4110b127
249
+ Firing 2 requests simultaneously. The server takes ~2.5s to process...
250
+ ======================================================================
251
+
252
+ Response #1:
253
+ Status Code: 200
254
+ Headers:
255
+ X-Ichido-Status: executed
256
+ Retry-After: None
257
+ Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}
258
+
259
+ Response #2:
260
+ Status Code: 409
261
+ Headers:
262
+ X-Ichido-Status: None
263
+ Retry-After: 1
264
+ Body: {"error":"request_in_progress","message":"A request with idempotency key '2391c028-804f-460f-b1f2-1e3e4110b127' is currently being processed.Please retry later."}
265
+
266
+ ======================================================================
267
+ DEMO 2: REPLAYED REQUEST (State: COMPLETED)
268
+ Firing a 3rd request with the SAME key and SAME body...
269
+ ======================================================================
270
+
271
+ Response #3 (Replayed):
272
+ Status Code: 200
273
+ Headers:
274
+ X-Ichido-Status: replayed
275
+ Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}
276
+
277
+ ======================================================================
278
+ DEMO 3: REQUEST PARAMETER MISMATCH (State: COMPLETED, Different Body)
279
+ Firing a 4th request with the SAME key but a DIFFERENT amount ($99.99)...
280
+ ======================================================================
281
+
282
+ Response #4 (Mismatch):
283
+ Status Code: 422
284
+ 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."}
285
+ ```
286
+
287
+ ---
288
+
289
+ ## πŸ’‘ Interview Q&A / Architecture Defense
290
+
291
+ Be ready to explain these core distributed systems design decisions:
292
+
293
+ ### 1. Why Redis instead of PostgreSQL for Idempotency?
294
+ - **Speed**: Idempotency checks sit in the critical hot path of payments APIs. Redis is an in-memory datastore that offers sub-millisecond lookups.
295
+ - **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.
296
+ - **Atomic Operations**: Redis executes commands single-threaded, allowing us to perform atomic `SET NX EX` locks in a single roundtrip.
297
+ - **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.
298
+
299
+ ### 2. How is the concurrent duplicate request problem solved?
300
+ - When two identical requests with the same key arrive simultaneously, both attempt a Redis `SET key value NX EX TTL`.
301
+ - Exactly one request succeeds (receiving the lock) and proceeds to run the downstream endpoint logic, setting the state to `processing`.
302
+ - 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.
303
+ - **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.
304
+
305
+ ### 3. Why did you choose pure ASGI middleware instead of Starlette's `BaseHTTPMiddleware`?
306
+ - 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.
307
+ - 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.
308
+
309
+ ### 4. What is At-Least-Once vs. Exactly-Once processing?
310
+ - **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).
311
+ - **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.
312
+
313
+ ---
314
+
315
+ ## πŸ“„ License
316
+
317
+ Distributed under the MIT License. See `LICENSE` for details.
ichido-0.1.0/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # Ichido (δΈ€εΊ¦) β€” "One Time"
2
+
3
+ [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
4
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688.svg)](https://fastapi.tiangolo.com)
5
+ [![Redis](https://img.shields.io/badge/Redis-5.0%2B-red.svg)](https://redis.io)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ **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.
9
+
10
+ It implements the core idempotency specifications used by leading API platforms like **Stripe** and **Razorpay**.
11
+
12
+ ---
13
+
14
+ ## πŸ’‘ Why Ichido? (Motivation & Business Value)
15
+
16
+ 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?**
17
+
18
+ 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).
19
+
20
+ ```
21
+ [ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment successfully)
22
+ [ Client ] ◄─── (Network drops / Timeout) ─── [ Server ] (Response lost)
23
+
24
+ *Client retries the request*
25
+
26
+ [ Client ] ───────── POST /charge ─────────► [ Server ] (Processes payment AGAIN - Double Charge!)
27
+ ```
28
+
29
+ ### The Solution: Exactly-Once Semantics
30
+ **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**:
31
+
32
+ - **Card Charge Protection**: Prevents duplicate financial transactions, keeping disputes and chargebacks low.
33
+ - **Resource Protection**: Stops redundant database writes and expensive third-party API executions (e.g., mail sending, billing endpoints).
34
+ - **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.
35
+
36
+ ---
37
+
38
+ ## πŸ—οΈ Architecture & State Machine
39
+
40
+ ```
41
+ Client Request (Idempotency-Key)
42
+ β”‚
43
+ β–Ό
44
+ Atomically claim key (SET NX EX)
45
+ / \
46
+ [Lock Acquired] [Key Already Exists]
47
+ / \
48
+ β–Ό β–Ό
49
+ State: PROCESSING Get stored record
50
+ β”‚ / \
51
+ Run endpoint logic State: PROCESSING State: COMPLETED
52
+ β”‚ β”‚ β”‚
53
+ [Completion] Return 409 Conflict Compare fingerprint
54
+ / \ (Retry-After: 1) / \
55
+ Success Failure Match Mismatch
56
+ / \ / \
57
+ Update to DELETE key β–Ό β–Ό
58
+ COMPLETED (Allow retry) Replay cached Return 422 Error
59
+ with response response (Key reused for
60
+ different body)
61
+ ```
62
+
63
+ ### Redis Record JSON Schema
64
+ 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.
65
+
66
+ ```json
67
+ {
68
+ "status": "processing" | "completed",
69
+ "fingerprint": "sha256_hash_value",
70
+ "response_status": 200,
71
+ "response_headers": {
72
+ "content-type": "application/json",
73
+ "x-custom-header": "value"
74
+ },
75
+ "response_body": "eyJhIjoxLCJiIjoyfQ==" // Base64 encoded payload
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## πŸ› οΈ File Structure
82
+
83
+ ```
84
+ Ichido-Idempotency/
85
+ β”œβ”€β”€ pyproject.toml # Package config and metadata
86
+ β”œβ”€β”€ LICENSE # MIT license
87
+ β”œβ”€β”€ README.md # You are here
88
+ β”œβ”€β”€ src/
89
+ β”‚ └── ichido/
90
+ β”‚ β”œβ”€β”€ __init__.py # Public exports (Middleware, Config, Store)
91
+ β”‚ β”œβ”€β”€ config.py # Frozen configuration dataclass
92
+ β”‚ β”œβ”€β”€ exceptions.py # Library internal exceptions
93
+ β”‚ β”œβ”€β”€ fingerprint.py # Request body canonicalizer & SHA256 hasher
94
+ β”‚ β”œβ”€β”€ middleware.py # Pure ASGI middleware handling request/response flows
95
+ β”‚ └── storage.py # Redis connection client wrapper & record schema
96
+ β”œβ”€β”€ tests/
97
+ β”‚ β”œβ”€β”€ conftest.py # pytest setup with mock FastAPI & fakeredis client
98
+ β”‚ β”œβ”€β”€ test_concurrency.py # Multi-task concurrent execution tests
99
+ β”‚ β”œβ”€β”€ test_fingerprint.py # JSON sorting & fingerprint verification
100
+ β”‚ β”œβ”€β”€ test_middleware.py # Standard request lifecycle tests
101
+ β”‚ └── test_storage.py # Redis store serialization tests
102
+ └── examples/
103
+ β”œβ”€β”€ demo_app.py # Runnable local FastAPI server with fallback support
104
+ └── demo_concurrent.py # Client script firing concurrent/sequential request scenarios
105
+ ```
106
+
107
+ ---
108
+
109
+ ## βš™οΈ Configuration Reference
110
+
111
+ Use `IchidoConfig` to customize the middleware's behavior:
112
+
113
+ | Parameter | Type | Default | Description |
114
+ |---|---|---|---|
115
+ | `header_name` | `str` | `"Idempotency-Key"` | The HTTP request header checked by the middleware. |
116
+ | `ttl_seconds` | `int` | `86400` (24 Hours) | Time-to-Live in seconds for the cached responses in Redis. |
117
+ | `enforce_methods` | `tuple[str, ...]` | `("POST", "PATCH", "PUT")` | HTTP methods that will run through the idempotency filter. GET/DELETE are naturally idempotent. |
118
+ | `key_prefix` | `str` | `"ichido:"` | Namespace prefix prepended to all Redis keys to avoid collisions. |
119
+
120
+ ---
121
+
122
+ ## πŸš€ Quick Start
123
+
124
+ ### 1. Installation
125
+
126
+ Install the package directly in editable development mode:
127
+
128
+ ```bash
129
+ pip install -e ".[dev]"
130
+ ```
131
+
132
+ ### 2. Basic Integration
133
+
134
+ ```python
135
+ import redis.asyncio as aioredis
136
+ from fastapi import FastAPI
137
+ from ichido import IchidoMiddleware, RedisIdempotencyStore, IchidoConfig
138
+
139
+ app = FastAPI()
140
+
141
+ # 1. Initialize Redis connection pool
142
+ redis_client = aioredis.from_url("redis://localhost:6379/0", decode_responses=True)
143
+
144
+ # 2. Setup the idempotency storage backend
145
+ store = RedisIdempotencyStore(redis_client=redis_client, ttl=86400)
146
+
147
+ # 3. Add the middleware to FastAPI
148
+ app.add_middleware(
149
+ IchidoMiddleware,
150
+ store=store,
151
+ config=IchidoConfig(
152
+ header_name="Idempotency-Key",
153
+ ttl_seconds=86400 # 24 Hours
154
+ )
155
+ )
156
+
157
+ @app.post("/payments/charge")
158
+ async def process_payment(amount: float):
159
+ # This logic is guaranteed to execute exactly once for any unique Idempotency-Key header.
160
+ return {"status": "success", "charge_id": "ch_12345"}
161
+ ```
162
+
163
+ ---
164
+
165
+ ## πŸ§ͺ Testing & Live Demo
166
+
167
+ Ichido includes an automated suite and an end-to-end sandbox application.
168
+
169
+ ### Running Unit Tests
170
+
171
+ To run the unit tests (which automatically mock Redis using `fakeredis`):
172
+
173
+ ```bash
174
+ python -m pytest tests/ -v
175
+ ```
176
+
177
+ ---
178
+
179
+ ### Running the Live Sandbox Demo
180
+
181
+ You can run the live demo either locally (via a mock store) or connected to a **free Cloud Redis** provider (like Upstash).
182
+
183
+ #### Option A: Zero Configuration (In-Memory Mock)
184
+ If you don't have Redis installed, the demo app will automatically fall back to an in-memory `fakeredis` database.
185
+
186
+ 1. **Start the API Server**:
187
+ ```bash
188
+ uvicorn examples.demo_app:app --port 8000 --reload
189
+ ```
190
+ 2. **Execute the Simulator Client** in another terminal:
191
+ ```bash
192
+ python examples.demo_concurrent.py
193
+ ```
194
+
195
+ #### Option B: Connected to a Cloud Redis Server (e.g. Upstash)
196
+ 1. Go to [Upstash](https://console.upstash.com/) and create a free serverless database.
197
+ 2. Copy the connection string under **Redis URL** (`rediss://default:...`).
198
+ 3. **Start the API Server** with the environment variable set:
199
+ ```bash
200
+ # Windows PowerShell
201
+ $env:REDIS_URL="rediss://default:yourpassword@your-endpoint.upstash.io:6379"
202
+ uvicorn examples.demo_app:app --port 8000 --reload
203
+ ```
204
+ 4. **Execute the Simulator Client**:
205
+ ```bash
206
+ python examples.demo_concurrent.py
207
+ ```
208
+
209
+ ---
210
+
211
+ ### Expected Console Output Trace
212
+
213
+ When executing `demo_concurrent.py`, the client output demonstrates the state transitions:
214
+
215
+ ```text
216
+ ======================================================================
217
+ DEMO 1: CONCURRENT DUPLICATE REQUESTS (State: PROCESSING)
218
+ Idempotency Key: 2391c028-804f-460f-b1f2-1e3e4110b127
219
+ Firing 2 requests simultaneously. The server takes ~2.5s to process...
220
+ ======================================================================
221
+
222
+ Response #1:
223
+ Status Code: 200
224
+ Headers:
225
+ X-Ichido-Status: executed
226
+ Retry-After: None
227
+ Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}
228
+
229
+ Response #2:
230
+ Status Code: 409
231
+ Headers:
232
+ X-Ichido-Status: None
233
+ Retry-After: 1
234
+ Body: {"error":"request_in_progress","message":"A request with idempotency key '2391c028-804f-460f-b1f2-1e3e4110b127' is currently being processed.Please retry later."}
235
+
236
+ ======================================================================
237
+ DEMO 2: REPLAYED REQUEST (State: COMPLETED)
238
+ Firing a 3rd request with the SAME key and SAME body...
239
+ ======================================================================
240
+
241
+ Response #3 (Replayed):
242
+ Status Code: 200
243
+ Headers:
244
+ X-Ichido-Status: replayed
245
+ Body: {"status":"success","transaction_id":"tx_2fc0df1b420d","amount_charged":49.99,"currency":"USD","msg":"Payment processed successfully."}
246
+
247
+ ======================================================================
248
+ DEMO 3: REQUEST PARAMETER MISMATCH (State: COMPLETED, Different Body)
249
+ Firing a 4th request with the SAME key but a DIFFERENT amount ($99.99)...
250
+ ======================================================================
251
+
252
+ Response #4 (Mismatch):
253
+ Status Code: 422
254
+ 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."}
255
+ ```
256
+
257
+ ---
258
+
259
+ ## πŸ’‘ Interview Q&A / Architecture Defense
260
+
261
+ Be ready to explain these core distributed systems design decisions:
262
+
263
+ ### 1. Why Redis instead of PostgreSQL for Idempotency?
264
+ - **Speed**: Idempotency checks sit in the critical hot path of payments APIs. Redis is an in-memory datastore that offers sub-millisecond lookups.
265
+ - **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.
266
+ - **Atomic Operations**: Redis executes commands single-threaded, allowing us to perform atomic `SET NX EX` locks in a single roundtrip.
267
+ - **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.
268
+
269
+ ### 2. How is the concurrent duplicate request problem solved?
270
+ - When two identical requests with the same key arrive simultaneously, both attempt a Redis `SET key value NX EX TTL`.
271
+ - Exactly one request succeeds (receiving the lock) and proceeds to run the downstream endpoint logic, setting the state to `processing`.
272
+ - 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.
273
+ - **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.
274
+
275
+ ### 3. Why did you choose pure ASGI middleware instead of Starlette's `BaseHTTPMiddleware`?
276
+ - 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.
277
+ - 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.
278
+
279
+ ### 4. What is At-Least-Once vs. Exactly-Once processing?
280
+ - **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).
281
+ - **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.
282
+
283
+ ---
284
+
285
+ ## πŸ“„ License
286
+
287
+ Distributed under the MIT License. See `LICENSE` for details.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0,<77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ichido"
7
+ version = "0.1.0"
8
+ description = "Stripe-inspired idempotency key middleware for FastAPI β€” exactly-once request processing."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Akhil"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: FastAPI",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ dependencies = [
27
+ "fastapi>=0.100.0",
28
+ "redis>=5.0.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0",
34
+ "pytest-asyncio>=0.23",
35
+ "httpx>=0.27",
36
+ "fakeredis>=2.21",
37
+ "uvicorn>=0.30",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/akhil/ichido"
42
+ Repository = "https://github.com/akhil/ichido"
43
+ Issues = "https://github.com/akhil/ichido/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
ichido-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+