|
13 | 13 | from datetime import timezone |
14 | 14 | from types import MappingProxyType |
15 | 15 | from typing import Any |
| 16 | +from typing import IO |
16 | 17 | from typing import Literal |
17 | 18 | from typing import overload |
18 | 19 | from typing import Self |
|
40 | 41 | from ._utils import MapContextManager |
41 | 42 | from ._utils import zero_or_one |
42 | 43 | from .exceptions import EntryNotFoundError |
| 44 | +from .exceptions import FeedError |
43 | 45 | from .exceptions import FeedExistsError |
44 | 46 | from .exceptions import FeedNotFoundError |
45 | 47 | from .exceptions import ParseError |
|
64 | 66 | from .types import EntryUpdateStatus |
65 | 67 | from .types import Feed |
66 | 68 | from .types import FeedCounts |
| 69 | +from .types import FeedExport |
| 70 | +from .types import FeedImportResult |
67 | 71 | from .types import FeedInput |
68 | 72 | from .types import FeedSort |
69 | 73 | from .types import JSONType |
|
76 | 80 | from .types import UpdateResult |
77 | 81 |
|
78 | 82 | if TYPE_CHECKING: # pragma: no cover |
| 83 | + from . import opml |
79 | 84 | from ._parser import Parser |
80 | 85 |
|
81 | 86 |
|
@@ -2181,6 +2186,80 @@ def delete_tag( |
2181 | 2186 | if not missing_ok: |
2182 | 2187 | raise |
2183 | 2188 |
|
| 2189 | + def import_feeds(self, file: IO[bytes], /) -> None: |
| 2190 | + """Import feeds from an OPML subscription list. |
| 2191 | +
|
| 2192 | + Existing and unsupported feeds are silently skipped. |
| 2193 | +
|
| 2194 | + Args: |
| 2195 | + file (file): A binary file. |
| 2196 | +
|
| 2197 | + Raises: |
| 2198 | + FeedImportError: If the file could not be parsed. |
| 2199 | + StorageError |
| 2200 | +
|
| 2201 | + .. versionadded:: 3.23 |
| 2202 | +
|
| 2203 | + """ |
| 2204 | + from . import opml |
| 2205 | + |
| 2206 | + for _ in self.import_feeds_iter(opml.parse(file)): |
| 2207 | + pass |
| 2208 | + |
| 2209 | + def import_feeds_iter( |
| 2210 | + self, feeds: Iterable[opml.Feed], / |
| 2211 | + ) -> Iterable[FeedImportResult]: |
| 2212 | + """Import feeds returned by :func:`reader.opml.parse`. |
| 2213 | +
|
| 2214 | + Args: |
| 2215 | + feeds (iterable(reader.opml.Feed)): The feeds to import. |
| 2216 | +
|
| 2217 | + Yields: |
| 2218 | + :class:`FeedImportResult`: The feed and whether it was added. |
| 2219 | +
|
| 2220 | + Raises: |
| 2221 | + StorageError |
| 2222 | +
|
| 2223 | + .. versionadded:: 3.23 |
| 2224 | +
|
| 2225 | + """ |
| 2226 | + for feed in feeds: |
| 2227 | + try: |
| 2228 | + self.add_feed(feed) |
| 2229 | + except FeedError as e: |
| 2230 | + yield FeedImportResult(feed, e) |
| 2231 | + else: |
| 2232 | + yield FeedImportResult(feed) |
| 2233 | + |
| 2234 | + def export_feeds(self, feeds: Iterable[Feed] | None = None, /) -> FeedExport: |
| 2235 | + """Export all or some feeds as an OPML subscription list. |
| 2236 | +
|
| 2237 | + Args: |
| 2238 | + feeds (iterable(Feed)): The feeds to export; if None, export all feeds. |
| 2239 | +
|
| 2240 | + Returns: |
| 2241 | + FeedExport: The OPML export. |
| 2242 | +
|
| 2243 | + Raises: |
| 2244 | + StorageError |
| 2245 | +
|
| 2246 | + .. versionadded:: 3.23 |
| 2247 | +
|
| 2248 | + """ |
| 2249 | + from . import opml |
| 2250 | + |
| 2251 | + if feeds is None: |
| 2252 | + feeds = self.get_feeds() |
| 2253 | + |
| 2254 | + feeds = (f for f in feeds if not f.url.startswith('reader:')) |
| 2255 | + |
| 2256 | + now = self._now() |
| 2257 | + title = 'reader feeds' |
| 2258 | + filename = f"{title.replace(' ', '-')}-{now:%Y-%m-%d-%H-%M-%S}.opml" |
| 2259 | + content = opml.unparse(feeds, title=title, created=now) |
| 2260 | + |
| 2261 | + return FeedExport(content, filename) |
| 2262 | + |
2184 | 2263 | def make_reader_reserved_name(self, key: str, /) -> str: |
2185 | 2264 | """Create a *reader*-reserved tag name. |
2186 | 2265 | See :ref:`reserved names` for details. |
|
0 commit comments