pikuri-extractors 0.0.7 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be1bcb56e2c29751356aaca763f9dc6d4ea4c91605255522c3fcdd7a80d7483b
4
- data.tar.gz: e3f82c933661d434173898156294573a0a6ee1f9de56efe78b8b8e5af035ce60
3
+ metadata.gz: 987bc9fcc9828a020ffaabd52551f22c40d5862a807cb8a6b528dd807182ab7b
4
+ data.tar.gz: c2e1ac45003e57acf35efde390d200b12b447ce0249c43a8748eddf431bda4e3
5
5
  SHA512:
6
- metadata.gz: 2218b261c7400bd30ea8f113ec3b2cc4530786bd4c7384cfba68fd9453f065110fe17bf60eaf35df5ac9cfa27df9682e3346a10104850d1578fd7a2487ca709b
7
- data.tar.gz: 62b679db1d2d9f0410e1af73a2a05b0fb85d59761ac8ac08beea49dce526faeb2c5177917c9dd7d45fe2b1c71049b092639ed66d9dcce205992b18b4a84334b9
6
+ metadata.gz: 45e4ea2c9d7127f2d45621dab27e03d54d742aeaca310424c6cc21463993c08129dbc716dd9738d70daf09c6c0f55564762441472eeb9a8c1b30c95fcd4a9558
7
+ data.tar.gz: c64393a2f30e85803262021cc7756d38b3ffb773589d7a1f278eefe7454e77e8bf7a73f282e920e8f1e96b61144852fb8d244a7364007e64f5dcdbeda4e651dc
data/README.md CHANGED
@@ -50,8 +50,9 @@ wiring.** This gem parses PDFs inside the sandbox (poppler is native
50
50
  code chewing attacker-controlled bytes — exactly what the container
51
51
  is for) but re-converts the whole document on every paged read;
52
52
  pikuri-pdf is in-process pure Ruby with lazy page-windowed reads and
53
- no infrastructure to set up. The guide wires pikuri-pdf in chapter 3
54
- and supersedes it with this gem in chapter 7's assistant.
53
+ no infrastructure to set up. The guide wires pikuri-pdf in the
54
+ vector-DB chapter and supersedes it with this gem in the
55
+ universal-assistant chapter.
55
56
 
56
57
  ## Install
57
58
 
@@ -6,120 +6,35 @@ module Pikuri
6
6
  module Extractors
7
7
  # Document extractor for the {Pikuri::Extractor} registry:
8
8
  # DOCX / ODT / XLSX / legacy XLS / PPTX / EPUB / RTF / PDF →
9
- # Markdown, by piping the document bytes through pandoc (ODF,
10
- # RTF, EPUB, DOCX), markitdown (the OOXML spreadsheet /
11
- # presentation arms), or pdftotext (PDF), selected per format.
9
+ # Markdown, by piping the document bytes through pandoc (ODF, RTF,
10
+ # EPUB, DOCX), markitdown (OOXML spreadsheet / presentation), or
11
+ # pdftotext (PDF), selected per format.
12
12
  #
13
- # == Container first, host CLI second
13
+ # Prefers a one-shot, networkless docker container ({IMAGE}) — bytes
14
+ # in via stdin, Markdown out via stdout, no volume mounts — and falls
15
+ # back to host +pandoc+ / +markitdown+ / +pdftotext+ CLIs on the same
16
+ # stdin→stdout contract when docker is absent. Requiring the gem
17
+ # defines this class + the shared {DOCUMENTS} instance but registers
18
+ # nothing; a host opts in with +DOCUMENTS.register+.
14
19
  #
