Coverage for src/epublib/source.py: 88%
50 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
1from datetime import datetime
2from pathlib import Path
3from typing import IO, Literal, Protocol, override
4from zipfile import ZipInfo
6from epublib.exceptions import EPUBError
9def zip_info_now():
10 now = datetime.now()
12 if now.year > 2107:
13 now = now.replace(year=2107, month=12, day=31)
15 return (
16 now.year,
17 now.month,
18 now.day,
19 now.hour,
20 now.minute,
21 now.second,
22 )
25class SourceProtocol(Protocol):
26 """Protocol for a source of EPUB data."""
28 def getinfo(self, name: str) -> ZipInfo: ...
29 def open(
30 self,
31 name: str | ZipInfo,
32 mode: Literal["r", "w"] = "r",
33 pwd: bytes | None = None,
34 *,
35 force_zip64: bool = False,
36 ) -> IO[bytes]: ...
37 def infolist(self) -> list[ZipInfo]: ...
40class SinkProtocol(Protocol):
41 """Protocol for a sink of EPUB data."""
43 def writestr(
44 self,
45 zinfo_or_arcname: str | ZipInfo,
46 data: bytes | str,
47 compress_type: int | None = None,
48 compresslevel: int | None = None,
49 ) -> None: ...
52class DirectorySource(SourceProtocol):
53 """An EPUB source that reads the book from a directory on the filesystem (an 'unzipped' EPUB)."""
55 def __init__(self, path: str | Path):
56 self.path: Path = Path(path)
57 if not self.path.is_dir():
58 raise EPUBError(f"'{path}' is not a directory")
60 @override
61 def infolist(self) -> list[ZipInfo]:
62 return [
63 self._to_zipinfo(str(file.relative_to(self.path)))
64 for file in self.path.rglob("*")
65 if not file.is_dir()
66 ]
68 @override
69 def getinfo(self, name: str) -> ZipInfo:
70 target = self.path / name
71 if target.is_file():
72 return self._to_zipinfo(name)
73 raise KeyError("There is no item named '{name}' in the folder")
75 def _to_zipinfo(self, name: str):
76 return ZipInfo.from_file(
77 self.path / name,
78 arcname=name,
79 strict_timestamps=False,
80 )
82 @override
83 def open(
84 self,
85 name: str | ZipInfo,
86 mode: Literal["r", "w"] = "r",
87 pwd: bytes | None = None,
88 *,
89 force_zip64: bool = False,
90 ) -> IO[bytes]:
91 if isinstance(name, ZipInfo):
92 filename = self.path / name.filename
93 else:
94 filename = self.path / name
96 return open(filename, mode + "b")
99class DirectorySink(SinkProtocol):
100 """An EPUB sink that writes the book to a directory on the filesystem (as an 'unzipped' EPUB)."""
102 def __init__(self, path: str | Path) -> None:
103 self.path: Path = Path(path)
104 if not self.path.is_dir():
105 raise EPUBError(f"'{path}' is not a directory")
107 @override
108 def writestr(
109 self,
110 zinfo_or_arcname: str | ZipInfo,
111 data: bytes | str,
112 compress_type: int | None = None,
113 compresslevel: int | None = None,
114 ) -> None:
115 if isinstance(zinfo_or_arcname, ZipInfo):
116 filename = zinfo_or_arcname.filename
117 else:
118 filename = zinfo_or_arcname
120 full_path = self.path / filename
122 if str(full_path.parent) != ".":
123 full_path.parent.mkdir(exist_ok=True, parents=True)
125 mode = "w" if isinstance(data, str) else "wb"
126 with open(full_path, mode) as f:
127 __ = f.write(data)