Metadata-Version: 2.5
Name: syndikit
Version: 0.1.0
Summary: A layered feed parser: use it at any granularity, from byte decoding to a feedparser-compatible dict.
Project-URL: Homepage, https://github.com/daaquan/syndikit
Project-URL: Issues, https://github.com/daaquan/syndikit/issues
Author: daaquan
License-Expression: MIT
License-File: LICENSE
Keywords: atom,feed,jsonfeed,parser,rdf,rss,syndication
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Text Processing :: Markup :: XML
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: feedparser>=6; extra == 'dev'
Requires-Dist: hypothesis>=6; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: http
Requires-Dist: httpx>=0.27; extra == 'http'
Description-Content-Type: text/markdown

# syndikit

A feed parser you can use at any granularity — from "just tell me the encoding"
up to a drop-in replacement for `feedparser.parse()`.

Pure standard library. No network in the parser. Every result is picklable.

## Why

[feedparser](https://github.com/kurtmckee/feedparser) is the incumbent, and its
best-known defects are not isolated bugs — they follow from two design choices:

1. **It rewrites the document text.** Regex-substituting the XML declaration is
   the root of [#511](https://github.com/kurtmckee/feedparser/issues/511) and
   [#508](https://github.com/kurtmckee/feedparser/issues/508); the split between
   the `str` and `bytes` paths is the root of
   [#427](https://github.com/kurtmckee/feedparser/issues/427).
2. **It flattens the tree into one dict** keyed by `prefix_localname`, so
   repeated sibling elements clobber each other
   ([#139](https://github.com/kurtmckee/feedparser/issues/139),
   [#145](https://github.com/kurtmckee/feedparser/issues/145),
   [#190](https://github.com/kurtmckee/feedparser/issues/190),
   [#317](https://github.com/kurtmckee/feedparser/issues/317),
   [#409](https://github.com/kurtmckee/feedparser/issues/409)).

syndikit does neither. The prolog is *recorded*, not rewritten, and the document
stays a tree that convenience views project from. Those defect classes cannot
occur here, rather than being patched one at a time.

syndikit contains no feedparser code. It is an independent implementation.

## Layers

| Layer | Module | In → out |
|---|---|---|
| L0 | `syndikit.encoding` | `bytes` → `Document` (text, encoding, prolog) |
| L1 | `syndikit.pull` | `str` → `Iterator[Event]` |
| L2 | `syndikit.tree` | events → `Node` tree with positions, repeats kept |
| L3 | `syndikit.model` | tree → `Feed` / `Entry` dataclasses |
| L4 | `syndikit.compat.feedparser` | anything → `feedparser`-shaped dict |
| — | `syndikit.loose` | broken markup → the same tree, recovered |
| — | `syndikit.http` | URL → `FetchResult` (conditional GET) |
| — | `syndikit.sanitize` | untrusted feed HTML → allowlisted HTML |

Use any layer directly; each one only depends on the layers below it.

## Install

```bash
pip install syndikit
```

## Use

```python
import syndikit

result = syndikit.parse(raw_bytes)        # bytes | str | PathLike | file-like
result.feed.title
result.entries[0].published               # tz-aware datetime, always UTC
result.entries[0].categories              # every one of them, not just the last
result.diagnostics                        # parsing never raises by default
result.entries[0].node                    # escape hatch back to the tree
```

Only the encoding:

```python
from syndikit.encoding import decode

document = decode(raw_bytes, http_charset="shift_jis")
document.encoding, document.encoding_source, document.prolog.decl
```

Migrating from feedparser — change the import, keep the code:

```python
from syndikit.compat import feedparser
feed = feedparser.parse(url)
feed.bozo, feed.entries[0].title, feed.entries[0].published_parsed
```

## Broken feeds

A feed that stops being well-formed half way through is re-read with a tolerant
parser, so you get every entry *and* the diagnostic saying what was wrong:

```python
result = syndikit.parse(feed_with_a_raw_ampersand)
result.entries                  # all of them
result.ok                       # False
[d.code for d in result.diagnostics]
# ['xml.not-well-formed', 'xml.recovered']
```

Nothing raises unless you ask: `syndikit.parse(data, strict=True)` turns
error-severity diagnostics into a `SyndikitError` that is still picklable.

## Testing

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

Tests run offline against 32 real feeds frozen in `tests/corpus/`, including a
parity check against feedparser itself. `tools/fetch_corpus.py` refreshes them;
`docs/feedparser-differences.md` lists every deliberate divergence.

## License

MIT
