Skip to content

Commit bdf4195

Browse files
authored
Refactor DataProvider and implement Search class
1 parent 7f69c4f commit bdf4195

File tree

1 file changed

+59
-72
lines changed

1 file changed

+59
-72
lines changed

pyopds2/provider.py

Lines changed: 59 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
"""
55

66
from abc import ABC, abstractmethod
7-
from dataclasses import dataclass
8-
import functools
7+
from collections.abc import Mapping
98
from typing import List, Optional
109
from pydantic import BaseModel
1110

@@ -44,102 +43,90 @@ def to_publication(self) -> Publication:
4443
)
4544

4645

46+
class Search(BaseModel, Mapping):
47+
query: str
48+
limit: int = 50
49+
offset: int = 0
50+
sort: Optional[str] = None
51+
52+
def __iter__(self):
53+
"""Allows **Search(...) to unpack into a dict for DataProvider.search(**s)"""
54+
return iter(self.model_dump())
55+
56+
def __getitem__(self, item):
57+
return getattr(self, item)
58+
59+
def __len__(self):
60+
return len(self.model_fields)
61+
62+
@property
63+
def params(self) -> dict[str, str]:
64+
d = self.model_dump(exclude_none=True)
65+
return {k: str(v) for k, v in d.items()}
66+
67+
68+
class SearchResponse(BaseModel):
69+
"""Response from a search query."""
70+
provider: type["DataProvider"]
71+
search: Search
72+
records: List[DataProviderRecord]
73+
total: int
74+
75+
@property
76+
def params(self) -> dict[str, str]:
77+
return self.search.params
78+
79+
@property
80+
def page(self) -> int:
81+
"""Calculate current page number based on offset and limit."""
82+
if self.search.limit <= 0: return 1
83+
return (self.search.offset // self.search.limit) + 1
84+
85+
@property
86+
def last_page(self) -> int:
87+
"""Calculate last page number based on total and limit."""
88+
if self.search.limit <= 0 or self.total == 0: return 1
89+
return (self.total + self.search.limit - 1) // self.search.limit
90+
91+
@property
92+
def has_more(self) -> bool:
93+
"""Determine if there are more results beyond the current page."""
94+
return (self.search.offset + self.search.limit) < self.total
95+
96+
4797
class DataProvider(ABC):
4898
"""Abstract base class for OPDS 2.0 data providers.
4999
50100
Consumers of this library should extend this class to provide
51101
their own implementation for searching and retrieving publications.
52-
53-
Example:
54-
class MyDataProvider(DataProvider):
55-
def search(self, query: str, limit: int = 50, offset: int = 0) -> List[Publication]:
56-
# Implement search logic
57-
results = my_search_function(query, limit, offset)
58-
return [self._to_publication(item) for item in results]
59102
"""
60103

61104
TITLE: str = "Generic OPDS Service"
62-
63105
BASE_URL: str = "http://localhost"
64-
"""The base url for the data provider."""
65-
66106
SEARCH_URL: str = "/opds/search{?query}"
67-
"""The relative url template for search queries."""
68-
69-
@dataclass
70-
class SearchResponse:
71-
"""Response from a search query."""
72-
provider: 'DataProvider | type[DataProvider]'
73-
query: str
74-
limit: int
75-
offset: int
76-
sort: Optional[str]
77-
records: List[DataProviderRecord]
78-
total: int
79-
title: Optional[str] = None
80-
81-
def get_search_url(self, **kwargs: str) -> str:
82-
base_url = self.provider.SEARCH_URL.replace("{?query}", "")
83-
if base_url.startswith("/"):
84-
base_url = self.provider.BASE_URL.rstrip('/') + base_url
85-
return build_url(base_url, params=self.params | kwargs)
86-
87-
@functools.cached_property
88-
def params(self) -> dict:
89-
p: dict[str, str] = {}
90-
if self.query:
91-
p["query"] = self.query
92-
if self.limit:
93-
p["limit"] = str(self.limit)
94-
if self.page > 1:
95-
p["page"] = str(self.page)
96-
if self.sort:
97-
p["sort"] = self.sort
98-
return p
99-
100-
@property
101-
def page(self) -> int:
102-
"""Calculate current page number based on offset and limit."""
103-
return (self.offset // self.limit) + 1 if self.limit else 1
104-
105-
@property
106-
def last_page(self) -> int:
107-
"""Calculate last page number based on total and limit."""
108-
return (self.total + self.limit - 1) // self.limit
109-
110-
@property
111-
def has_more(self) -> bool:
112-
"""Determine if there are more results beyond the current page."""
113-
return (self.offset + self.limit) < self.total
114-
115-
@staticmethod
116-
@abstractmethod
107+
108+
@classmethod
117109
def search(
110+
cls,
118111
query: str,
119112
limit: int = 50,
120113
offset: int = 0,
121114
sort: Optional[str] = None,
122-
title: Optional[str] = None,
123-
) -> 'DataProvider.SearchResponse':
115+
) -> SearchResponse:
124116
"""Search for publications matching the query.
125117
126118
Args:
127119
query: Search query string
128120
limit: Maximum number of results to return (default: 50)
129121
offset: Offset for pagination (default: 0)
130122
sort: Optional sorting parameter
131-
title: Title to pass along to Catalog.create
132123
133124
Returns:
134125
SearchResponse object containing search results
135126
"""
136-
return DataProvider.SearchResponse(
137-
DataProvider,
127+
return SearchResponse(
128+
provider=cls,
129+
search=Search(query=query, limit=limit, offset=offset, sort=sort),
138130
records=[],
139131
total=0,
140-
query=query,
141-
limit=limit,
142-
offset=offset,
143-
sort=sort,
144-
title=title,
145132
)

0 commit comments

Comments
 (0)