15
- # The preferred converter is a one-shot, locally-built docker
16
- # container ({IMAGE}, built from this gem's +docker/+ directory):
17
- # +docker run --rm -i --network=none --read-only --cap-drop=ALL+,
18
- # bytes in via stdin, Markdown out via stdout, **no volume
19
- # mounts**. Two reasons this beats running a host-installed
20
- # converter directly:
21
- #
22
- # * *Security.* These documents typically arrive via +fetch+ /
23
- # +web_scrape+ — untrusted bytes — and complex format parsers
24
- # are a classic exploitation surface. In the container the
25
- # parser sees no network and no host filesystem; the worst a
26
- # malicious document can do is produce garbage Markdown.
27
- # * *Reproducibility.* The Dockerfile pins pandoc (via the base
28
- # image's apt) and markitdown (exact pip version); a host
29
- # install is whatever version the machine happens to have.
30
- #
31
- # When docker is unavailable (binary absent or daemon down), the
32
- # extractor falls back to host-installed +pandoc+ / +markitdown+
33
- # / +pdftotext+ CLIs — same stdin→stdout contract, one code path
34
- # with two argv builders. Which arm was picked is logged once via
35
- # +Pikuri.logger_for('Extractors')+.
36
- #
37
- # == Registration is explicit
38
- #
39
- # Requiring pikuri-extractors defines this class and the shared
40
- # {DOCUMENTS} instance but registers nothing. A host script opts
41
- # in with +Pikuri::Extractors::DOCUMENTS.register+, which inserts
42
- # the instance before the registry's terminal +Passthrough+ entry
43
- # (and after core's +HTML+ — and pikuri-pdf's front-inserted
44
- # +PDF+, when the host registers that too — which keep winning
45
- # their formats). Same opt-in philosophy as +c.add_extension+ —
46
- # no behavior changes by require alone.
20
+ # The cross-cutting *why* the container's security + reproducibility
21
+ # rationale, the PDF-arm trade-off against pikuri-pdf, the
22
+ # deliberately-unsupported formats (ODS/ODP, OCR/audio), and why
23
+ # paging re-converts each time lives in
24
+ # +pikuri-extractors/DESIGN.md+.
47
25
  #
48
26
  # == Format detection
49
27
  #
