PyStreamPDF 2.1.2__tar.gz → 2.3.0__tar.gz

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 (83) hide show
  1. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/Cargo.lock +2 -2
  2. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/Cargo.toml +1 -1
  3. pystreampdf-2.3.0/PKG-INFO +271 -0
  4. pystreampdf-2.3.0/README.md +235 -0
  5. pystreampdf-2.3.0/core/src/pdf_parser.rs +183 -0
  6. pystreampdf-2.3.0/core/src/security.rs +205 -0
  7. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/pyproject.toml +13 -1
  8. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/__init__.py +3 -2
  9. pystreampdf-2.3.0/python/pystreampdf/_mcp_connector.py +179 -0
  10. pystreampdf-2.3.0/python/pystreampdf/_mcp_tools.py +537 -0
  11. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cache.py +82 -9
  12. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli_daemon.py +14 -6
  13. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/extraction.py +7 -3
  14. pystreampdf-2.3.0/python/pystreampdf/okf_entity_profiles.py +82 -0
  15. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/metadata.py +3 -1
  16. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/selector.py +7 -2
  17. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/assembler.py +14 -8
  18. pystreampdf-2.3.0/python/pystreampdf/tokenizer.py +68 -0
  19. pystreampdf-2.1.2/PKG-INFO +0 -243
  20. pystreampdf-2.1.2/README.md +0 -216
  21. pystreampdf-2.1.2/core/src/pdf_parser.rs +0 -179
  22. pystreampdf-2.1.2/core/src/security.rs +0 -126
  23. pystreampdf-2.1.2/python/pystreampdf/intelligence/__init__.py.bak +0 -28
  24. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/LICENSE +0 -0
  25. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/Cargo.toml +0 -0
  26. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/audit.rs +0 -0
  27. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/context.rs +0 -0
  28. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/document.rs +0 -0
  29. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/error.rs +0 -0
  30. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/forms.rs +0 -0
  31. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/heading_extractor.rs +0 -0
  32. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/index.rs +0 -0
  33. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/lib.rs +0 -0
  34. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/markdown.rs +0 -0
  35. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/navigator.rs +0 -0
  36. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/page.rs +0 -0
  37. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/structure.rs +0 -0
  38. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/Cargo.toml +0 -0
  39. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli.py +0 -0
  40. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli_dashboard.py +0 -0
  41. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/excel_export.py +0 -0
  42. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/__init__.py +0 -0
  43. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/code_intelligence.py +0 -0
  44. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/json_intelligence.py +0 -0
  45. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/log_intelligence.py +0 -0
  46. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/sql_intelligence.py +0 -0
  47. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/types.py +0 -0
  48. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/yaml_intelligence.py +0 -0
  49. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/__init__.py +0 -0
  50. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/manager.py +0 -0
  51. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/pipeline.py +0 -0
  52. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/provider.py +0 -0
  53. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/__init__.py +0 -0
  54. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/paddle.py +0 -0
  55. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/tesseract.py +0 -0
  56. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/__init__.py +0 -0
  57. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/chunking.py +0 -0
  58. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/compressor.py +0 -0
  59. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/pipeline.py +0 -0
  60. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/scripts/__init__.py +0 -0
  61. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/scripts/post_install.py +0 -0
  62. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/__init__.py +0 -0
  63. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/citations.py +0 -0
  64. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/entities.py +0 -0
  65. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/graph.py +0 -0
  66. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/relationships.py +0 -0
  67. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/topics.py +0 -0
  68. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/verifier.py +0 -0
  69. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/__init__.py +0 -0
  70. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/builder.py +0 -0
  71. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/exporters.py +0 -0
  72. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/recovery.py +0 -0
  73. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/relationships.py +0 -0
  74. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/token_budget.py +0 -0
  75. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/__init__.py +0 -0
  76. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/layout.py +0 -0
  77. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/scorer.py +0 -0
  78. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/table.py +0 -0
  79. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/text.py +0 -0
  80. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/types.py +0 -0
  81. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/src/lib.rs +0 -0
  82. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/streampdf/cli.py +0 -0
  83. {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/streampdf/server.py +0 -0
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "pystreampdf"
578
- version = "2.1.2"
578
+ version = "2.3.0"
579
579
  dependencies = [
580
580
  "chrono",
581
581
  "pyo3",
@@ -721,7 +721,7 @@ dependencies = [
721
721
 
722
722
  [[package]]
723
723
  name = "streampdf-core"
724
- version = "2.1.2"
724
+ version = "2.3.0"
725
725
  dependencies = [
726
726
  "chrono",
727
727
  "pdfium-render",
@@ -3,7 +3,7 @@ members = ["core", "python"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "2.1.2"
6
+ version = "2.3.0"
7
7
  edition = "2021"
8
8
  authors = ["Georgi Mammen Mullassery <mullassery@gmail.com>"]
9
9
  license = "Proprietary"
@@ -0,0 +1,271 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyStreamPDF
3
+ Version: 2.3.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: License :: Other/Proprietary License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Rust
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Text Processing
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
20
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
21
+ Requires-Dist: pytest-timeout>=2.1 ; extra == 'dev'
22
+ Requires-Dist: reportlab>=4.0 ; extra == 'dev'
23
+ Requires-Dist: tiktoken>=0.5 ; extra == 'dev'
24
+ Requires-Dist: tiktoken>=0.5 ; extra == 'tiktoken'
25
+ Provides-Extra: dev
26
+ Provides-Extra: tiktoken
27
+ License-File: LICENSE
28
+ Summary: Intelligence engine for PDFs: selective retrieval, structure analysis, and token-efficient RAG
29
+ Keywords: pdf,retrieval,rag,ai,intelligence,search,indexing
30
+ Author-email: Georgi Mammen Mullassery <mullassery@gmail.com>
31
+ Requires-Python: >=3.9
32
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
33
+ Project-URL: Issues, https://github.com/Mullassery/StreamPDF/issues
34
+ Project-URL: Repository, https://github.com/Mullassery/StreamPDF
35
+
36
+ # PyStreamPDF
37
+
38
+ **Extract only what matters from PDFs, so you send less to LLMs.**
39
+
40
+ Stop sending entire documents to LLMs. PyStreamPDF parses PDF structure and
41
+ splits content into semantic, token-budget-aware chunks so you can send
42
+ only the relevant parts of a document to a model. See "Token Savings"
43
+ below for how much that saves in practice — it depends on your documents.
44
+
45
+ [![PyPI](https://img.shields.io/pypi/v/pystreampdf)](https://pypi.org/project/pystreampdf)
46
+ [![Python 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue)](https://www.python.org)
47
+ [![Tests: 557 Passing](https://img.shields.io/badge/tests-557%20passing-success)](./tests)
48
+ [![License: Proprietary](https://img.shields.io/badge/License-Proprietary-blue.svg)](./LICENSE)
49
+
50
+ ---
51
+
52
+ ## 30-Second Start
53
+
54
+ ```python
55
+ from pystreampdf import SemanticChunker, ElementType
56
+
57
+ with open("financial_report.pdf", "rb") as f:
58
+ text = f.read().decode("latin-1", errors="ignore") # or use a real PDF text extractor first
59
+
60
+ chunker = SemanticChunker(target_tokens=500)
61
+ chunks = chunker.chunk_content(
62
+ text, element_type=ElementType.TEXT, page_start=1, page_end=1
63
+ )
64
+ print(f"Split into {len(chunks)} semantic chunks")
65
+ ```
66
+
67
+ There is no `Document`/`.extract_relevant()` class in this package —
68
+ `SemanticChunker` (shown above) and `PDFCache` (see "Quick Start: Document
69
+ Extraction" below) are the real, exported API. If you built against a
70
+ `Document` class from an earlier version of these docs, it never actually
71
+ existed in source; see Known Issues.
72
+
73
+ ---
74
+
75
+ ## Why PyStreamPDF?
76
+
77
+ **The Problem:**
78
+ - You send entire PDFs to LLMs (wasteful, expensive)
79
+ - RAG systems retrieve too much content
80
+ - Token costs skyrocket on large documents
81
+ - No way to know which parts actually matter
82
+
83
+ **The Solution:**
84
+ - Semantic chunking with context awareness (real, in `SemanticChunker`)
85
+ - Token-budget-aware caching (real, in `PDFCache` / `TokenBudgetConfig`)
86
+ - Reduces the volume of text you send per query — see "Token Savings"
87
+ below for what's measured vs. illustrative
88
+
89
+ ---
90
+
91
+ ## Key Features
92
+
93
+ - **Real encryption/permission detection:** Backed by PDFium's actual document security APIs — not a stub. Encrypted, password-protected, and permission-restricted PDFs are detected and reported accurately; opening with a wrong/missing password fails closed rather than silently succeeding.
94
+ - **Honest failure signaling:** If a PDF can't be parsed (corrupt, truncated, unsupported), you get a real error — never fabricated placeholder content.
95
+ - **Intelligent Extraction:** Find relevant sections automatically
96
+ - **Semantic Chunking:** Context-aware splitting, not just word count
97
+ - **Multi-Format:** Text, tables, images, charts, OCR
98
+ - **Token Budgeting:** Allocate tokens by document type
99
+ - **Smart Caching:** L1 memory + L2 disk (HMAC-signed on disk — no unverified deserialization)
100
+ - **Metadata Preservation:** Keep tables, images, structure
101
+ - **Production-Ready:** 557 passing tests (2 skipped when optional OCR system deps aren't installed)
102
+
103
+ ---
104
+
105
+ ## Real-World Use Cases
106
+
107
+ See [examples/basic_parse.py](examples/basic_parse.py) for the Rust-backed
108
+ `pystreampdf.open()` path (document/page/structure inspection — requires
109
+ the compiled `_core` extension, see Known Issues) and
110
+ [examples/token_budget_and_cache_example.py](examples/token_budget_and_cache_example.py)
111
+ for the pure-Python `SemanticChunker` + `PDFCache` + `TokenBudgetConfig`
112
+ path shown above. Both are real, runnable scripts — copying them is more
113
+ reliable than a README snippet for an evolving API.
114
+
115
+ ---
116
+
117
+ ## Token Savings
118
+
119
+ **A note on how token counts are computed:** by default, token counts use a
120
+ `len(text) / 4` heuristic (a common rule of thumb, but approximate — it can
121
+ be off by a meaningful margin, especially for code or non-English text).
122
+ Install the optional `tiktoken` extra for exact BPE token counts:
123
+
124
+ ```bash
125
+ pip install "pystreampdf[tiktoken]"
126
+ ```
127
+
128
+ When `tiktoken` is installed, `pystreampdf.tokenizer.is_exact()` returns
129
+ `True` and counts use the real `cl100k_base` encoding; otherwise it falls
130
+ back to the heuristic. Check `pystreampdf.tokenizer.TOKENIZER_MODE` to see
131
+ which mode produced a given number.
132
+
133
+ Actual token savings depend entirely on your documents and how narrowly
134
+ you scope `extract_relevant`-style queries — there's no committed
135
+ benchmark result in this repo backing a specific percentage (the earlier
136
+ "70%"/"10-50x" style claims in this README and in `__init__.py`'s
137
+ docstring weren't measured against anything checked in). Run
138
+ [examples/token_budget_and_cache_example.py](examples/token_budget_and_cache_example.py)
139
+ against your own documents to get a real number for your use case.
140
+
141
+ ---
142
+
143
+ ## Security Notes
144
+
145
+ - **Encryption & permissions**: `pystreampdf._core.PyPdfDocument.is_encrypted()`,
146
+ `.permissions()`, and `.open_with_password()` are backed by PDFium's real
147
+ document security APIs. A wrong or missing password on an encrypted PDF
148
+ raises an error — it never falls back to an unauthenticated parse.
149
+ - **Cache integrity**: the on-disk (L2) cache is HMAC-SHA256 signed using a
150
+ key generated on first use and stored with owner-only (0600) permissions.
151
+ A tampered or foreign cache file fails signature verification and is
152
+ discarded before any deserialization occurs.
153
+ - **MCP/DAB connector**: binds to `127.0.0.1` with no cross-origin access
154
+ and read-only permissions by default. Wider exposure requires an explicit
155
+ `allow_remote=True` opt-in — review the security implications first, since
156
+ the connector has no authentication of its own.
157
+
158
+ ---
159
+
160
+ ## Installation
161
+
162
+ ```bash
163
+ pip install pystreampdf
164
+
165
+ # With exact token counting (tiktoken)
166
+ pip install "pystreampdf[tiktoken]"
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Documentation
172
+
173
+ - [Quick Start](docs/QUICKSTART.md) — Process your first PDF
174
+ - [Extraction Strategies](docs/EXTRACTION.md) — Different approaches for different documents
175
+ - [Token Budgeting](docs/TOKEN_BUDGETS.md) — Control context allocation
176
+ - [Token Budget Multipliers](docs/TOKEN_BUDGET_MULTIPLIERS.md) — Comprehensive token budget guide with examples
177
+ - [Examples](examples/) — Real-world RAG optimization
178
+
179
+ ## Quick Start: Document Extraction
180
+
181
+ ```python
182
+ from pystreampdf import SemanticChunker, PDFCache, TokenBudgetConfig, BudgetRule
183
+
184
+ # Setup budget config
185
+ budget_config = TokenBudgetConfig(
186
+ base_budget=800,
187
+ rules=[BudgetRule("complex", 1.1, match_fields=["filename"])]
188
+ )
189
+
190
+ # Initialize cache with budget management
191
+ cache = PDFCache(
192
+ memory_limit_mb=500,
193
+ disk_cache_dir="./pdf_cache",
194
+ token_budget_config=budget_config
195
+ )
196
+
197
+ # Process PDF with intelligent chunking
198
+ def extract_pdf(pdf_path):
199
+ chunks, preview, title, pages = cache.get_or_process(
200
+ pdf_path,
201
+ process_fn=extract_chunks
202
+ )
203
+ return chunks
204
+
205
+ # Use semantic chunker directly
206
+ chunker = SemanticChunker(target_tokens=500)
207
+ chunks = chunker.chunk_content(text, element_type=ElementType.TEXT, page_start=1, page_end=1)
208
+ ```
209
+
210
+ ## Budget Configuration
211
+
212
+ ### Default Settings
213
+ - **Minimum budget**: 500 tokens (fixed)
214
+ - **Maximum budget**: 1000 tokens (fixed)
215
+ - **Default base**: 1000 tokens
216
+ - **Multiplier range**: 0.5 - 1.5 (common)
217
+
218
+ ### Dynamic Adjustment via Multipliers
219
+ Use keyword-based rules to scale within the 500-1000 range without changing hard limits. See examples for [financial](docs/TOKEN_BUDGET_MULTIPLIERS.md#high-complexity-documents), [legal](docs/TOKEN_BUDGET_MULTIPLIERS.md#high-complexity-documents), and [summary](docs/TOKEN_BUDGET_MULTIPLIERS.md#low-complexity-documents) documents.
220
+
221
+ ## MCP Integration (optional, local by default)
222
+
223
+ PyStreamPDF ships an optional MCP/DAB connector (`pystreampdf._mcp_connector`)
224
+ exposing document-processing tools (extract text/tables/images, OCR,
225
+ structure detection, metadata, chunking, citations, validation). It's
226
+ opt-in, binds to `127.0.0.1` only, and requires `allow_remote=True` to be
227
+ exposed beyond localhost. See [examples/mcp_pystreampdf.py](examples/mcp_pystreampdf.py).
228
+
229
+ ## Known Issues
230
+
231
+ - Earlier versions of this README documented a `Document` class with
232
+ `.extract_relevant()` and a `.token_savings` attribute — verified
233
+ against source: this class has never existed in this package. Fixed
234
+ in this pass to describe the real, exported API (`SemanticChunker`,
235
+ `PDFCache`, `TokenBudgetConfig`, and the Rust-backed `pystreampdf.open()`).
236
+ - The published PyPI wheel is the only wheel on PyPI — macOS arm64 only,
237
+ no sdist. On any other platform or Python version, `pip install` will
238
+ fail without a local Rust toolchain to build from source.
239
+ - ~~The published version (2.2.1) is ahead of what's tagged in this repo's
240
+ Cargo.toml/__init__.py (2.2.0)~~ Resolved: 2.3.0 (this release) is
241
+ unambiguously ahead of both 2.2.0 and 2.2.1.
242
+ - The Rust-backed `pystreampdf.open()` / `pystreampdf.load_index()` API
243
+ (used in `examples/basic_parse.py`) silently becomes `None` if the
244
+ compiled `_core` extension isn't available (e.g. a from-source install
245
+ without `maturin develop`) — falling back to `SemanticChunker`/`PDFCache`
246
+ in that case, per the code above, not a hard error.
247
+ - The "Tests & Build" GitHub Actions workflow was red for several weeks
248
+ due to two CI infrastructure bugs (not the test suite itself), both
249
+ now fixed and confirmed green in CI:
250
+ [run 32611116541](https://github.com/Mullassery/PyStreamPDF/actions/runs/32611116541).
251
+ First, `dtolnay/rust-toolchain@v1` required an explicit `toolchain`
252
+ input the workflow didn't provide, failing both jobs before any tests
253
+ ran — fixed by pinning to `dtolnay/rust-toolchain@stable`. Second, once
254
+ that was fixed, `maturin develop` failed in the Python-test job because
255
+ `actions/setup-python` doesn't provide an active virtualenv, which
256
+ maturin requires — fixed by creating and activating a `.venv` before
257
+ the build step. Actual CI output as of 2.2.0: `pytest` — 536 passed, 2
258
+ skipped (Python 3.10, 3.11, and 3.12, each identical); `cargo test` — 23
259
+ passed, 0 failed. (The Rust suite has 23 tests, not 536 — an earlier
260
+ version of this section conflated the pytest count with the Rust one.)
261
+ As of 2.3.0: `pytest` — 557 passed, 2 skipped (21 new tests added for
262
+ `tests/test_mcp_tools.py`).
263
+
264
+ ## License
265
+
266
+ Proprietary License — Free to use with explicit attribution. See [LICENSE](LICENSE).
267
+
268
+ ---
269
+
270
+ **PyStreamPDF v2.3.0** | Intelligent PDF processing for AI | Python 3.9+ | 557 passing tests
271
+
@@ -0,0 +1,235 @@
1
+ # PyStreamPDF
2
+
3
+ **Extract only what matters from PDFs, so you send less to LLMs.**
4
+
5
+ Stop sending entire documents to LLMs. PyStreamPDF parses PDF structure and
6
+ splits content into semantic, token-budget-aware chunks so you can send
7
+ only the relevant parts of a document to a model. See "Token Savings"
8
+ below for how much that saves in practice — it depends on your documents.
9
+
10
+ [![PyPI](https://img.shields.io/pypi/v/pystreampdf)](https://pypi.org/project/pystreampdf)
11
+ [![Python 3.9+](https://img.shields.io/badge/Python-3.9%2B-blue)](https://www.python.org)
12
+ [![Tests: 557 Passing](https://img.shields.io/badge/tests-557%20passing-success)](./tests)
13
+ [![License: Proprietary](https://img.shields.io/badge/License-Proprietary-blue.svg)](./LICENSE)
14
+
15
+ ---
16
+
17
+ ## 30-Second Start
18
+
19
+ ```python
20
+ from pystreampdf import SemanticChunker, ElementType
21
+
22
+ with open("financial_report.pdf", "rb") as f:
23
+ text = f.read().decode("latin-1", errors="ignore") # or use a real PDF text extractor first
24
+
25
+ chunker = SemanticChunker(target_tokens=500)
26
+ chunks = chunker.chunk_content(
27
+ text, element_type=ElementType.TEXT, page_start=1, page_end=1
28
+ )
29
+ print(f"Split into {len(chunks)} semantic chunks")
30
+ ```
31
+
32
+ There is no `Document`/`.extract_relevant()` class in this package —
33
+ `SemanticChunker` (shown above) and `PDFCache` (see "Quick Start: Document
34
+ Extraction" below) are the real, exported API. If you built against a
35
+ `Document` class from an earlier version of these docs, it never actually
36
+ existed in source; see Known Issues.
37
+
38
+ ---
39
+
40
+ ## Why PyStreamPDF?
41
+
42
+ **The Problem:**
43
+ - You send entire PDFs to LLMs (wasteful, expensive)
44
+ - RAG systems retrieve too much content
45
+ - Token costs skyrocket on large documents
46
+ - No way to know which parts actually matter
47
+
48
+ **The Solution:**
49
+ - Semantic chunking with context awareness (real, in `SemanticChunker`)
50
+ - Token-budget-aware caching (real, in `PDFCache` / `TokenBudgetConfig`)
51
+ - Reduces the volume of text you send per query — see "Token Savings"
52
+ below for what's measured vs. illustrative
53
+
54
+ ---
55
+
56
+ ## Key Features
57
+
58
+ - **Real encryption/permission detection:** Backed by PDFium's actual document security APIs — not a stub. Encrypted, password-protected, and permission-restricted PDFs are detected and reported accurately; opening with a wrong/missing password fails closed rather than silently succeeding.
59
+ - **Honest failure signaling:** If a PDF can't be parsed (corrupt, truncated, unsupported), you get a real error — never fabricated placeholder content.
60
+ - **Intelligent Extraction:** Find relevant sections automatically
61
+ - **Semantic Chunking:** Context-aware splitting, not just word count
62
+ - **Multi-Format:** Text, tables, images, charts, OCR
63
+ - **Token Budgeting:** Allocate tokens by document type
64
+ - **Smart Caching:** L1 memory + L2 disk (HMAC-signed on disk — no unverified deserialization)
65
+ - **Metadata Preservation:** Keep tables, images, structure
66
+ - **Production-Ready:** 557 passing tests (2 skipped when optional OCR system deps aren't installed)
67
+
68
+ ---
69
+
70
+ ## Real-World Use Cases
71
+
72
+ See [examples/basic_parse.py](examples/basic_parse.py) for the Rust-backed
73
+ `pystreampdf.open()` path (document/page/structure inspection — requires
74
+ the compiled `_core` extension, see Known Issues) and
75
+ [examples/token_budget_and_cache_example.py](examples/token_budget_and_cache_example.py)
76
+ for the pure-Python `SemanticChunker` + `PDFCache` + `TokenBudgetConfig`
77
+ path shown above. Both are real, runnable scripts — copying them is more
78
+ reliable than a README snippet for an evolving API.
79
+
80
+ ---
81
+
82
+ ## Token Savings
83
+
84
+ **A note on how token counts are computed:** by default, token counts use a
85
+ `len(text) / 4` heuristic (a common rule of thumb, but approximate — it can
86
+ be off by a meaningful margin, especially for code or non-English text).
87
+ Install the optional `tiktoken` extra for exact BPE token counts:
88
+
89
+ ```bash
90
+ pip install "pystreampdf[tiktoken]"
91
+ ```
92
+
93
+ When `tiktoken` is installed, `pystreampdf.tokenizer.is_exact()` returns
94
+ `True` and counts use the real `cl100k_base` encoding; otherwise it falls
95
+ back to the heuristic. Check `pystreampdf.tokenizer.TOKENIZER_MODE` to see
96
+ which mode produced a given number.
97
+
98
+ Actual token savings depend entirely on your documents and how narrowly
99
+ you scope `extract_relevant`-style queries — there's no committed
100
+ benchmark result in this repo backing a specific percentage (the earlier
101
+ "70%"/"10-50x" style claims in this README and in `__init__.py`'s
102
+ docstring weren't measured against anything checked in). Run
103
+ [examples/token_budget_and_cache_example.py](examples/token_budget_and_cache_example.py)
104
+ against your own documents to get a real number for your use case.
105
+
106
+ ---
107
+
108
+ ## Security Notes
109
+
110
+ - **Encryption & permissions**: `pystreampdf._core.PyPdfDocument.is_encrypted()`,
111
+ `.permissions()`, and `.open_with_password()` are backed by PDFium's real
112
+ document security APIs. A wrong or missing password on an encrypted PDF
113
+ raises an error — it never falls back to an unauthenticated parse.
114
+ - **Cache integrity**: the on-disk (L2) cache is HMAC-SHA256 signed using a
115
+ key generated on first use and stored with owner-only (0600) permissions.
116
+ A tampered or foreign cache file fails signature verification and is
117
+ discarded before any deserialization occurs.
118
+ - **MCP/DAB connector**: binds to `127.0.0.1` with no cross-origin access
119
+ and read-only permissions by default. Wider exposure requires an explicit
120
+ `allow_remote=True` opt-in — review the security implications first, since
121
+ the connector has no authentication of its own.
122
+
123
+ ---
124
+
125
+ ## Installation
126
+
127
+ ```bash
128
+ pip install pystreampdf
129
+
130
+ # With exact token counting (tiktoken)
131
+ pip install "pystreampdf[tiktoken]"
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Documentation
137
+
138
+ - [Quick Start](docs/QUICKSTART.md) — Process your first PDF
139
+ - [Extraction Strategies](docs/EXTRACTION.md) — Different approaches for different documents
140
+ - [Token Budgeting](docs/TOKEN_BUDGETS.md) — Control context allocation
141
+ - [Token Budget Multipliers](docs/TOKEN_BUDGET_MULTIPLIERS.md) — Comprehensive token budget guide with examples
142
+ - [Examples](examples/) — Real-world RAG optimization
143
+
144
+ ## Quick Start: Document Extraction
145
+
146
+ ```python
147
+ from pystreampdf import SemanticChunker, PDFCache, TokenBudgetConfig, BudgetRule
148
+
149
+ # Setup budget config
150
+ budget_config = TokenBudgetConfig(
151
+ base_budget=800,
152
+ rules=[BudgetRule("complex", 1.1, match_fields=["filename"])]
153
+ )
154
+
155
+ # Initialize cache with budget management
156
+ cache = PDFCache(
157
+ memory_limit_mb=500,
158
+ disk_cache_dir="./pdf_cache",
159
+ token_budget_config=budget_config
160
+ )
161
+
162
+ # Process PDF with intelligent chunking
163
+ def extract_pdf(pdf_path):
164
+ chunks, preview, title, pages = cache.get_or_process(
165
+ pdf_path,
166
+ process_fn=extract_chunks
167
+ )
168
+ return chunks
169
+
170
+ # Use semantic chunker directly
171
+ chunker = SemanticChunker(target_tokens=500)
172
+ chunks = chunker.chunk_content(text, element_type=ElementType.TEXT, page_start=1, page_end=1)
173
+ ```
174
+
175
+ ## Budget Configuration
176
+
177
+ ### Default Settings
178
+ - **Minimum budget**: 500 tokens (fixed)
179
+ - **Maximum budget**: 1000 tokens (fixed)
180
+ - **Default base**: 1000 tokens
181
+ - **Multiplier range**: 0.5 - 1.5 (common)
182
+
183
+ ### Dynamic Adjustment via Multipliers
184
+ Use keyword-based rules to scale within the 500-1000 range without changing hard limits. See examples for [financial](docs/TOKEN_BUDGET_MULTIPLIERS.md#high-complexity-documents), [legal](docs/TOKEN_BUDGET_MULTIPLIERS.md#high-complexity-documents), and [summary](docs/TOKEN_BUDGET_MULTIPLIERS.md#low-complexity-documents) documents.
185
+
186
+ ## MCP Integration (optional, local by default)
187
+
188
+ PyStreamPDF ships an optional MCP/DAB connector (`pystreampdf._mcp_connector`)
189
+ exposing document-processing tools (extract text/tables/images, OCR,
190
+ structure detection, metadata, chunking, citations, validation). It's
191
+ opt-in, binds to `127.0.0.1` only, and requires `allow_remote=True` to be
192
+ exposed beyond localhost. See [examples/mcp_pystreampdf.py](examples/mcp_pystreampdf.py).
193
+
194
+ ## Known Issues
195
+
196
+ - Earlier versions of this README documented a `Document` class with
197
+ `.extract_relevant()` and a `.token_savings` attribute — verified
198
+ against source: this class has never existed in this package. Fixed
199
+ in this pass to describe the real, exported API (`SemanticChunker`,
200
+ `PDFCache`, `TokenBudgetConfig`, and the Rust-backed `pystreampdf.open()`).
201
+ - The published PyPI wheel is the only wheel on PyPI — macOS arm64 only,
202
+ no sdist. On any other platform or Python version, `pip install` will
203
+ fail without a local Rust toolchain to build from source.
204
+ - ~~The published version (2.2.1) is ahead of what's tagged in this repo's
205
+ Cargo.toml/__init__.py (2.2.0)~~ Resolved: 2.3.0 (this release) is
206
+ unambiguously ahead of both 2.2.0 and 2.2.1.
207
+ - The Rust-backed `pystreampdf.open()` / `pystreampdf.load_index()` API
208
+ (used in `examples/basic_parse.py`) silently becomes `None` if the
209
+ compiled `_core` extension isn't available (e.g. a from-source install
210
+ without `maturin develop`) — falling back to `SemanticChunker`/`PDFCache`
211
+ in that case, per the code above, not a hard error.
212
+ - The "Tests & Build" GitHub Actions workflow was red for several weeks
213
+ due to two CI infrastructure bugs (not the test suite itself), both
214
+ now fixed and confirmed green in CI:
215
+ [run 32611116541](https://github.com/Mullassery/PyStreamPDF/actions/runs/32611116541).
216
+ First, `dtolnay/rust-toolchain@v1` required an explicit `toolchain`
217
+ input the workflow didn't provide, failing both jobs before any tests
218
+ ran — fixed by pinning to `dtolnay/rust-toolchain@stable`. Second, once
219
+ that was fixed, `maturin develop` failed in the Python-test job because
220
+ `actions/setup-python` doesn't provide an active virtualenv, which
221
+ maturin requires — fixed by creating and activating a `.venv` before
222
+ the build step. Actual CI output as of 2.2.0: `pytest` — 536 passed, 2
223
+ skipped (Python 3.10, 3.11, and 3.12, each identical); `cargo test` — 23
224
+ passed, 0 failed. (The Rust suite has 23 tests, not 536 — an earlier
225
+ version of this section conflated the pytest count with the Rust one.)
226
+ As of 2.3.0: `pytest` — 557 passed, 2 skipped (21 new tests added for
227
+ `tests/test_mcp_tools.py`).
228
+
229
+ ## License
230
+
231
+ Proprietary License — Free to use with explicit attribution. See [LICENSE](LICENSE).
232
+
233
+ ---
234
+
235
+ **PyStreamPDF v2.3.0** | Intelligent PDF processing for AI | Python 3.9+ | 557 passing tests