Skip to content

Commit 3777f95

Browse files
Copilotmekarpeles
andauthored
Update tests for new Catalog.create() API and rename package to pyopds2 (#19)
* Add GitHub Actions workflow for linting and testing * Fix tests and library errors for new Catalog.create() API * Apply linting fixes for formatting and whitespace * Rename package from opds2 to pyopds2 * run for py3.11 and 3.12 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Michael E. Karpeles (Mek) <michael.karpeles@gmail.com>
1 parent decd070 commit 3777f95

File tree

9 files changed

+353
-405
lines changed

9 files changed

+353
-405
lines changed

.github/workflows/test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Python Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.11', '3.12']
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e .[dev]
28+
pip install flake8
29+
30+
- name: Lint with flake8
31+
run: |
32+
# Stop the build if there are Python syntax errors or undefined names
33+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
34+
# Exit-zero treats all errors as warnings
35+
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
36+
37+
- name: Run tests with pytest
38+
run: |
39+
pytest tests/ -v --cov=opds2 --cov-report=term-missing
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
__version__ = "0.1.0"
88

9-
from opds2.models import (
9+
from pyopds2.models import (
1010
Catalog,
1111
Contributor,
1212
Link,
1313
Metadata,
1414
Navigation,
1515
Publication,
1616
)
17-
from opds2.provider import DataProvider, DataProviderRecord
17+
from pyopds2.provider import DataProvider, DataProviderRecord
1818

1919
__all__ = [
2020
"Catalog",
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from urllib.parse import urlencode, urlparse, parse_qsl, urlunparse
22

3-
def build_url(base_url: str, params: dict[str, str] | None = None) -> str:
3+
4+
def build_url(base_url: str, params: dict[str, str] | None = None) -> str:
45
if not params:
56
return base_url
67

@@ -9,4 +10,4 @@ def build_url(base_url: str, params: dict[str, str] | None = None) -> str:
910
query.update(params)
1011
url_parts[4] = urlencode(query, doseq=True) # doseq=True supports lists
1112

12-
return urlunparse(url_parts)
13+
return urlunparse(url_parts)

opds2/models.py renamed to pyopds2/models.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55

66
from datetime import datetime
77
from typing import TYPE_CHECKING, Any, Dict, List, Optional
8-
from urllib.parse import urlencode
98

10-
from pydantic import BaseModel, Field # field_validator
9+
from pydantic import BaseModel, Field # field_validator
1110

1211

1312
if TYPE_CHECKING:
14-
from opds2.provider import DataProvider
13+
from pyopds2.provider import DataProvider
1514

1615

1716
class Link(BaseModel):
1817
"""Represents a link in OPDS 2.0.
19-
18+
2019
Links are used to associate resources with a publication or catalog.
2120
"""
2221
href: str = Field(..., description="URI or URI template of the linked resource")
@@ -42,7 +41,7 @@ class Contributor(BaseModel):
4241

4342
class Metadata(BaseModel):
4443
"""Metadata for a publication or catalog.
45-
44+
4645
Contains descriptive information about the resource.
4746
"""
4847
title: str = Field(..., description="Title of the resource")
@@ -65,7 +64,7 @@ class Metadata(BaseModel):
6564

6665
class Publication(BaseModel):
6766
"""Represents a publication in OPDS 2.0.
68-
67+
6968
A publication is a digital work (book, audiobook, etc.) with metadata and links.
7069
"""
7170
metadata: Metadata = Field(..., description="Metadata about the publication")
@@ -93,13 +92,13 @@ class Navigation(BaseModel):
9392

9493
class Catalog(BaseModel):
9594
"""Represents an OPDS 2.0 catalog/feed.
96-
95+
9796
A catalog is a collection of publications with optional navigation.
9897
"""
9998
metadata: Metadata = Field(..., description="Metadata about the catalog")
10099
links: List[Link] = Field(default_factory=list, description="Links (self, search, etc.)")
101100
publications: Optional[List[Publication]] = Field(
102-
None,
101+
None,
103102
description="List of publications in this catalog"
104103
)
105104
navigation: Optional[List[Navigation]] = Field(
@@ -144,7 +143,7 @@ def model_dump_json(self, **kwargs) -> str:
144143
# Use model_dump to get the dict with @context, then serialize to JSON
145144
data = self.model_dump(**kwargs)
146145
return json.dumps(data, default=str)
147-
146+
148147
def add_pagination(self, response: 'DataProvider.SearchResponse'):
149148
"""
150149
Add pagination to the current Catalog based on the SearchResponse.
@@ -196,7 +195,7 @@ def add_pagination(self, response: 'DataProvider.SearchResponse'):
196195

197196
@classmethod
198197
def create(
199-
cls,
198+
cls,
200199
response: Optional['DataProvider.SearchResponse'] = None,
201200
paginate: bool = True,
202201
# Catalog properties
@@ -209,12 +208,12 @@ def create(
209208
) -> 'Catalog':
210209
"""
211210
Create an OPDS Catalog, optionally from search results.
212-
211+
213212
Args:
214213
response: Optional SearchResponse for paginated search results
215214
paginate: Whether to add pagination links (requires data)
216215
"""
217-
metadata = metadata or Metadata()
216+
metadata = metadata or Metadata(title="OPDS Catalog")
218217
links = links or []
219218
publications = publications or []
220219

@@ -229,10 +228,16 @@ def create(
229228

230229
if response:
231230
if publications:
232-
raise ValueError("Cannot specify both publications and response parameters - publications are generated from the response")
233-
catalog.publications = [record.to_publication() for record in response.records]
234-
235-
if paginate:
231+
raise ValueError(
232+
"Cannot specify both publications and response "
233+
"parameters - publications are generated from "
234+
"the response"
235+
)
236+
catalog.publications = [
237+
record.to_publication() for record in response.records
238+
]
239+
240+
if paginate:
236241
catalog.add_pagination(response)
237242

238243
return catalog
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from typing import List, Optional
1010
from pydantic import BaseModel
1111

12-
from opds2.models import Metadata, Publication, Link
13-
from opds2.helpers import build_url
12+
from pyopds2.models import Metadata, Publication, Link
13+
from pyopds2.helpers import build_url
1414

1515

1616
class DataProviderRecord(BaseModel, ABC):
1717
"""Abstract base class for data records returned by DataProvider.
18-
18+
1919
Consumers of this library should extend this class to define
2020
their own data record structure.
2121
"""
@@ -43,20 +43,21 @@ def to_publication(self) -> Publication:
4343
images=self.images()
4444
)
4545

46+
4647
class DataProvider(ABC):
4748
"""Abstract base class for OPDS 2.0 data providers.
48-
49+
4950
Consumers of this library should extend this class to provide
5051
their own implementation for searching and retrieving publications.
51-
52+
5253
Example:
5354
class MyDataProvider(DataProvider):
5455
def search(self, query: str, limit: int = 50, offset: int = 0) -> List[Publication]:
5556
# Implement search logic
5657
results = my_search_function(query, limit, offset)
5758
return [self._to_publication(item) for item in results]
5859
"""
59-
60+
6061
TITLE: str = "Generic OPDS Service"
6162

6263
BASE_URL: str = "http://localhost"
@@ -99,12 +100,12 @@ def params(self) -> dict:
99100
def page(self) -> int:
100101
"""Calculate current page number based on offset and limit."""
101102
return (self.offset // self.limit) + 1 if self.limit else 1
102-
103+
103104
@property
104105
def last_page(self) -> int:
105106
"""Calculate last page number based on total and limit."""
106107
return (self.total + self.limit - 1) // self.limit
107-
108+
108109
@property
109110
def has_more(self) -> bool:
110111
"""Determine if there are more results beyond the current page."""
@@ -119,13 +120,13 @@ def search(
119120
sort: Optional[str] = None,
120121
) -> 'DataProvider.SearchResponse':
121122
"""Search for publications matching the query.
122-
123+
123124
Args:
124125
query: Search query string
125126
limit: Maximum number of results to return (default: 50)
126127
offset: Offset for pagination (default: 0)
127128
sort: Optional sorting parameter
128-
129+
129130
Returns:
130131
SearchResponse object containing search results
131132
"""

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "opds2"
6+
name = "pyopds2"
77
version = "0.1.0"
88
description = "A Python library for generating OPDS 2.0 compliant feeds"
99
readme = "README.md"
@@ -34,5 +34,5 @@ dev = [
3434
]
3535

3636
[tool.setuptools.packages.find]
37-
include = ["opds2*"]
37+
include = ["pyopds2*"]
3838
exclude = ["tests*"]

0 commit comments

Comments
 (0)