50
- # {#matches?} claims content by normalized content-type
51
- # ({CONTENT_TYPES}) or by byte sniff: PDF's +%PDF-+ prefix, RTF's
52
- # +{\\rtf+ prefix; for
53
- # zip-based formats, ODF and EPUB mandate an uncompressed
54
- # +mimetype+ first entry (so the literal mime string sits inside
55
- # the leading sample), and OOXML is recognised by the
56
- # +[Content_Types].xml+ entry plus a +word/+ / +ppt/+ / +xl/+
57
- # entry-name scan. {#extract} re-sniffs (the registry duck type
58
- # doesn't pass +content_type+ to +extract+); when content was
59
- # claimed by content-type but the sniff is blind (legacy XLS — an
60
- # OLE2 container whose discriminating directory sits at the end of
61
- # the file, past the sample), the bytes go to markitdown with no
62
- # format hint and its own magic-byte detection takes over. The
63
- # consequence: a *local* +.xls+ (no transport content-type, sniff
64
- # blind) is not claimed at all and keeps today's binary refusal.
65
- # One ordering edge vs pikuri-pdf: this instance sits *after*
66
- # core's +HTML+ in the registry, so a PDF served under a lying
67
- # +text/html+ header goes to the HTML extractor (pikuri-pdf
68
- # front-inserts and wins that case). Accepted — lying-header PDFs
69
- # under specifically +text/html+ are rare.
70
- #
71
- # == PDF: this gem or pikuri-pdf — pick one per wiring
72
- #
73
- # The PDF arm (pdftotext, with {#pdf_page_lines} restoring the
74
- # +"--- Page N ---"+ markers from pdftotext's +\f+ separators)
75
- # makes this extractor a complete superset of pikuri-pdf's
76
- # formats, so a host that registers {DOCUMENTS} does NOT also
77
- # register +Extractors::PDF+ — one extractor per format keeps the
78
- # registry's first-match-wins semantics legible. The trade per
79
- # wiring:
80
- #
81
- # * *This gem* — PDF parsing happens inside the sandbox (poppler
82
- # is native code parsing attacker-controlled bytes; the
83
- # container is exactly the right place for it), one gem covers
84
- # every document format. Costs: docker (or host CLIs), no lazy
85
- # paging (each paged read re-converts the whole PDF), and the
86
- # generic +:document+ kind (the Read tools say "End of file",
87
- # not "End of PDF", and a scanned PDF reads as "(Empty file)"
88
- # rather than the scanned-image hint).
89
- # * *pikuri-pdf* — in-process pure Ruby (no infrastructure), lazy
90
- # +extract_lines+ paging (a windowed read of a 500-page PDF
91
- # parses only its window), PDF-specific Read-tool wording.
92
- # Costs: pdf-reader's dependency subtree, parsing untrusted
93
- # bytes in-process (pure Ruby, so DoS at worst).
94
- #
95
- # The guide walks this as a progression: chapter 3 wires
96
- # pikuri-pdf (no docker yet), chapter 7's assistant supersedes it
97
- # with this extractor.
98
- #
99
- # == Deliberately out of scope
100
- #
101
- # * *ODS / ODP* — neither pandoc nor markitdown reads them; the
102
- # only converter that does (LibreOffice headless) costs a 2 GB+
103
- # image. Excluded rather than half-supported.
104
- # * *Image OCR / audio transcription* — markitdown's optional
105
- # arms need model downloads; the converter image stays
106
- # networkless and small. A multi-modal main LLM is the pikuri
107
- # answer to images.
108
- #
109
- # == Paging economics
110
- #
111
- # A subprocess converter needs the whole document before it can
112
- # emit anything, so there is no lazy parse: every
113
- # +Extractor.extract_paged+ call (each +Read+ page of a long DOCX)
114
- # re-runs the full conversion. Accepted — no result cache in v1.
115
- # Both legs of one conversion still stream, though: the source +io+
116
- # is handed to {Pikuri::Subprocess.run} and copied straight into
117
- # the converter's stdin (+IO.copy_stream+ — a big local file never
118
- # loads into the Ruby heap), and the converter's stdout lands in a
119
- # Tempfile (also what makes the stdin/stdout pumping deadlock-free
120
- # — see {Pikuri::Subprocess.run}) whose lines {#extract_lines}
121
- # yields from disk — so neither the document nor the full Markdown
122
- # String is ever resident during paging.
28
+ # {#matches?} claims content by content-type ({CONTENT_TYPES}) or byte
29
+ # sniff (see {#sniff}). {#extract} re-sniffs (the registry duck type
30
+ # doesn't pass +content_type+ to +extract+); when the sniff is blind
31
+ # legacy XLS, an OLE2 container whose discriminating directory sits
32
+ # past the sample bytes go to markitdown with no hint and its own
33
+ # detection takes over. Consequence: a *local* +.xls+ (no transport
34
+ # content-type, sniff blind) isn't claimed at all and keeps today's
35
+ # binary refusal. Ordering edge: this instance sits *after* core's
36
+ # +HTML+, so a PDF served under a lying +text/html+ header goes to
37
+ # HTML (pikuri-pdf front-inserts and wins that) accepted, rare.
123
38
  class Documents
124
39
  # @return [Logger] gem-wide diagnostics logger.
125
40
  LOGGER = Pikuri.logger_for('Extractors')
@@ -145,14 +60,10 @@ module Pikuri
145
60
  # claimed by content-type but the byte sniff is blind.
146
61
  AUTO = 'auto'
147
62
 
148
- # @return [String] the PDF format tag. Singled out as a constant
149
- # because PDF is the one format whose converter output gets a
150
- # post-processing pass: pdftotext emits +\f+ between pages, and
151
- # {#extract} / {#extract_lines} turn those into the same
152
- # +"--- Page N ---"+ marker lines pikuri-pdf's extractor emits,
153
- # so page provenance (vectordb chunk citations, the Read tools'
154
- # page references) survives whichever PDF extractor a host
155
- # wires.
63
+ # @return [String] the PDF format tag. A constant because PDF is the
64
+ # one format whose output gets a post-processing pass ({#pdf_page_lines}
65
+ # restoring +"--- Page N ---"+ markers), so page provenance survives
66
+ # whichever PDF extractor a host wires.
156
67
  PDF = 'pdf'
