fylepy 0.1.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.
Files changed (44) hide show
  1. fyle/__init__.py +46 -0
  2. fyle/_core/__init__.py +5 -0
  3. fyle/_core/api.py +164 -0
  4. fyle/_core/chunking.py +107 -0
  5. fyle/_core/document.py +345 -0
  6. fyle/_core/fetcher.py +68 -0
  7. fyle/_core/registry.py +107 -0
  8. fyle/_core/sniffer.py +251 -0
  9. fyle/_readers/__init__.py +32 -0
  10. fyle/_readers/_md_structure.py +208 -0
  11. fyle/_readers/_whisper.py +126 -0
  12. fyle/_readers/archive/__init__.py +8 -0
  13. fyle/_readers/archive/stdlib.py +513 -0
  14. fyle/_readers/audio/__init__.py +9 -0
  15. fyle/_readers/audio/faster_whisper.py +162 -0
  16. fyle/_readers/base.py +70 -0
  17. fyle/_readers/csv/__init__.py +6 -0
  18. fyle/_readers/csv/stdlib.py +119 -0
  19. fyle/_readers/docx/__init__.py +6 -0
  20. fyle/_readers/docx/mammoth.py +130 -0
  21. fyle/_readers/html/__init__.py +6 -0
  22. fyle/_readers/html/markdownify.py +113 -0
  23. fyle/_readers/image/__init__.py +18 -0
  24. fyle/_readers/image/stdlib.py +136 -0
  25. fyle/_readers/markdown/__init__.py +6 -0
  26. fyle/_readers/markdown/stdlib.py +61 -0
  27. fyle/_readers/pdf/__init__.py +2 -0
  28. fyle/_readers/pdf/pymupdf4llm.py +202 -0
  29. fyle/_readers/pptx/__init__.py +7 -0
  30. fyle/_readers/pptx/python_pptx.py +306 -0
  31. fyle/_readers/sqlite/__init__.py +8 -0
  32. fyle/_readers/sqlite/stdlib.py +366 -0
  33. fyle/_readers/text/__init__.py +7 -0
  34. fyle/_readers/text/stdlib.py +76 -0
  35. fyle/_readers/video/__init__.py +10 -0
  36. fyle/_readers/video/scenedetect.py +330 -0
  37. fyle/_readers/xlsx/__init__.py +6 -0
  38. fyle/_readers/xlsx/openpyxl.py +158 -0
  39. fyle/errors.py +42 -0
  40. fyle/sqlite.py +175 -0
  41. fylepy-0.1.0.dist-info/METADATA +272 -0
  42. fylepy-0.1.0.dist-info/RECORD +44 -0
  43. fylepy-0.1.0.dist-info/WHEEL +4 -0
  44. fylepy-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,136 @@
