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.
@@ -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
+ [![CI - Mainline](https://github.com/sevdani/opds-client/actions/workflows/mainline.yml/badge.svg)](https://github.com/sevdani/opds-client/actions/workflows/mainline.yml)
34
+ [![CI - Features](https://github.com/sevdani/opds-client/actions/workflows/features.yml/badge.svg)](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)