surf-cli 0.7.0__py3-none-any.whl
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.
- surf/__init__.py +5 -0
- surf/__main__.py +7 -0
- surf/adapters.py +174 -0
- surf/logic.py +557 -0
- surf/models.py +148 -0
- surf/orchestrator.py +300 -0
- surf_cli-0.7.0.dist-info/METADATA +177 -0
- surf_cli-0.7.0.dist-info/RECORD +11 -0
- surf_cli-0.7.0.dist-info/WHEEL +4 -0
- surf_cli-0.7.0.dist-info/entry_points.txt +2 -0
- surf_cli-0.7.0.dist-info/licenses/LICENSE +21 -0
surf/__init__.py
ADDED
surf/__main__.py
ADDED
surf/adapters.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# adapters.py
|
|
2
|
+
"""Razor-thin filesystem I/O. Models only."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from pypdf import PdfReader
|
|
11
|
+
from pypdf.generic import Destination
|
|
12
|
+
|
|
13
|
+
from surf.models import (
|
|
14
|
+
ByteCount,
|
|
15
|
+
DocumentLines,
|
|
16
|
+
FileRef,
|
|
17
|
+
HeadingText,
|
|
18
|
+
OutlineLevel,
|
|
19
|
+
OutlineRecord,
|
|
20
|
+
PageCount,
|
|
21
|
+
PageIndex,
|
|
22
|
+
PageText,
|
|
23
|
+
PdfCatalog,
|
|
24
|
+
PdfDocument,
|
|
25
|
+
RenderedBody,
|
|
26
|
+
TexIncludeRelPath,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
type PypdfOutlineItem = Destination | list[PypdfOutlineItem]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PdfIngestError(Exception):
|
|
33
|
+
"""PDF bytes could not be read as a document outline."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_file(file_ref: FileRef) -> Path:
|
|
37
|
+
path = Path(str(file_ref)).expanduser()
|
|
38
|
+
if path.exists():
|
|
39
|
+
return path
|
|
40
|
+
if not path.suffix:
|
|
41
|
+
with_md = path.with_suffix(".md")
|
|
42
|
+
if with_md.exists():
|
|
43
|
+
return with_md
|
|
44
|
+
raise FileNotFoundError(f"File not found: {file_ref}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_document(path: Path) -> DocumentLines:
|
|
48
|
+
return tuple(path.read_text(encoding="utf-8-sig").splitlines())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def document_byte_count(path: Path) -> ByteCount:
|
|
52
|
+
return ByteCount(path.stat().st_size)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_tex_include(base_dir: Path, rel: TexIncludeRelPath) -> Path | None:
|
|
56
|
+
candidate = base_dir / str(rel)
|
|
57
|
+
if candidate.is_file():
|
|
58
|
+
return candidate
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def write_output(text: RenderedBody, dest: FileRef | None) -> None:
|
|
63
|
+
payload = text if text.endswith("\n") else text + "\n"
|
|
64
|
+
if dest is None:
|
|
65
|
+
print(text)
|
|
66
|
+
return
|
|
67
|
+
Path(str(dest)).write_text(payload, encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _optional_float(value: object) -> float | None:
|
|
71
|
+
match value:
|
|
72
|
+
case None:
|
|
73
|
+
return None
|
|
74
|
+
case int() | float() | str():
|
|
75
|
+
try:
|
|
76
|
+
return float(value)
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
case _:
|
|
80
|
+
try:
|
|
81
|
+
return float(str(value))
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _outline_record(reader: PdfReader, item: Destination, *, level: OutlineLevel) -> OutlineRecord:
|
|
87
|
+
title = item.title
|
|
88
|
+
page_number = reader.get_destination_page_number(item)
|
|
89
|
+
page_index = PageIndex(page_number) if page_number is not None else None
|
|
90
|
+
return OutlineRecord(
|
|
91
|
+
level=level,
|
|
92
|
+
title=HeadingText("" if title is None else str(title)),
|
|
93
|
+
page_index=page_index,
|
|
94
|
+
top=_optional_float(item.top),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _walk_outline(
|
|
99
|
+
reader: PdfReader,
|
|
100
|
+
items: Iterable[PypdfOutlineItem],
|
|
101
|
+
*,
|
|
102
|
+
level: OutlineLevel,
|
|
103
|
+
) -> Iterator[OutlineRecord]:
|
|
104
|
+
for item in items:
|
|
105
|
+
match item:
|
|
106
|
+
case list():
|
|
107
|
+
nested = cast(list[PypdfOutlineItem], item)
|
|
108
|
+
yield from _walk_outline(reader, nested, level=OutlineLevel(int(level) + 1))
|
|
109
|
+
case Destination():
|
|
110
|
+
yield _outline_record(reader, item, level=level)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _open_reader(path: Path) -> PdfReader:
|
|
114
|
+
reader = PdfReader(path)
|
|
115
|
+
if reader.is_encrypted:
|
|
116
|
+
raise PdfIngestError(f"could not read PDF: {path}")
|
|
117
|
+
return reader
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _outline_from_reader(reader: PdfReader) -> tuple[OutlineRecord, ...]:
|
|
121
|
+
outline_root = cast(Iterable[PypdfOutlineItem], reader.outline)
|
|
122
|
+
return tuple(_walk_outline(reader, outline_root, level=OutlineLevel(1)))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _pages_from_reader(
|
|
126
|
+
reader: PdfReader, start: PageIndex, end: PageIndex | None
|
|
127
|
+
) -> tuple[PageText, ...]:
|
|
128
|
+
pages = reader.pages
|
|
129
|
+
start_i = int(start)
|
|
130
|
+
end_i = len(pages) if end is None else int(end)
|
|
131
|
+
start_i = max(0, start_i)
|
|
132
|
+
end_i = min(len(pages), end_i)
|
|
133
|
+
if start_i >= end_i:
|
|
134
|
+
return ()
|
|
135
|
+
return tuple((pages[i].extract_text() or "") for i in range(start_i, end_i))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_pdf_catalog(path: Path) -> PdfCatalog:
|
|
139
|
+
try:
|
|
140
|
+
reader = _open_reader(path)
|
|
141
|
+
return PdfCatalog(
|
|
142
|
+
outline=_outline_from_reader(reader),
|
|
143
|
+
page_count=PageCount(len(reader.pages)),
|
|
144
|
+
byte_count=ByteCount(path.stat().st_size),
|
|
145
|
+
)
|
|
146
|
+
except PdfIngestError:
|
|
147
|
+
raise
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
raise PdfIngestError(f"could not read PDF: {path}") from exc
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_pdf_outline(path: Path) -> tuple[OutlineRecord, ...]:
|
|
153
|
+
return read_pdf_catalog(path).outline
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def read_pdf_pages(path: Path, start: PageIndex, end: PageIndex | None) -> tuple[PageText, ...]:
|
|
157
|
+
try:
|
|
158
|
+
return _pages_from_reader(_open_reader(path), start, end)
|
|
159
|
+
except PdfIngestError:
|
|
160
|
+
raise
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
raise PdfIngestError(f"could not read PDF: {path}") from exc
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_pdf(path: Path) -> PdfDocument:
|
|
166
|
+
try:
|
|
167
|
+
reader = _open_reader(path)
|
|
168
|
+
outline = _outline_from_reader(reader)
|
|
169
|
+
pages = _pages_from_reader(reader, PageIndex(0), None)
|
|
170
|
+
return PdfDocument(outline=outline, pages=pages)
|
|
171
|
+
except PdfIngestError:
|
|
172
|
+
raise
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
raise PdfIngestError(f"could not read PDF: {path}") from exc
|