1
+ """Image reader implementation backed by the Python standard library.
2
+
3
+ Strategy:
4
+ - Detect the concrete image subtype (png / jpeg / webp / ...) from magic
5
+ bytes or the file extension.
6
+ - Wrap the raw bytes as a ``data:<mime>;base64,<payload>`` URL.
7
+ - Expose the image both as an ``Image`` element and as a Markdown image
8
+ token in ``Page.text`` so the document can be fed directly into a
9
+ multimodal LLM prompt.
10
+ - Optionally record pixel dimensions in ``meta.warnings`` if Pillow is
11
+ available; failure to read dimensions never fails the parse.
12
+
13
+ Why no OCR here: fyle's contract is "open anything, return Markdown".
14
+ OCR / VLM calls are an application-level choice (network, cost, model
15
+ selection) and are deliberately kept out of the reader layer.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import base64
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ from ..base import Reader
25
+ from ..._core.document import Document, Image, Meta, Page
26
+ from ...errors import ParseError
27
+
28
+
29
+ # Magic-byte prefixes that pin down the concrete image subtype. Order
30
+ # matters: WEBP also starts with ``RIFF``, so it is checked after the
31
+ # RIFF-aware case below.
32
+ _MAGIC_MIME: list[tuple[bytes, str]] = [
33
+ (b"\x89PNG\r\n\x1a\n", "image/png"),
34
+ (b"\xff\xd8\xff", "image/jpeg"),
35
+ (b"GIF87a", "image/gif"),
36
+ (b"GIF89a", "image/gif"),
37
+ (b"BM", "image/bmp"),
38
+ # TIFF little / big endian
39
+ (b"II*\x00", "image/tiff"),
40
+ (b"MM\x00*", "image/tiff"),
41
+ ]
42
+
43
+ # Fallback extension -> MIME if magic-byte detection missed.
44
+ _EXT_MIME: dict[str, str] = {
45
+ ".png": "image/png",
46
+ ".jpg": "image/jpeg",
47
+ ".jpeg": "image/jpeg",
48
+ ".gif": "image/gif",
49
+ ".bmp": "image/bmp",
50
+ ".tif": "image/tiff",
51
+ ".tiff": "image/tiff",
52
+ ".webp": "image/webp",
53
+ }
54
+
55
+
56
+ def _detect_mime(data: bytes, source_name: Optional[str]) -> str:
57
+ """Return the best-guess ``image/*`` MIME for ``data``.
58
+
59
+ Falls back to ``application/octet-stream`` only if nothing matches;
60
+ callers receive a usable data URL either way.
61
+ """
62
+ # WEBP: ``RIFF....WEBP`` (4-byte size between RIFF and WEBP).
63
+ if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
64
+ return "image/webp"
65
+ for prefix, mime in _MAGIC_MIME:
66
+ if data.startswith(prefix):
67
+ return mime
68
+ if source_name:
69
+ ext = Path(source_name).suffix.lower()
70
+ if ext in _EXT_MIME:
71
+ return _EXT_MIME[ext]
72
+ return "application/octet-stream"
73
+
74
+
75
+ def _try_dimensions(data: bytes, warnings: list[str]) -> Optional[tuple[int, int]]:
76
+ """Best-effort (width, height) via Pillow. Never raises on failure."""
77
+ try:
78
+ from PIL import Image as _PILImage
79
+ except ImportError:
80
+ return None
81
+ try:
82
+ import io as _io
83
+ with _PILImage.open(_io.BytesIO(data)) as im:
84
+ return int(im.width), int(im.height)
85
+ except Exception as e:
86
+ warnings.append(f"image dimension read failed: {e}")
87
+ return None
88
+
89
+
90
+ class ImageReader(Reader):
91
+ name = "image-stdlib"
92
+ formats = ("image",)
93
+ is_default = True
94
+
95
+ def read(self, data: bytes, *, source_name: Optional[str] = None, **_) -> Document:
96
+ if not data:
97
+ raise ParseError("image-stdlib reader: input is empty")
98
+
99
+ warnings: list[str] = []
100
+
101
+ mime = _detect_mime(data, source_name)
102
+ b64 = base64.b64encode(data).decode("ascii")
103
+ data_url = f"data:{mime};base64,{b64}"
104
+
105
+ # Caption / title: filename stem if available, else the format name.
106
+ stem: Optional[str] = None
107
+ if source_name:
108
+ try:
109
+ stem = Path(source_name).stem or None
110
+ except (TypeError, ValueError):
111
+ stem = None
112
+ caption = stem or mime.split("/", 1)[-1]
113
+
114
+ dims = _try_dimensions(data, warnings)
115
+ if dims is not None:
116
+ warnings.append(f"image dimensions: {dims[0]}x{dims[1]}")
117
+
118
+ # Markdown image token. LLM prompts can consume this verbatim.
119
+ page_text = f"![{caption}]({data_url})"
120
+
121
+ image = Image(data_url=data_url, data=data, caption=caption, page=1)
122
+
123
+ try:
124
+ page = Page(text=page_text, number=1, images=[image])
125
+ meta = Meta(
126
+ format="image",
127
+ pages=1,
128
+ size=len(data),
129
+ title=stem,
130
+ reader=self.name,
131
+ created_at=datetime.now(timezone.utc),
132
+ warnings=warnings,
133
+ )
134
+ return Document(pages=[page], meta=meta)
135
+ except Exception as e: # pragma: no cover - defensive
136
+ raise ParseError(f"image reader failed to build Document: {e}") from e
@@ -0,0 +1,6 @@
1
+ """Markdown reader — passthrough of ``.md`` / ``.markdown`` files.
2
+
3
+ File name inside the subpackage is the *core driver library* (the Python
4
+ standard library — no external library is used for passthrough).
5
+ """
6
+ from . import stdlib # noqa: F401
@@ -0,0 +1,61 @@
1
+ """Markdown reader backed by the Python standard library.
2
+
3
+ For ``.md`` / ``.markdown`` inputs the content *is already* our target
4
+ representation, so ``Page.text`` is a **byte-preserving passthrough** of
5
+ the decoded source. On top of that passthrough we populate ``doc.tables``
6
+ and ``doc.images`` via the shared Markdown structure extractor
7
+ (``_md_structure``), which delegates to ``markdown-it-py`` + BeautifulSoup.
8
+
9
+ File naming rule: ``stdlib.py`` — the *core* dependency is the Python
10
+ standard library (decoding). ``markdown-it-py`` and BeautifulSoup are
11
+ ancillary parsers used for structural extraction and do not determine the
12
+ file name.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ from ..base import Reader
21
+ from ..._core.document import Document, Meta, Page
22
+ from ...errors import ParseError
23
+ from .._md_structure import extract_images, extract_tables
24
+ from ..text.stdlib import _decode_text
25
+
26
+
27
+ class MarkdownReader(Reader):
28
+ name = "markdown-stdlib"
29
+ formats = ("markdown",)
30
+ is_default = True
31
+
32
+ def read(self, data: bytes, *, source_name: Optional[str] = None, **_) -> Document:
33
+ warnings: list[str] = []
34
+ text, decode_warning = _decode_text(data)
35
+ if decode_warning:
36
+ warnings.append(decode_warning)
37
+
38
+ title: Optional[str] = None
39
+ if source_name:
40
+ try:
41
+ title = Path(source_name).stem or None
42
+ except (TypeError, ValueError):
43
+ title = None
44
+
45
+ tables = extract_tables(text, page=1, warnings=warnings)
46
+ images = extract_images(text, page=1, warnings=warnings, include_html_img=True)
47
+
48
+ try:
49
+ page = Page(text=text, number=1, tables=tables, images=images)
50
+ meta = Meta(
51
+ format="markdown",
52
+ pages=1,
53
+ size=len(data),
54
+ title=title,
55
+ reader=self.name,
56
+ created_at=datetime.now(timezone.utc),
57
+ warnings=warnings,
58
+ )
59
+ return Document(pages=[page], meta=meta)
60
+ except Exception as e: # pragma: no cover - defensive
61
+ raise ParseError(f"markdown-stdlib reader failed: {e}") from e
@@ -0,0 +1,2 @@
1
+ """PDF readers — default ``pymupdf4llm``; ``pdfplumber`` / ``pypdf`` as alternatives."""
2
+ from . import pymupdf4llm # noqa: F401
@@ -0,0 +1,202 @@
1
+ """PDF reader powered by ``pymupdf4llm`` — the default PDF reader.
2
+
3
+ Rationale: ``pymupdf4llm`` (maintained by the PyMuPDF team) already produces
4
+ LLM-friendly Markdown from PDFs and handles headings, lists, tables, and
5
+ embedded images. It is substantially less work than rebuilding those heuristics
6
+ on top of raw PyMuPDF.
7
+
8
+ Strategy:
9
+ - Open the PDF once with ``pymupdf`` to read document-level metadata.
10
+ - Call ``pymupdf4llm.to_markdown(doc, page_chunks=True, embed_images=True)`` to
11
+ get per-page Markdown with images inlined as base64 data URLs.
12
+ - Populate ``Page.text`` from the returned per-page Markdown. ``Page.tables``
13
+ and ``Page.images`` are back-filled via the shared ``_md_structure``
14
+ extractor (GFM table tokens + native / HTML image references) so the
15
+ surface stays consistent with the Markdown / DOCX / HTML readers.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import contextlib
20
+ import os
21
+ import sys
22
+ from datetime import datetime
23
+ from typing import Iterator, Optional
24
+
25
+ from ..._core.document import Document, Meta, Page
26
+ from ...errors import ParseError
27
+ from ..base import Reader
28
+ from .._md_structure import extract_images, extract_tables
29
+
30
+
31
+ class PyMuPdf4LlmReader(Reader):
32
+ name = "pymupdf4llm"
33
+ formats = ("pdf",)
34
+ is_default = True
35
+
36
+ def read(self, data: bytes, *, source_name: Optional[str] = None, **_) -> Document:
37
+ try:
38
+ import pymupdf as fitz # PyMuPDF >= 1.24
39
+ except ImportError:
40
+ try:
41
+ import fitz # type: ignore[no-redef]
42
+ except ImportError as e:
43
+ raise ParseError(
44
+ "PyMuPDF (pymupdf) is required: pip install pymupdf"
45
+ ) from e
46
+
47
+ try:
48
+ import pymupdf4llm # noqa: F401
49
+ except ImportError as e:
50
+ raise ParseError(
51
+ "pymupdf4llm is required for PDF parsing: pip install pymupdf4llm"
52
+ ) from e
53
+
54
+ warnings: list[str] = []
55
+
56
+ try:
57
+ pdf = fitz.open(stream=data, filetype="pdf")
58
+ except Exception as e:
59
+ raise ParseError(f"Failed to open PDF: {e}") from e
60
+
61
+ try:
62
+ try:
63
+ # pymupdf4llm + MuPDF emit a ``=== Document parser
64
+ # messages ===`` banner (e.g. "Using Tesseract for OCR
65
+ # processing.", "OCR on page.number=0/1.") during
66
+ # ``to_markdown``. Some lines come from Python
67
+ # ``print`` calls, but the per-page OCR progress and
68
+ # MuPDF warnings are emitted by the C extension
69
+ # directly via ``fprintf(stderr, ...)`` / ``stdout``,
70
+ # which ``contextlib.redirect_stdout`` cannot touch
71
+ # because it only rebinds ``sys.stdout`` at the Python
72
+ # level. We therefore redirect file descriptors 1 and 2
73
+ # at the OS level — the only way to silence a C
74
+ # extension's writes — for the duration of the call.
75
+ # ``show_progress=False`` still helps but does not
76
+ # cover the banner or the OCR lines.
77
+ with _silence_fds():
78
+ chunks = pymupdf4llm.to_markdown(
79
+ pdf,
80
+ page_chunks=True,
81
+ embed_images=True,
82
+ write_images=False,
83
+ show_progress=False,
84
+ )
85
+ except Exception as e:
86
+ raise ParseError(f"pymupdf4llm.to_markdown failed: {e}") from e
87
+
88
+ pages: list[Page] = []
89
+ for idx, chunk in enumerate(chunks, start=1):
90
+ text = (chunk.get("text") or "").strip()
91
+ tables = extract_tables(text, page=idx, warnings=warnings)
92
+ images = extract_images(
93
+ text, page=idx, warnings=warnings, include_html_img=True
94
+ )
95
+ pages.append(Page(text=text, number=idx, tables=tables, images=images))
96
+
97
+ if not pages:
98
+ # Degenerate case: pymupdf4llm returned nothing. Keep at least
99
+ # one empty page so ``doc.pages`` is never an empty list.
100
+ pages.append(Page(text="", number=1))
101
+ warnings.append("pymupdf4llm returned no page chunks")
102
+
103
+ md = pdf.metadata or {}
104
+ title = _clean(md.get("title"))
105
+ author = _clean(md.get("author"))
106
+ created_at = _parse_pdf_date(md.get("creationDate"))
107
+
108
+ if not title and source_name:
109
+ title = source_name
110
+
111
+ meta = Meta(
112
+ format="pdf",
113
+ pages=len(pages),
114
+ size=len(data),
115
+ title=title,
116
+ author=author,
117
+ created_at=created_at,
118
+ reader=self.name,
119
+ warnings=warnings,
120
+ )
121
+ finally:
122
+ try:
123
+ pdf.close()
124
+ except Exception:
125
+ pass
126
+
127
+ return Document(pages=pages, meta=meta)
128
+
129
+
130
+ def _clean(value) -> Optional[str]:
131
+ if not value:
132
+ return None
133
+ if isinstance(value, bytes):
134
+ try:
135
+ value = value.decode("utf-8", errors="ignore")
136
+ except Exception:
137
+ return None
138
+ s = str(value).strip()
139
+ return s or None
140
+
141
+
142
+ @contextlib.contextmanager
143
+ def _silence_fds() -> Iterator[None]:
144
+ """Redirect fds 1 and 2 to ``/dev/null`` at the OS level.
145
+
146
+ ``contextlib.redirect_stdout`` only rebinds ``sys.stdout`` at the
147
+ Python level, so any C extension that calls ``fprintf(stdout, ...)``
148
+ or ``fprintf(stderr, ...)`` directly (MuPDF warnings, the Tesseract
149
+ OCR progress banner, etc.) bypasses it. Duplicating ``/dev/null``
150
+ onto file descriptors 1 and 2 is the only way to silence those
151
+ writes without a third-party dependency. fds are restored on exit
152
+ even if the wrapped call raises.
153
+ """
154
+ # Flush Python-level buffers so nothing we care about ends up
155
+ # arriving at the silenced fds.
156
+ sys.stdout.flush()
157
+ sys.stderr.flush()
158
+ saved_stdout_fd = os.dup(1)
159
+ saved_stderr_fd = os.dup(2)
160
+ devnull_fd = os.open(os.devnull, os.O_WRONLY)
161
+ try:
162
+ os.dup2(devnull_fd, 1)
163
+ os.dup2(devnull_fd, 2)
164
+ try:
165
+ yield
166
+ finally:
167
+ sys.stdout.flush()
168
+ sys.stderr.flush()
169
+ os.dup2(saved_stdout_fd, 1)
170
+ os.dup2(saved_stderr_fd, 2)
171
+ finally:
172
+ os.close(devnull_fd)
173
+ os.close(saved_stdout_fd)
174
+ os.close(saved_stderr_fd)
175
+
176
+
177
+ def _parse_pdf_date(s) -> Optional[datetime]:
178
+ """Parse the PDF standard date format ``D:YYYYMMDDHHmmSS+OFFSET``.
179
+
180
+ Returns ``None`` on any parse failure.
181
+ """
182
+ if not s:
183
+ return None
184
+ if isinstance(s, bytes):
185
+ try:
186
+ s = s.decode("utf-8", errors="ignore")
187
+ except Exception:
188
+ return None
189
+ s = str(s).strip()
190
+ if s.startswith("D:"):
191
+ s = s[2:]
192
+ if len(s) >= 14:
193
+ try:
194
+ return datetime.strptime(s[:14], "%Y%m%d%H%M%S")
195
+ except ValueError:
196
+ pass
197
+ if len(s) >= 8:
198
+ try:
199
+ return datetime.strptime(s[:8], "%Y%m%d")
200
+ except ValueError:
201
+ pass
202
+ return None
@@ -0,0 +1,7 @@
1
+ """PPTX reader.
2
+
3
+ File naming rule: ``python_pptx.py`` — the core driver is ``python-pptx``.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from . import python_pptx # noqa: F401