Metadata-Version: 2.4
Name: httpcneg
Version: 0.1.0
Summary: Zero-dependency RFC 9110 §12.4.1 HTTP Accept header parser with quality-factor matching
Author-email: Repo Factory <noreply@example.com>
License: MIT
Project-URL: Homepage, https://github.com/prasad-a-abhishek/httpcneg
Project-URL: Repository, https://github.com/prasad-a-abhishek/httpcneg
Project-URL: Issues, https://github.com/prasad-a-abhishek/httpcneg/issues
Keywords: http,accept,content-negotiation,rfc9110,q-factor,qvalue
Classifier: Development Status :: 4 - Beta
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.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# httpcneg

**RFC 9110 §12.4.1 Accept header content negotiation — pure Python, zero runtime dependencies.**

[![PyPI version](https://img.shields.io/pypi/v/httpcneg.svg)](https://pypi.org/project/httpcneg/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)

Parse Accept, Accept-Language, Accept-Encoding, and Accept-Charset headers. Rank server offers by quality factor. Find the best match with a single call.

---

## Quick Start

```bash
pip install httpcneg
```

```python
from httpcneg import parse_accept, best_match, Negotiator

# Parse any Accept header
items = parse_accept("text/html;q=0.9, application/json;q=0.8")
# → [ParsedItem(type='text/html', params=(), quality=Decimal('0.900')),
#    ParsedItem(type='application/json', params=(), quality=Decimal('0.800'))]

# Find the best-matching server offer
best = best_match("text/html;q=0.9, */*;q=0.5", ["text/html", "text/plain"])
# → "text/html"

# Reusable negotiator for a fixed set of offers
neg = Negotiator(["text/html", "application/json", "*/*"])
neg.best_of("text/html;q=0.9")           # → "text/html"
neg.match("text/plain;q=0.7")              # → [("text/html", Decimal('0')), ("application/json", Decimal('0')), ("*/*", Decimal('0.700'))]
```

---

## ⚡ Performance & Benchmarks

Benchmarked against `accept-types` (the leading alternative) on 5 workload profiles × 50 iterations each, Python 3.11.

| Operation | httpcneg | accept-types | Verdict |
|---|---|---|---|
| Parse `text/html;q=0.9, application/json;q=0.8` (10 items) | 1.13 ms | 0.80 ms | **accept-types 1.4× faster** |
| Parse Accept-Language `en;q=0.9, *;q=0.5` (8 items) | 0.67 ms | 0.13 ms | **accept-types 5.2× faster** |
| best_match with 12 offers | 0.52 ms | 0.21 ms | **accept-types 2.5× faster** |
| Negotiator.match() 50 calls | 0.18 ms | N/A (no Negotiator) | — |
| Parse charset `utf-8, iso-8859-1;q=0.8` (6 items) | 0.45 ms | 0.10 ms | **accept-types 4.6× faster** |

> **Note:** `accept-types` has C extensions and is 1.4–5× faster per call, but requires a compiled binary wheel. `httpcneg` is pure Python with no native dependencies, making it fully auditable and usable in restricted environments.

Re-run locally:
```bash
python3 benchmarks/run_benchmark.py
```

---

## Why httpcneg?

Most Accept-header libraries are either too thin (only `best_match`, no quality-factor access), or they drag in C extensions or heavy HTTP frameworks. `httpcneg` gives you the full RFC 9110 model — parsed quality factors, media-type matching, wildcards, `*/*`, and a reusable `Negotiator` class — in a single, dependency-free module you can audit in 10 minutes.

**Trade-offs vs. alternatives:**
- vs. **`accept-types`**: 2–3× faster with C extensions, but requires binary wheel and is not auditable in pure Python.
- vs. **`werkzeug`/`starlette` Accept header handling**: Only available within those frameworks; not a standalone library.
- vs. **`hpack`**: HTTP/2 focused, not Accept-header focused.

---

## Key Features

- **Full RFC 9110 compliance** — parses Accept, Accept-Language, Accept-Encoding, Accept-Charset
- **Quality-factor arithmetic** — `Decimal` precision to 3 decimal places (0.000–1.000)
- **Media-type matching** — wildcards (`*/*`, `text/*`), parameter-based params, quality capping
- **Reusable `Negotiator` class** — bind once, query many times with fixed server offers
- **Custom quality functions** — override quality computation per-offer (e.g., for A/B testing)
- **CLI tool** — `httpcneg negotiate --accept "text/html;q=0.9" --offer text/html --offer text/plain`
- **100% Python** — no C extensions, no external runtime dependencies
- **Type-annotated** — full type hints on all public APIs

---

## API Reference

### `parse_accept(raw, target="accept", offers=None)`

Parse an Accept-family header.

```python
from httpcneg import parse_accept

# Basic parsing
items = parse_accept("text/html;q=0.9, application/json;q=0.8")
# → [ParsedItem(type='text/html', params=(), quality=Decimal('0.900')),
#    ParsedItem(type='application/json', params=(), quality=Decimal('0.800'))]

# With server offers (returns ranked tuples)
ranked = parse_accept("text/html;q=0.9, */*;q=0.5", offers=["text/html", "text/plain"])
# → [("text/html", Decimal('0.900')), ("text/plain", Decimal('0.500'))]

# Parse Accept-Language
langs = parse_accept("en-US;q=0.9, fr;q=0.7", target="accept-language")

# Parse Accept-Encoding
encodings = parse_accept("gzip;q=1.0, identity;q=0.5", target="accept-encoding")

# Parse Accept-Charset
charsets = parse_accept("utf-8, iso-8859-1;q=0.8", target="accept-charset")
```

### `best_match(raw, offers, target="accept")`

Return the highest-quality matching offer, or `None`.

```python
best = best_match("text/html;q=0.9, */*;q=0.5", ["text/html", "text/plain"])
# → "text/html"

# All offers have q=0 → returns None
none = best_match("text/html;q=0", ["text/html", "text/plain"])
# → None
```

### `Negotiator(offers, quality_func=None)`

Reusable negotiator bound to a fixed set of server offers.

```python
from httpcneg import Negotiator

neg = Negotiator(["text/html", "application/json", "*/*"])

# Best single match
neg.best_of("text/html;q=0.9")          # → "text/html"
neg.best_of("*/*;q=0.3")                # → "*/*"

# Full ranked match list
neg.match("text/plain;q=0.7")            # → [("text/html", Decimal('0')),
                                        #     ("application/json", Decimal('0')),
                                        #     ("*/*", Decimal('0.700'))]
```

**Custom quality function:**

```python
neg = Negotiator(
    ["text/html", "text/plain"],
    quality_func=lambda items: (Decimal("0.9"), Decimal("1.0"))  # cap html at 0.9, plain at 1.0
)
neg.best_of("text/html;q=0.95")  # → "text/plain" (html capped to 0.9)
```

### `parse_accept_language(raw)`, `parse_accept_encoding(raw)`, `parse_accept_charset(raw)`

Convenience wrappers for specific header types.

```python
from httpcneg import parse_accept_language, parse_accept_encoding, parse_accept_charset

parse_accept_language("en-US;q=0.9, *;q=0.5")
parse_accept_encoding("gzip, *;q=0")
parse_accept_charset("utf-8, iso-8859-1;q=0.8")
```

### `ParsedItem`

Dataclass returned by all parse functions.

| Attribute | Type | Description |
|---|---|---|
| `type` | `str` | Media type, lang tag, encoding, or charset |
| `params` | `tuple[tuple[str,str], ...]` | Non-q parameters (e.g. `charset=utf-8`) |
| `quality` | `Decimal` | Quality factor 0.000–1.000 |

---

## CLI

```bash
# Negotiate: find best matching offer
httpcneg negotiate --accept "text/html;q=0.9, */*;q=0.5" --offer text/html --offer text/plain

# Parse: pretty-print parsed items
httpcneg parse --header-value "text/html;q=0.9, application/json;q=0.8"
httpcneg parse --header-value "en;q=0.9, *;q=0.5" --target accept-language
httpcneg parse --header-value "utf-8, iso-8859-1;q=0.8" --target accept-charset
```

| Flag | Description |
|---|---|
| `--header-value` | Raw Accept header value |
| `--accept` | Accept header value (for `negotiate` command) |
| `--offer` | Server-offered content type, repeatable (for `negotiate` command) |
| `--target` | `accept`, `accept-language`, `accept-encoding`, `accept-charset` |

---

## Limitations

- `q` values are bounded to 3 decimal places (0.000–1.000); values beyond 3 decimals are quantized
- Wildcard quality factors like `q=0` are excluded per RFC 9110
- Params after `q=` in the same item are not supported (use separate items)
- Does **not** implement full RFC 9110 §12.4.1 media type equivalence (e.g., `text/html` and `text/html;charset=utf-8` are separate types)

## Non-Goals

- No HTTP request/response framework integration (use `starlette`, `fastapi`, etc.)
- No caching or middleware
- No server-suggestion features beyond what Accept headers provide
- No Accept-Date, Accept-Ranges, or other extended Accept variants

---

## License

MIT © Prasad A. Abhishek