157
68
 
158
69
  # @return [Hash{String => String}] normalized content-type →
@@ -231,17 +142,14 @@ module Pikuri
231
142
  end
232
143
  end
233
144
 
234
- # Same content as {#extract}, as a stream of +chomp+ed lines
235
- # read off the converter's stdout Tempfile the whole-document
236
- # conversion still runs up front (subprocess converters can't
237
- # parse lazily), but neither the document nor the Markdown ever
238
- # materialises as one String: the conversion fires on first
239
- # consumption, streaming +io+ into the converter. The enumerator
240
- # owns the Tempfile and deletes it when iteration ends.
145
+ # Same content as {#extract}, as a stream of +chomp+ed lines off the
146
+ # converter's stdout Tempfile. The full conversion still runs up
147
+ # front (fired on first consumption), but neither the document nor
148
+ # the Markdown ever materialises as one String. The enumerator owns
149
+ # the Tempfile and deletes it when iteration ends.
241
150
  #
242
- # @param io [IO, StringIO] seekable IO positioned at the start;
243
- # must remain open until the enumerator is consumed (same
244
- # contract as pikuri-pdf's lazy +extract_lines+).
151
+ # @param io [IO, StringIO] seekable IO positioned at the start; must
152
+ # remain open until the enumerator is consumed.
245
153
  # @return [Enumerator<String>]
246
154
  # @raise [Pikuri::Extractor::Error] as for {#extract}, raised on
247
155
  # first consumption.
@@ -257,9 +165,10 @@ module Pikuri
257
165
  end
258
166
  end
259
167
 
260
- # Plug this extractor into {Pikuri::Extractor.registry}, before
261
- # the terminal +Passthrough+ entry. Idempotent a second call
262
- # is a no-op.
168
+ # Plug this extractor into {Pikuri::Extractor.registry}, before the
169
+ # terminal +Passthrough+ entry but after core's +HTML+ and
170
+ # pikuri-pdf's front-inserted +PDF+ (both keep winning their
171
+ # formats). Idempotent — a second call is a no-op.
263
172
  #
264
173
  # @return [Documents] self, for chaining.
265
174
  def register
@@ -2,19 +2,10 @@
2
2
 
3
3
  require 'pikuri-core'
4
4
 
5
- # Entry file for the pikuri-extractors gem. Sets up a dedicated
6
- # Zeitwerk loader rooted at this gem's +lib/+, contributing to the
7
- # shared +Pikuri::+ namespace alongside pikuri-core. After +require
8
- # 'pikuri-extractors'+, +Pikuri::Extractors::Documents+ and the shared
9
- # +Pikuri::Extractors::DOCUMENTS+ instance are defined — but *nothing
10
- # is registered*: extractors plug into +Pikuri::Extractor.registry+
11
- # only when the host script calls their +#register+ explicitly, so a
12
- # +bin/pikuri-*+ picks which extractors it wires in (same opt-in
13
- # philosophy as +c.add_extension+).
14
- #
15
- # The loader is per-gem (not shared with pikuri-core's loader) so each
16
- # gem owns its own +lib/+ tree and the cooperation between gems is via
17
- # the Pikuri namespace alone.
5
+ # Entry file for the pikuri-extractors gem: sets up the per-gem Zeitwerk
6
+ # loader. Requiring it defines {Pikuri::Extractors::Documents} + the
7
+ # shared {Pikuri::Extractors::DOCUMENTS} but registers nothing a host
8
+ # opts in with +DOCUMENTS.register+ (see there).
18
9
  module Pikuri
19
10
  module Extractors
20
11
  LOADER = Zeitwerk::Loader.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pikuri-extractors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Vysny
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.0.7
18
+ version: 0.1.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.0.7
25
+ version: 0.1.0
26
26
  description: |
27
27
  pikuri-extractors plugs additional document formats into
28
28
  pikuri-core's +Pikuri::Extractor+ registry. The bundled