Metadata-Version: 2.4
Name: midkit
Version: 0.1.0
Summary: A small collection of reusable middlewares for FastAPI: request logging, rate limiting and response standardization.
Project-URL: Homepage, https://github.com/Seojooyeon-creat/midkit
Project-URL: Repository, https://github.com/Seojooyeon-creat/midkit
Project-URL: Issues, https://github.com/Seojooyeon-creat/midkit/issues
Author-email: Jooyeon Seo <killoper35@gmail.com>
License: MIT
License-File: LICENSE
Keywords: fastapi,logging,middleware,rate-limit
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.100.0
Requires-Dist: starlette>=0.27.0
Provides-Extra: dev
Requires-Dist: httpx>=0.24; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# midkit

**English** | [한국어](README.ko.md)

A small, dependency-light collection of reusable middlewares for [FastAPI](https://fastapi.tiangolo.com/).
No external runtime dependencies beyond FastAPI/Starlette — everything else is the standard library.

## Features

| Middleware                  | What it does                                                                 |
| --------------------------- | --------------------------------------------------------------------------- |
| `RequestLoggerMiddleware`   | Logs method, path, status code and elapsed time (ms) for each request.      |
| `RateLimitMiddleware`       | IP-based sliding-window rate limiting with `429` + rate-limit headers.      |
| `ResponseWrapperMiddleware` | Wraps JSON responses in a standard `{success, data, error}` envelope.       |

## Installation

```bash
pip install midkit
```

## Quick Start

```python
from fastapi import FastAPI
from midkit import (
    RequestLoggerMiddleware,
    RateLimitMiddleware,
    ResponseWrapperMiddleware,
)

app = FastAPI()

# 추가 순서가 곧 적용 순서: 나중에 추가한 것이 바깥쪽(요청을 먼저 받음).
# ResponseWrapper 를 먼저 추가해 가장 안쪽에서 응답을 먼저 감싼다.
app.add_middleware(ResponseWrapperMiddleware)
app.add_middleware(RateLimitMiddleware, max_requests=100, window_seconds=60)
app.add_middleware(RequestLoggerMiddleware, exclude_paths=["/health"])


@app.get("/hello")
def hello():
    return {"msg": "hi"}
```

> **Ordering matters.** FastAPI wraps middleware as an onion: the middleware added
> *last* sits on the *outside*. Add `ResponseWrapperMiddleware` first so it is the
> innermost layer and wraps the handler's raw output before the others run.
> See [`examples/basic_usage.py`](examples/basic_usage.py).

All three middlewares share an `exclude_paths` option that accepts exact paths,
prefixes (`/api`) and shell-style globs (`/static/*`).

---

## `RequestLoggerMiddleware`

Logs one line per request. 4xx/5xx responses are logged at `WARNING`, everything
else at `INFO`. The logger name is `midkit.logger`.

| Option            | Type              | Default | Description                                       |
| ----------------- | ----------------- | ------- | ------------------------------------------------- |
| `exclude_paths`   | `list[str]`       | `[]`    | Paths that should not be logged.                  |
| `log_body`        | `bool`            | `False` | Log up to the first 200 chars of the request body.|
| `logger_instance` | `Logger \| None`  | `None`  | Custom logger; defaults to the library logger.    |

```python
app.add_middleware(
    RequestLoggerMiddleware,
    exclude_paths=["/health", "/metrics"],
    log_body=True,
)
```

Example log line:

```
INFO  midkit.logger  GET /hello -> 200 (1.23ms)
```

> **Configure logging to see the output.** The logger emits `INFO` for 2xx/3xx
> and `WARNING` for 4xx/5xx, but Python's root logger defaults to `WARNING` and
> has no handler attached — so by default only the 4xx/5xx lines appear (or none
> at all). Set up logging once at startup to see every request:
>
> ```python
> import logging
>
> # 모든 요청(INFO 포함)을 보려면 레벨을 INFO 로 낮춘다
> logging.basicConfig(level=logging.INFO)
> ```
>
> Already running under Uvicorn/Gunicorn? They configure their own handlers, so
> you usually only need to raise the level for the midkit logger specifically:
>
> ```python
> logging.getLogger("midkit.logger").setLevel(logging.INFO)
> ```

---

## `RateLimitMiddleware`

Sliding-window limiter keyed per client. Timestamps are stored in a `deque` and
pruned on each request.

| Option           | Type                                   | Default | Description                                  |
| ---------------- | -------------------------------------- | ------- | -------------------------------------------- |
| `max_requests`   | `int`                                  | `100`   | Max requests allowed per window.             |
| `window_seconds` | `int`                                  | `60`    | Window length in seconds.                    |
| `exclude_paths`  | `list[str]`                            | `[]`    | Paths exempt from rate limiting.             |
| `key_func`       | `Callable[[Request], str] \| None`     | `None`  | Custom client key; defaults to client IP.    |

By default the client key is the first IP in `X-Forwarded-For`, falling back to
`request.client.host`.

Every response carries:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
```

When the limit is exceeded the request is rejected with `429`:

```json
{
  "error": "rate_limit_exceeded",
  "detail": "Rate limit of 100 requests per 60s exceeded."
}
```

and a `Retry-After` header indicating when to try again.

---

## `ResponseWrapperMiddleware`

Wraps `application/json` responses in a standard envelope. Non-JSON responses
(HTML, files, streams) pass through untouched, and `/docs`, `/redoc` and
`/openapi.json` are excluded by default.

| Option          | Type        | Default | Description                                      |
| --------------- | ----------- | ------- | ------------------------------------------------ |
| `exclude_paths` | `list[str]` | `[]`    | Extra paths returned without wrapping.           |
| `wrap_errors`   | `bool`      | `True`  | Also wrap 4xx/5xx responses.                     |

Success response:

```json
{ "success": true, "data": { "id": 1, "name": "widget" }, "error": null }
```

Error response (4xx/5xx):

```json
{ "success": false, "data": null, "error": { "code": 404, "message": "Not Found" } }
```

Responses that already contain the `success`/`data`/`error` keys are passed
through as-is to avoid double wrapping.

---

## Usage Examples

A complete, runnable app combining all three middlewares lives in
[`examples/basic_usage.py`](examples/basic_usage.py). Run it with:

```bash
pip install -e ".[dev]" uvicorn
uvicorn examples.basic_usage:app --reload
```

It applies the middlewares like this (note the ordering):

```python
# 추가 순서가 곧 적용 순서: 나중에 추가한 것이 바깥쪽.
app.add_middleware(ResponseWrapperMiddleware, exclude_paths=["/raw"])
app.add_middleware(RateLimitMiddleware, max_requests=10, window_seconds=60,
                   exclude_paths=["/health"])
app.add_middleware(RequestLoggerMiddleware, exclude_paths=["/health"], log_body=True)
```

### Successful response (wrapped + rate-limit headers)

```bash
$ curl -i http://127.0.0.1:8000/users/1
HTTP/1.1 200 OK
x-ratelimit-limit: 10
x-ratelimit-remaining: 9
{"success":true,"data":{"id":1,"name":"user-1"},"error":null}
```

### Error response (wrapped too)

```bash
$ curl -i http://127.0.0.1:8000/users/0
HTTP/1.1 404 Not Found
{"success":false,"data":null,"error":{"code":404,"message":"User not found"}}
```

### Excluded path (returned raw, not wrapped)

```bash
$ curl -s http://127.0.0.1:8000/raw
{"raw":true}
```

### Rate limit exceeded

```bash
# Hammer the endpoint past max_requests=10
$ for i in $(seq 1 12); do
    curl -s -o /dev/null -w "%{http_code} " http://127.0.0.1:8000/users/1
  done
200 200 200 200 200 200 200 200 200 200 429 429

$ curl -i http://127.0.0.1:8000/users/1
HTTP/1.1 429 Too Many Requests
retry-after: 42
x-ratelimit-limit: 10
x-ratelimit-remaining: 0
{"error":"rate_limit_exceeded","detail":"Rate limit of 10 requests per 60s exceeded."}
```

### Request log output

With logging configured (see the note above), each request prints one line:

```
INFO     midkit.logger  GET /users/1 -> 200 (0.40ms)
WARNING  midkit.logger  GET /users/0 -> 404 (0.31ms)
```

---

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

## License

[MIT](LICENSE)
