opds-client 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opds_client-0.0.1/LICENSE +21 -0
- opds_client-0.0.1/PKG-INFO +521 -0
- opds_client-0.0.1/README.md +497 -0
- opds_client-0.0.1/opds_client/__init__.py +47 -0
- opds_client-0.0.1/opds_client/client.py +385 -0
- opds_client-0.0.1/opds_client/exceptions.py +93 -0
- opds_client-0.0.1/opds_client/models/__init__.py +51 -0
- opds_client-0.0.1/opds_client/models/acquisition.py +85 -0
- opds_client-0.0.1/opds_client/models/base.py +189 -0
- opds_client-0.0.1/opds_client/models/feed.py +256 -0
- opds_client-0.0.1/opds_client/models/publication.py +205 -0
- opds_client-0.0.1/opds_client/parser.py +262 -0
- opds_client-0.0.1/opds_client/utils.py +192 -0
- opds_client-0.0.1/opds_client.egg-info/PKG-INFO +521 -0
- opds_client-0.0.1/opds_client.egg-info/SOURCES.txt +24 -0
- opds_client-0.0.1/opds_client.egg-info/dependency_links.txt +1 -0
- opds_client-0.0.1/opds_client.egg-info/requires.txt +13 -0
- opds_client-0.0.1/opds_client.egg-info/top_level.txt +1 -0
- opds_client-0.0.1/pyproject.toml +92 -0
- opds_client-0.0.1/setup.cfg +4 -0
- opds_client-0.0.1/tests/test_acquisition.py +114 -0
- opds_client-0.0.1/tests/test_client.py +591 -0
- opds_client-0.0.1/tests/test_exceptions.py +80 -0
- opds_client-0.0.1/tests/test_integration.py +265 -0
- opds_client-0.0.1/tests/test_parser.py +223 -0
- opds_client-0.0.1/tests/test_utils.py +131 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sevy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opds-client
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: OPDS 2.0 Client Library for Python
|
|
5
|
+
Author: OPDS Client Authors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sevdani/opds-client
|
|
8
|
+
Project-URL: Issues, https://github.com/sevdani/opds-client/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: requests>=2.28.0
|
|
13
|
+
Requires-Dist: pydantic>=2.0.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.0.292; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
20
|
+
Provides-Extra: release
|
|
21
|
+
Requires-Dist: python-semantic-release>=10.0.0; extra == "release"
|
|
22
|
+
Requires-Dist: build>=1.0.0; extra == "release"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# OPDS Client
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="./assets/logo.svg" alt="Logo" width="150" />
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
A Python library for working with OPDS catalogs, supporting both [OPDS 1.2](https://specs.opds.io/opds-1.2) (Atom/XML) and [OPDS 2.0](https://specs.opds.io/opds-2.0) (JSON) specifications.
|
|
32
|
+
|
|
33
|
+
[](https://github.com/sevdani/opds-client/actions/workflows/mainline.yml)
|
|
34
|
+
[](https://github.com/sevdani/opds-client/actions/workflows/features.yml)
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
### OPDS 2.0 (JSON)
|
|
39
|
+
- ✅ Parse OPDS 2.0 feeds and publications
|
|
40
|
+
- ✅ Validate against OPDS 2.0 specification
|
|
41
|
+
- ✅ Navigate navigation, publications, groups, and facets collections
|
|
42
|
+
- ✅ Handle pagination (first, previous, next, last links)
|
|
43
|
+
- ✅ Search catalogs with URI template resolution
|
|
44
|
+
- ✅ Work with acquisition links (download, buy, borrow, etc.)
|
|
45
|
+
- ✅ HTTP error handling with retries
|
|
46
|
+
- ✅ Pydantic models for type safety
|
|
47
|
+
|
|
48
|
+
### OPDS 1.2 (Atom/XML)
|
|
49
|
+
- ✅ Parse OPDS 1.2 Atom feeds and entries
|
|
50
|
+
- ✅ Dublin Core metadata support
|
|
51
|
+
- ✅ Navigation and Acquisition feeds
|
|
52
|
+
- ✅ Facet groups with active facets and item counts
|
|
53
|
+
- ✅ Acquisition links with prices and indirect acquisitions
|
|
54
|
+
- ✅ OpenSearch integration
|
|
55
|
+
- ✅ Multiple acquisition relations (buy, borrow, subscribe, etc.)
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install opds-client
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### OPDS 2.0 (JSON)
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from opds_client import OPDSClient
|
|
69
|
+
|
|
70
|
+
# Create a client
|
|
71
|
+
client = OPDSClient()
|
|
72
|
+
|
|
73
|
+
# Fetch a feed
|
|
74
|
+
feed = client.fetch_feed("https://example.com/opds")
|
|
75
|
+
|
|
76
|
+
# Access feed metadata
|
|
77
|
+
print(f"Title: {feed.metadata.title}")
|
|
78
|
+
print(f"Publications: {len(feed.publications or [])}")
|
|
79
|
+
|
|
80
|
+
# Navigate navigation links
|
|
81
|
+
for nav in feed.navigation or []:
|
|
82
|
+
print(f"- {nav.title}: {nav.href}")
|
|
83
|
+
|
|
84
|
+
# Search
|
|
85
|
+
results = client.search(feed, "query")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### OPDS 1.2 (Atom/XML)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from opds_client import OPDS12Client
|
|
92
|
+
|
|
93
|
+
# Create a client for OPDS 1.2
|
|
94
|
+
client = OPDS12Client()
|
|
95
|
+
|
|
96
|
+
# Fetch a feed (XML format)
|
|
97
|
+
feed = client.fetch_feed("https://example.com/opds.xml")
|
|
98
|
+
|
|
99
|
+
# Access feed metadata
|
|
100
|
+
print(f"Title: {feed.title}")
|
|
101
|
+
print(f"Entries: {len(feed.entries or [])}")
|
|
102
|
+
|
|
103
|
+
# Navigate navigation links
|
|
104
|
+
start_link = feed.get_start_link()
|
|
105
|
+
|
|
106
|
+
# Get acquisition links from entries
|
|
107
|
+
if feed.entries:
|
|
108
|
+
entry = feed.entries[0]
|
|
109
|
+
acq_links = entry.get_acquisition_links()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Using Base URL (Origin)
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
### OPDS 2.0 Example
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from opds_client import OPDSClient
|
|
120
|
+
|
|
121
|
+
# Create client with origin
|
|
122
|
+
client = OPDSClient(origin="https://example.com")
|
|
123
|
+
|
|
124
|
+
# Fetch feed with relative path
|
|
125
|
+
feed = client.fetch_feed("/catalog") # → https://example.com/catalog
|
|
126
|
+
|
|
127
|
+
# Absolute URLs are used as-is
|
|
128
|
+
feed = client.fetch_feed("https://other.com/catalog") # → https://other.com/catalog
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### OPDS 1.2 Example
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from opds_client import OPDS12Client
|
|
135
|
+
|
|
136
|
+
# Create client with origin
|
|
137
|
+
client = OPDS12Client(origin="https://api.example.com")
|
|
138
|
+
|
|
139
|
+
# Fetch feed with relative path
|
|
140
|
+
feed = client.fetch_feed("/opds.xml") # → https://api.example.com/opds.xml
|
|
141
|
+
|
|
142
|
+
# Navigation links with origin
|
|
143
|
+
start_link = feed.get_start_link()
|
|
144
|
+
if start_link:
|
|
145
|
+
# The client automatically prepends the origin to relative URLs
|
|
146
|
+
root_feed = client.get_start_link(feed)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## Authentication
|
|
151
|
+
|
|
152
|
+
Both OPDS 1.2 and OPDS 2.0 clients support HTTP Basic Authentication.
|
|
153
|
+
|
|
154
|
+
### Basic Auth with Username/Password
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from opds_client import OPDSClient
|
|
158
|
+
|
|
159
|
+
# For OPDS 2.0
|
|
160
|
+
client = OPDSClient(username="your_username", password="your_password")
|
|
161
|
+
feed = client.fetch_feed("https://example.com/opds")
|
|
162
|
+
|
|
163
|
+
# For OPDS 1.2
|
|
164
|
+
client = OPDS12Client(username="your_username", password="your_password")
|
|
165
|
+
feed = client.fetch_feed("https://example.com/opds.xml")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Custom Auth Objects
|
|
169
|
+
|
|
170
|
+
For advanced authentication scenarios (Digest Auth, Bearer tokens, custom auth):
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from opds_client import OPDSClient
|
|
174
|
+
from requests.auth import HTTPDigestAuth, HTTPBasicAuth
|
|
175
|
+
|
|
176
|
+
# Digest authentication
|
|
177
|
+
client = OPDSClient(auth=HTTPDigestAuth("username", "password"))
|
|
178
|
+
feed = client.fetch_feed("https://example.com/opds")
|
|
179
|
+
|
|
180
|
+
# Custom auth (e.g., with session tokens)
|
|
181
|
+
class CustomAuth:
|
|
182
|
+
def __call__(self, r):
|
|
183
|
+
r.headers["X-Custom-Token"] = "your-token"
|
|
184
|
+
return r
|
|
185
|
+
|
|
186
|
+
client = OPDSClient(auth=CustomAuth())
|
|
187
|
+
feed = client.fetch_feed("https://example.com/opds")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Models
|
|
191
|
+
|
|
192
|
+
### OPDS 2.0 Models
|
|
193
|
+
|
|
194
|
+
#### Link
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from opds_client.models import Link, AcquisitionLink
|
|
198
|
+
|
|
199
|
+
# Basic link
|
|
200
|
+
link = Link(href="https://example.com", rel="self")
|
|
201
|
+
|
|
202
|
+
# Acquisition link
|
|
203
|
+
acq_link = AcquisitionLink(
|
|
204
|
+
href="https://example.com/download",
|
|
205
|
+
rel="download",
|
|
206
|
+
type="application/epub+zip",
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### OPDS 1.2 Models
|
|
211
|
+
|
|
212
|
+
##### Link
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from opds_client.opds12.models import Link, AcquisitionLink
|
|
216
|
+
|
|
217
|
+
# Basic link
|
|
218
|
+
link = Link(
|
|
219
|
+
href="https://example.com",
|
|
220
|
+
rel="http://opds-spec.org/acquisition",
|
|
221
|
+
type="application/epub+zip",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Acquisition link with price
|
|
225
|
+
acq_link = AcquisitionLink(
|
|
226
|
+
href="https://example.com/buy",
|
|
227
|
+
rel="http://opds-spec.org/acquisition/buy",
|
|
228
|
+
properties={"price": Price(value="19.99", currencycode="USD")},
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
##### Entry
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from opds_client.opds12.models import Entry, Link, Price
|
|
236
|
+
|
|
237
|
+
entry = Entry(
|
|
238
|
+
id="urn:uuid:test",
|
|
239
|
+
title="Test Entry",
|
|
240
|
+
updated="2010-01-10T10:00:00Z",
|
|
241
|
+
authors=["Test Author"],
|
|
242
|
+
links=[
|
|
243
|
+
Link(
|
|
244
|
+
href="https://example.com",
|
|
245
|
+
rel="http://opds-spec.org/acquisition",
|
|
246
|
+
type="application/epub+zip",
|
|
247
|
+
),
|
|
248
|
+
],
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
##### Feed
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from opds_client.opds12.models import Feed, Link, Entry
|
|
256
|
+
|
|
257
|
+
feed = Feed(
|
|
258
|
+
id="urn:uuid:feed",
|
|
259
|
+
title="OPDS 1.2 Feed",
|
|
260
|
+
updated="2010-01-10T10:00:00Z",
|
|
261
|
+
links=[Link(href="https://example.com", rel="self")],
|
|
262
|
+
entries=[entry],
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Publication (OPDS 2.0)
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from opds_client.models import OPDSPublication, Metadata, Link
|
|
270
|
+
|
|
271
|
+
pub = OPDSPublication(
|
|
272
|
+
metadata=Metadata(
|
|
273
|
+
title="Test Book",
|
|
274
|
+
author="Test Author",
|
|
275
|
+
),
|
|
276
|
+
links=[
|
|
277
|
+
Link(href="https://example.com", rel="self"),
|
|
278
|
+
Link(href="https://example.com/download", rel="download"),
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Feed (OPDS 2.0)
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from opds_client.models import OPDSFeed
|
|
287
|
+
|
|
288
|
+
# Feed with collections
|
|
289
|
+
feed = OPDSFeed(
|
|
290
|
+
metadata=Metadata(title="My Catalog"),
|
|
291
|
+
links=[Link(href="https://example.com", rel="self")],
|
|
292
|
+
navigation=[...],
|
|
293
|
+
publications=[...],
|
|
294
|
+
facets=[...],
|
|
295
|
+
groups=[...],
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Usage Examples
|
|
300
|
+
|
|
301
|
+
### OPDS 2.0 Examples
|
|
302
|
+
|
|
303
|
+
#### Navigation
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from opds_client import OPDSClient
|
|
307
|
+
|
|
308
|
+
client = OPDSClient()
|
|
309
|
+
feed = client.fetch_feed("https://example.com/opds")
|
|
310
|
+
|
|
311
|
+
# Follow navigation by title
|
|
312
|
+
new_feed = client.follow_navigation(feed, "New Publications")
|
|
313
|
+
|
|
314
|
+
# Navigate facets
|
|
315
|
+
featured_feed = client.follow_facet(feed, "Category", "Featured")
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### OPDS 1.2 Examples
|
|
319
|
+
|
|
320
|
+
##### Navigation
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from opds_client import OPDS12Client
|
|
324
|
+
|
|
325
|
+
client = OPDS12Client()
|
|
326
|
+
feed = client.fetch_feed("https://example.com/opds.xml")
|
|
327
|
+
|
|
328
|
+
# Get start link (catalog root)
|
|
329
|
+
start_link = feed.get_start_link()
|
|
330
|
+
|
|
331
|
+
# Get self link
|
|
332
|
+
self_link = feed.get_self_link()
|
|
333
|
+
|
|
334
|
+
# Get next page
|
|
335
|
+
next_feed = client.get_next_page(feed)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Pagination
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
```python
|
|
342
|
+
# Get next page
|
|
343
|
+
next_feed = client.get_next_page(feed)
|
|
344
|
+
|
|
345
|
+
# Get previous page
|
|
346
|
+
prev_feed = client.get_previous_page(feed)
|
|
347
|
+
|
|
348
|
+
# Get first/last page
|
|
349
|
+
first = client.get_first_page(feed)
|
|
350
|
+
last = client.get_last_page(feed)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### OPDS 1.2 Pagination
|
|
354
|
+
|
|
355
|
+
```python
|
|
356
|
+
from opds_client import OPDS12Client
|
|
357
|
+
|
|
358
|
+
client = OPDS12Client()
|
|
359
|
+
feed = client.fetch_feed("https://example.com/opds.xml")
|
|
360
|
+
|
|
361
|
+
# Get next page
|
|
362
|
+
next_feed = client.get_next_page(feed)
|
|
363
|
+
|
|
364
|
+
# Get previous page
|
|
365
|
+
prev_feed = client.get_previous_page(feed)
|
|
366
|
+
|
|
367
|
+
# Get catalog root
|
|
368
|
+
root_feed = client.get_start_link(feed)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Search
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
```python
|
|
375
|
+
# Search the catalog
|
|
376
|
+
results = client.search(feed, "search query", title="My Book")
|
|
377
|
+
|
|
378
|
+
# Check if search is supported
|
|
379
|
+
if feed.is_searchable():
|
|
380
|
+
results = client.search(feed, "query")
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### OPDS 1.2 Acquisition
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from opds_client import OPDS12Client
|
|
387
|
+
|
|
388
|
+
client = OPDS12Client()
|
|
389
|
+
feed = client.fetch_feed("https://example.com/opds.xml")
|
|
390
|
+
|
|
391
|
+
# Get acquisition links from entries
|
|
392
|
+
entry = feed.entries[0]
|
|
393
|
+
acq_links = entry.get_acquisition_links()
|
|
394
|
+
|
|
395
|
+
# Follow specific acquisition type
|
|
396
|
+
buy_link = client.follow_acquisition(entry, acquisition_type="buy")
|
|
397
|
+
if buy_link:
|
|
398
|
+
price = buy_link.get_price()
|
|
399
|
+
print(f"Price: {price.currencycode} {price.value}")
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Acquisition
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from opds_client.models import AcquisitionManager
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
# Get acquisition links
|
|
409
|
+
pub = client.fetch_publication("https://example.com/publication")
|
|
410
|
+
acq_links = pub.get_acquisition_links()
|
|
411
|
+
|
|
412
|
+
# Use acquisition manager
|
|
413
|
+
manager = AcquisitionManager(acq_links[0])
|
|
414
|
+
|
|
415
|
+
# Check for open access
|
|
416
|
+
if manager.get_open_access_link():
|
|
417
|
+
# Download directly
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# Check for buy link
|
|
421
|
+
buy_link = manager.get_buy_link()
|
|
422
|
+
if buy_link:
|
|
423
|
+
price = buy_link.get_price()
|
|
424
|
+
print(f"Price: {price.currency} {price.value}")
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## OPDS 1.2 vs OPDS 2.0
|
|
428
|
+
|
|
429
|
+
| Feature | OPDS 1.2 | OPDS 2.0 |
|
|
430
|
+
|---------|----------|----------|
|
|
431
|
+
| Format | Atom/XML | JSON |
|
|
432
|
+
| Parser | `OPDS12Parser` | `OPDSParser` |
|
|
433
|
+
| Client | `OPDS12Client` | `OPDSClient` |
|
|
434
|
+
| Feed Type | `Feed` | `OPDSFeed` |
|
|
435
|
+
| Entry Type | `Entry` | `OPDSPublication` |
|
|
436
|
+
| Metadata | Dublin Core + Atom | JSON-LD (schema.org) |
|
|
437
|
+
| Acquisition Links | XML `opds:price` | JSON `properties.price` |
|
|
438
|
+
|
|
439
|
+
Choose the appropriate client based on your catalog's OPDS version.
|
|
440
|
+
|
|
441
|
+
## Testing
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
# Install dev dependencies
|
|
445
|
+
pip install -e ".[dev]"
|
|
446
|
+
|
|
447
|
+
# Run tests
|
|
448
|
+
pytest
|
|
449
|
+
|
|
450
|
+
# Run with coverage
|
|
451
|
+
pytest --cov=opds_client
|
|
452
|
+
|
|
453
|
+
# Run type checks
|
|
454
|
+
mypy opds_client
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Project Structure
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
opds_client/
|
|
461
|
+
├── __init__.py # Package exports (OPDS 1.2 & 2.0)
|
|
462
|
+
├── client.py # OPDS 2.0 Client
|
|
463
|
+
├── parser.py # JSON parsing/validation (OPDS 2.0)
|
|
464
|
+
├── exceptions.py # Custom exceptions
|
|
465
|
+
├── utils.py # Utility functions
|
|
466
|
+
├── models/ # OPDS 2.0 models
|
|
467
|
+
│ ├── __init__.py
|
|
468
|
+
│ ├── base.py # Base types (Link, Metadata)
|
|
469
|
+
│ ├── publication.py # Publication models
|
|
470
|
+
│ ├── feed.py # Feed/Collection models
|
|
471
|
+
│ └── acquisition.py # Acquisition models
|
|
472
|
+
└── opds12/ # OPDS 1.2 specific module
|
|
473
|
+
├── __init__.py
|
|
474
|
+
├── client.py # OPDS 1.2 Client
|
|
475
|
+
├── parser.py # Atom/XML parsing (OPDS 1.2)
|
|
476
|
+
├── models.py # OPDS 1.2 models
|
|
477
|
+
├── namespace.py # XML namespace constants
|
|
478
|
+
└── exceptions.py # OPDS 1.2 exceptions
|
|
479
|
+
|
|
480
|
+
tests/
|
|
481
|
+
├── conftest.py
|
|
482
|
+
├── models/ # OPDS 2.0 model tests (mirrors opds_client/models/)
|
|
483
|
+
│ ├── __init__.py
|
|
484
|
+
│ ├── test_base.py # Base types (Link, Metadata)
|
|
485
|
+
│ ├── test_publication.py # Publication models
|
|
486
|
+
│ └── test_feed.py # Feed/Collection models
|
|
487
|
+
├── opds12/ # OPDS 1.2 tests (mirrors opds_client/opds12/)
|
|
488
|
+
│ ├── __init__.py
|
|
489
|
+
│ ├── test_models.py # OPDS 1.2 models
|
|
490
|
+
│ ├── test_parser.py # Atom/XML parsing
|
|
491
|
+
│ ├── test_client.py # OPDS 1.2 Client
|
|
492
|
+
│ └── fixtures/ # OPDS 1.2 XML fixtures
|
|
493
|
+
│ ├── root.xml
|
|
494
|
+
│ ├── acquisition.xml
|
|
495
|
+
│ └── facets.xml
|
|
496
|
+
├── test_parser.py # OPDS 2.0 parser tests
|
|
497
|
+
├── test_client.py # OPDS 2.0 client tests
|
|
498
|
+
├── test_acquisition.py # Acquisition tests
|
|
499
|
+
├── test_exceptions.py # Exception tests
|
|
500
|
+
├── test_integration.py # Integration tests
|
|
501
|
+
├── test_utils.py # Utility tests
|
|
502
|
+
└── fixtures/ # Test fixtures (JSON & XML)
|
|
503
|
+
├── opds12/ # OPDS 1.2 XML fixtures
|
|
504
|
+
│ ├── root.xml
|
|
505
|
+
│ ├── acquisition.xml
|
|
506
|
+
│ └── facets.xml
|
|
507
|
+
├── feed.json
|
|
508
|
+
├── publication.json
|
|
509
|
+
├── facets.json
|
|
510
|
+
└── groups.json
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## License
|
|
514
|
+
|
|
515
|
+
MIT License - see LICENSE file for details.
|
|
516
|
+
|
|
517
|
+
## References
|
|
518
|
+
|
|
519
|
+
- [OPDS 2.0 Specification](https://specs.opds.io/opds-2.0)
|
|
520
|
+
- [OPDS 1.2 Specification](https://specs.opds.io/opds-1.2)
|
|
521
|
+
- [GitHub: sevdani/opds-client](https://github.com/sevdani/opds-client)
|