OPDS Client
A Python library for working with OPDS catalogs, supporting both OPDS 1.2 (Atom/XML) and OPDS 2.0 (JSON) specifications.
Features
OPDS 2.0 (JSON)
- ✅ Parse OPDS 2.0 feeds and publications
- ✅ Validate against OPDS 2.0 specification
- ✅ Navigate navigation, publications, groups, and facets collections
- ✅ Handle pagination (first, previous, next, last links)
- ✅ Search catalogs with URI template resolution
- ✅ Work with acquisition links (download, buy, borrow, etc.)
- ✅ HTTP error handling with retries
- ✅ Pydantic models for type safety
OPDS 1.2 (Atom/XML)
- ✅ Parse OPDS 1.2 Atom feeds and entries
- ✅ Dublin Core metadata support
- ✅ Navigation and Acquisition feeds
- ✅ Facet groups with active facets and item counts
- ✅ Acquisition links with prices and indirect acquisitions
- ✅ OpenSearch integration
- ✅ Multiple acquisition relations (buy, borrow, subscribe, etc.)
Installation
pip install opds-client
Quick Start
OPDS 2.0 (JSON)
from opds_client import OPDSClient
# Create a client
client = OPDSClient()
# Fetch a feed
feed = client.fetch_feed("https://example.com/opds")
# Access feed metadata
print(f"Title: {feed.metadata.title}")
print(f"Publications: {len(feed.publications or [])}")
# Navigate navigation links
for nav in feed.navigation or []:
print(f"- {nav.title}: {nav.href}")
# Search
results = client.search(feed, "query")
OPDS 1.2 (Atom/XML)
from opds_client import OPDS12Client
# Create a client for OPDS 1.2
client = OPDS12Client()
# Fetch a feed (XML format)
feed = client.fetch_feed("https://example.com/opds.xml")
# Access feed metadata
print(f"Title: {feed.title}")
print(f"Entries: {len(feed.entries or [])}")
# Navigate navigation links
start_link = feed.get_start_link()
# Get acquisition links from entries
if feed.entries:
entry = feed.entries[0]
acq_links = entry.get_acquisition_links()
Using Base URL (Origin)
Both OPDS 1.2 and OPDS 2.0 clients support setting a base URL (origin) that will be automatically prepended to relative URLs. This is useful when working with catalogs that use relative paths.
OPDS 2.0 Example
from opds_client import OPDSClient
# Create client with origin
client = OPDSClient(origin="https://example.com")
# Fetch feed with relative path
feed = client.fetch_feed("/catalog") # → https://example.com/catalog
# Absolute URLs are used as-is
feed = client.fetch_feed("https://other.com/catalog") # → https://other.com/catalog
OPDS 1.2 Example
from opds_client import OPDS12Client
# Create client with origin
client = OPDS12Client(origin="https://api.example.com")
# Fetch feed with relative path
feed = client.fetch_feed("/opds.xml") # → https://api.example.com/opds.xml
# Navigation links with origin
start_link = feed.get_start_link()
if start_link:
# The client automatically prepends the origin to relative URLs
root_feed = client.get_start_link(feed)
Authentication
Both OPDS 1.2 and OPDS 2.0 clients support HTTP Basic Authentication.
Basic Auth with Username/Password
from opds_client import OPDSClient
# For OPDS 2.0
client = OPDSClient(username="your_username", password="your_password")
feed = client.fetch_feed("https://example.com/opds")
# For OPDS 1.2
client = OPDS12Client(username="your_username", password="your_password")
feed = client.fetch_feed("https://example.com/opds.xml")
Custom Auth Objects
For advanced authentication scenarios (Digest Auth, Bearer tokens, custom auth):
from opds_client import OPDSClient
from requests.auth import HTTPDigestAuth, HTTPBasicAuth
# Digest authentication
client = OPDSClient(auth=HTTPDigestAuth("username", "password"))
feed = client.fetch_feed("https://example.com/opds")
# Custom auth (e.g., with session tokens)
class CustomAuth:
def __call__(self, r):
r.headers["X-Custom-Token"] = "your-token"
return r
client = OPDSClient(auth=CustomAuth())
feed = client.fetch_feed("https://example.com/opds")
Models
OPDS 2.0 Models
Link
from opds_client.models import Link, AcquisitionLink
# Basic link
link = Link(href="https://example.com", rel="self")
# Acquisition link
acq_link = AcquisitionLink(
href="https://example.com/download",
rel="download",
type="application/epub+zip",
)
OPDS 1.2 Models
Link
from opds_client.opds12.models import Link, AcquisitionLink
# Basic link
link = Link(
href="https://example.com",
rel="http://opds-spec.org/acquisition",
type="application/epub+zip",
)
# Acquisition link with price
acq_link = AcquisitionLink(
href="https://example.com/buy",
rel="http://opds-spec.org/acquisition/buy",
properties={"price": Price(value="19.99", currencycode="USD")},
)
Entry
from opds_client.opds12.models import Entry, Link, Price
entry = Entry(
id="urn:uuid:test",
title="Test Entry",
updated="2010-01-10T10:00:00Z",
authors=["Test Author"],
links=[
Link(
href="https://example.com",
rel="http://opds-spec.org/acquisition",
type="application/epub+zip",
),
],
)
Feed
from opds_client.opds12.models import Feed, Link, Entry
feed = Feed(
id="urn:uuid:feed",
title="OPDS 1.2 Feed",
updated="2010-01-10T10:00:00Z",
links=[Link(href="https://example.com", rel="self")],
entries=[entry],
)
Publication (OPDS 2.0)
from opds_client.models import OPDSPublication, Metadata, Link
pub = OPDSPublication(
metadata=Metadata(
title="Test Book",
author="Test Author",
),
links=[
Link(href="https://example.com", rel="self"),
Link(href="https://example.com/download", rel="download"),
],
)
Feed (OPDS 2.0)
from opds_client.models import OPDSFeed
# Feed with collections
feed = OPDSFeed(
metadata=Metadata(title="My Catalog"),
links=[Link(href="https://example.com", rel="self")],
navigation=[...],
publications=[...],
facets=[...],
groups=[...],
)
Usage Examples
OPDS 2.0 Examples
Navigation
from opds_client import OPDSClient
client = OPDSClient()
feed = client.fetch_feed("https://example.com/opds")
# Follow navigation by title
new_feed = client.follow_navigation(feed, "New Publications")
# Navigate facets
featured_feed = client.follow_facet(feed, "Category", "Featured")
OPDS 1.2 Examples
Navigation
from opds_client import OPDS12Client
client = OPDS12Client()
feed = client.fetch_feed("https://example.com/opds.xml")
# Get start link (catalog root)
start_link = feed.get_start_link()
# Get self link
self_link = feed.get_self_link()
# Get next page
next_feed = client.get_next_page(feed)
Pagination
```python
# Get next page
next_feed = client.get_next_page(feed)
# Get previous page
prev_feed = client.get_previous_page(feed)
# Get first/last page
first = client.get_first_page(feed)
last = client.get_last_page(feed)
OPDS 1.2 Pagination
from opds_client import OPDS12Client
client = OPDS12Client()
feed = client.fetch_feed("https://example.com/opds.xml")
# Get next page
next_feed = client.get_next_page(feed)
# Get previous page
prev_feed = client.get_previous_page(feed)
# Get catalog root
root_feed = client.get_start_link(feed)
Search
```python
# Search the catalog
results = client.search(feed, "search query", title="My Book")
# Check if search is supported
if feed.is_searchable():
results = client.search(feed, "query")
OPDS 1.2 Acquisition
from opds_client import OPDS12Client
client = OPDS12Client()
feed = client.fetch_feed("https://example.com/opds.xml")
# Get acquisition links from entries
entry = feed.entries[0]
acq_links = entry.get_acquisition_links()
# Follow specific acquisition type
buy_link = client.follow_acquisition(entry, acquisition_type="buy")
if buy_link:
price = buy_link.get_price()
print(f"Price: {price.currencycode} {price.value}")
Acquisition
from opds_client.models import AcquisitionManager
```python
# Get acquisition links
pub = client.fetch_publication("https://example.com/publication")
acq_links = pub.get_acquisition_links()
# Use acquisition manager
manager = AcquisitionManager(acq_links[0])
# Check for open access
if manager.get_open_access_link():
# Download directly
pass
# Check for buy link
buy_link = manager.get_buy_link()
if buy_link:
price = buy_link.get_price()
print(f"Price: {price.currency} {price.value}")
OPDS 1.2 vs OPDS 2.0
| Feature | OPDS 1.2 | OPDS 2.0 |
|---|---|---|
| Format | Atom/XML | JSON |
| Parser | OPDS12Parser |
OPDSParser |
| Client | OPDS12Client |
OPDSClient |
| Feed Type | Feed |
OPDSFeed |
| Entry Type | Entry |
OPDSPublication |
| Metadata | Dublin Core + Atom | JSON-LD (schema.org) |
| Acquisition Links | XML opds:price |
JSON properties.price |
Choose the appropriate client based on your catalog's OPDS version.
Testing
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=opds_client
# Run type checks
mypy opds_client
Project Structure
opds_client/
├── __init__.py # Package exports (OPDS 1.2 & 2.0)
├── client.py # OPDS 2.0 Client
├── parser.py # JSON parsing/validation (OPDS 2.0)
├── exceptions.py # Custom exceptions
├── utils.py # Utility functions
├── models/ # OPDS 2.0 models
│ ├── __init__.py
│ ├── base.py # Base types (Link, Metadata)
│ ├── publication.py # Publication models
│ ├── feed.py # Feed/Collection models
│ └── acquisition.py # Acquisition models
└── opds12/ # OPDS 1.2 specific module
├── __init__.py
├── client.py # OPDS 1.2 Client
├── parser.py # Atom/XML parsing (OPDS 1.2)
├── models.py # OPDS 1.2 models
├── namespace.py # XML namespace constants
└── exceptions.py # OPDS 1.2 exceptions
tests/
├── conftest.py
├── models/ # OPDS 2.0 model tests (mirrors opds_client/models/)
│ ├── __init__.py
│ ├── test_base.py # Base types (Link, Metadata)
│ ├── test_publication.py # Publication models
│ └── test_feed.py # Feed/Collection models
├── opds12/ # OPDS 1.2 tests (mirrors opds_client/opds12/)
│ ├── __init__.py
│ ├── test_models.py # OPDS 1.2 models
│ ├── test_parser.py # Atom/XML parsing
│ ├── test_client.py # OPDS 1.2 Client
│ └── fixtures/ # OPDS 1.2 XML fixtures
│ ├── root.xml
│ ├── acquisition.xml
│ └── facets.xml
├── test_parser.py # OPDS 2.0 parser tests
├── test_client.py # OPDS 2.0 client tests
├── test_acquisition.py # Acquisition tests
├── test_exceptions.py # Exception tests
├── test_integration.py # Integration tests
├── test_utils.py # Utility tests
└── fixtures/ # Test fixtures (JSON & XML)
├── opds12/ # OPDS 1.2 XML fixtures
│ ├── root.xml
│ ├── acquisition.xml
│ └── facets.xml
├── feed.json
├── publication.json
├── facets.json
└── groups.json
License
MIT License - see LICENSE file for details.
References
Metadata
Release files for opds-client 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| opds_client-0.0.1.tar.gz | 26.0 kB | With provenance | Details for opds_client-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| opds_client-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for opds_client-0.0.1-py3-none-any.whl |
Total release size: 45.7 kB
Release files / opds_client-0.0.1.tar.gz
| Download URL | opds_client-0.0.1.tar.gz |
|---|---|
| Size | 26.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82eb29ec370f52c2df9b8a5f26d44cab8c6589ac559de3422204af2ddc1195db
|
|
BLAKE2b-256 checksum How to use checksums |
a0aa41ec842c2e54be4db7ab9349ee2cfb3feb54c573c3a94a550805af13ae9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.
Transparency logRelease files / opds_client-0.0.1-py3-none-any.whl
| Download URL | opds_client-0.0.1-py3-none-any.whl |
|---|---|
| Size | 19.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1110a1b4718ef56a01d361226d2e936e1b652a22349581d156c8c84b91bc5ef2
|
|
BLAKE2b-256 checksum How to use checksums |
bf414e0d435b57eb6fee1fd6faac8f01cdf49c0a8bb3e5f4fc1536b9299718c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.
Transparency logSecurity for opds-client 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for opds_client-0.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.