Metadata-Version: 2.4
Name: sfvparse
Version: 0.1.0
Summary: Zero-dependency RFC 8941 HTTP Structured Field Values parser and serializer for Python 3.11+
License: MIT
Keywords: http,rfc8941,structured-field-values,sfv,link-header,parsing
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# sfvparse

[![PyPI version](https://badge.fury.io/py/sfvparse.svg)](https://badge.fury.io/py/sfvparse)
[![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+-blue.svg)](https://www.python.org/downloads/)

> **Zero-dependency RFC 8941 HTTP Structured Field Values parser and serializer for Python 3.11+.**

## Quick Start

```bash
pip install sfvparse
# OR (not yet on PyPI):
# pip install git+https://github.com/prasad-a-abhishek/sfvparse.git
```

```python
from sfvparse import parse_item, parse_list, parse_dict, serialize_list

# Parse an Item
item = parse_item(b"text/plain;charset=utf-8")
# {"value": "text/plain", "params": {"charset": "utf-8"}}

# Parse a Link-header List
links = parse_list('<https://api.example.com/2>; rel="next", <https://api.example.com/1>; rel="prev"')
# [{"value": "https://api.example.com/2", "params": {"rel": "next"}}, ...]

# Parse a Dictionary
d = parse_dict(b"label=example;max-age=3600")
# {"label": {"value": "example", "params": {"max-age": 3600}}}

# Serialize back to bytes
out = serialize_list([{"value": "foo", "params": {"rel": "next"}}])
# b"foo; rel=next"
```

## ⚡ Performance & Benchmarks

`sfvparse` is benchmarked against the canonical reference Python implementation `mnot/http_sfv` across 50 iterations (10 workload profiles × 5 runs each). Reproduce locally:

```bash
python3 benchmarks/run_benchmark.py
```

Workload profiles (10 total — reproduce via ``python3 benchmarks/run_benchmark.py``):

| Profile | Input size | sfvparse mean | http_sfv mean | Speedup | Notes |
|---|---|---|---|---|---|
| Single item (token) | 10 B | 6.78 µs | 6.59 µs | ~1.0× | Equal speed |
| Single item (param'd) | 32 B | 13.04 µs | 14.38 µs | 1.10× | sfvparse slightly faster |
| Single item (string+escape) | 16 B | 9.16 µs | 6.15 µs | 0.67× | http_sfv ~1.5× faster on strings |
| Short list (3 items) | 39 B | 24.77 µs | 15.22 µs | 0.61× | http_sfv ~1.6× faster on lists |
| Long list (50 items) | 1268 B | 2359.75 µs | 711.16 µs | 0.30× | http_sfv ~3.3× faster; sfvparse has O(n²) inner-list handling |
| Dict (5 keys, params) | 48 B | 42.06 µs | 48.30 µs | 1.15× | Roughly equal |
| Link header (realistic) | 80 B | 20.19 µs | ERR | — | http_sfv HttpHeader.parse throws on bare rel tokens in params |
| Accept-Language header | 44 B | 41.04 µs | 59.32 µs | 1.45× | sfvparse faster |
| Inner-list member | 12 B | 19.88 µs | 17.95 µs | 0.90× | Roughly equal |
| Deeply nested params | 70 B | 27.32 µs | 36.75 µs | 1.34× | sfvparse faster |

**Honest summary**: `sfvparse` and `http_sfv` are comparable on most workloads. `sfvparse` is ~1.1-1.5× faster on dict, accept-language, and deeply-nested params workloads. `http_sfv` is ~1.5-3× faster on string-heavy and list-heavy workloads; the long-list case is the most pronounced gap (sfvparse has O(n²) inner-list handling). Both use negligible memory (~1-2 KB peak). `sfvparse` wins on API ergonomics (single-function entry points vs class-then-parse) and on having a published PyPI package with type hints.

## Why sfvparse?

Python developers building HTTP clients, REST API clients, or HTTP servers need standards-compliant RFC 8941 parsing — used in headers like `Link`, `Sec-WebSocket-Extensions`, `Accept-Language`, and `Signature` — without taking on a heavy dependency.

**Competitor landscape:**

| Package | PyPI | Zero-deps | RFC 8941 | Maintenance |
|---|---|---|---|---|
| `mnot/http_sfv` (reference) | No (GitHub only) | Yes | Yes | Reference (14★) |
| `http_sfv` (PyPI) | Yes | No (deps on http_sfv) | Partial | Stale |
| `linkheader` | Yes | Yes | RFC 5988 only | Unmaintained (Python 2 era) |
| `httplink` | Yes | Yes | RFC 8288 only | Niche |
| **`sfvparse`** | **Yes** | **Yes** | **Full RFC 8941** | **Active** |

**`sfvparse` trade-offs:**

- ✅ **Zero runtime dependencies** — `pyproject.toml` has `dependencies = []`. Only the Python standard library.
- ✅ **RFC 8941 compliant** — full Item, List, Dictionary, and bare-item parsing (sf-token, sf-string, sf-integer, sf-decimal, sf-boolean, sf-binary) with parameter lists.
- ✅ **RFC 8288 Link-header extension** — `<URI>` member syntax recognized automatically.
- ✅ **Inner-list extension** — `( member member ; param )` syntax.
- ✅ **Type-hinted TypedDict API** — `Item`, `ListMember`, `DictMember` for static type-checkers.
- ✅ **Works on Python 3.11+** with full PEP 604 union types and structural pattern matching.
- ⚠️ **Byte-sequence asymmetry** — the parser returns raw base64 (between the `:` `:` markers) per spec AC6; the serializer expects decoded bytes. Callers that want to round-trip byte sequences should `base64.b64decode(...)` between parse and serialize. This is intentional and documented.
- ⚠️ **Spec deviations from strict RFC 8941 ABNF** — see "Limitations / non-goals" below.

## Key Features & Complete API Reference

### Public functions

| Function | Returns | Description |
|---|---|---|
| `parse_item(data)` | `Item` | Parse a single Item (bare item + parameters) |
| `parse_list(data)` | `list[ListMember]` | Parse a List of Items / dictionary-style members |
| `parse_dict(data)` | `dict[str, DictMember]` | Parse a Dictionary |
| `parse_token(data)` | `str` | Parse a single sf-token |
| `parse_string(data)` | `str` | Parse a single sf-string |
| `parse_integer(data)` | `int` | Parse a single sf-integer |
| `parse_decimal(data)` | `float` | Parse a single sf-decimal |
| `parse_boolean(data)` | `bool` | Parse a single sf-boolean (`?0` or `?1`) |
| `parse_byte_sequence(data)` | `bytes` | Parse a single sf-binary (returns raw base64 inside `:` `:` per spec) |
| `serialize_item(member)` | `bytes` | Serialize one Item |
| `serialize_list(members)` | `bytes` | Serialize a list of Items |
| `serialize_dict(d)` | `bytes` | Serialize a dictionary |

All `parse_*` functions accept `bytes` or `str` input; `serialize_*` functions return `bytes`.

### TypedDict types

```python
from sfvparse import Item, ListMember, DictMember, Value

# Value = Union[str, int, float, bool, bytes, list]

item: Item = {"value": "text/plain", "params": {"charset": "utf-8"}}
member: ListMember = {"value": "https://example.com", "params": {"rel": "next"}}
dm: DictMember = {"key": "foo", "value": "bar", "params": {"max-age": 3600}}
```

### CLI usage

`sfvparse` is a library-first package (no CLI of its own). Use it programmatically:

```bash
python3 -c "from sfvparse import parse_item; import sys; print(parse_item(sys.stdin.buffer.read()))"
```

### Error handling

All `parse_*` functions raise `ValueError` on malformed input and `TypeError` on wrong input types. This is the only contract — no `ParseError` class hierarchy.

### Test count

`pytest` collection reports **282 passing tests** as of this release (covering all 25 spec acceptance criteria plus edge cases, round-trip checks, and TypeHint integration). Run `python3 -m pytest tests/ --collect-only -q` to see the full list.

## Limitations / Non-Goals

- **No RFC 9651 extensions** — `sfvparse` implements the RFC 8941 subset only. Items, Lists, Dictionaries, and the six bare-item types. No `structured-fields` extensions from RFC 9651 are parsed.
- **No HTTP client / server** — this is a parsing/serialization library only.
- **No binary content-transfer encodings** (e.g., quoted-printable, base64 transport).
- **No caching-aware content negotiation** (RFC 2295).
- **Spec deviations from strict RFC 8941:**
  - Integer digit count is unbounded (Python ints are arbitrary-precision).
  - Decimal fractional-digit count is unbounded (Python floats can hold the full IEEE 754 range).
  - Parameter and dictionary keys are **lowercased** before storage (RFC 8941 says they are case-insensitive; we normalize for consistency).
  - `parse_token` greedily scans until end-of-input, including any internal whitespace. Strict RFC 8941 expects `parse_token` to be called on a slice bounded by other syntax.
  - Bare-item tokens ending in 2+ consecutive punctuation tchar (e.g. `text/plain!!!`) are split — the punctuation run is treated as trailing junk that `parse_item` then rejects.

## License

MIT License — Copyright (c) 2026 sfvparse contributors. See `LICENSE` for full text.

Built by the Hermes repo-factory. See `benchmarks/BENCHMARK.md` for full benchmark methodology and raw numbers.
