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.
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/Cargo.lock +2 -2
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/Cargo.toml +1 -1
- pystreampdf-2.3.0/PKG-INFO +271 -0
- pystreampdf-2.3.0/README.md +235 -0
- pystreampdf-2.3.0/core/src/pdf_parser.rs +183 -0
- pystreampdf-2.3.0/core/src/security.rs +205 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/pyproject.toml +13 -1
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/__init__.py +3 -2
- pystreampdf-2.3.0/python/pystreampdf/_mcp_connector.py +179 -0
- pystreampdf-2.3.0/python/pystreampdf/_mcp_tools.py +537 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cache.py +82 -9
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli_daemon.py +14 -6
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/extraction.py +7 -3
- pystreampdf-2.3.0/python/pystreampdf/okf_entity_profiles.py +82 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/metadata.py +3 -1
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/selector.py +7 -2
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/assembler.py +14 -8
- pystreampdf-2.3.0/python/pystreampdf/tokenizer.py +68 -0
- pystreampdf-2.1.2/PKG-INFO +0 -243
- pystreampdf-2.1.2/README.md +0 -216
- pystreampdf-2.1.2/core/src/pdf_parser.rs +0 -179
- pystreampdf-2.1.2/core/src/security.rs +0 -126
- pystreampdf-2.1.2/python/pystreampdf/intelligence/__init__.py.bak +0 -28
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/LICENSE +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/Cargo.toml +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/audit.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/context.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/document.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/error.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/forms.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/heading_extractor.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/index.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/lib.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/markdown.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/navigator.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/page.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/core/src/structure.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/Cargo.toml +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/cli_dashboard.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/excel_export.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/code_intelligence.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/json_intelligence.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/log_intelligence.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/sql_intelligence.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/types.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/intelligence/yaml_intelligence.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/manager.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/pipeline.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/provider.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/paddle.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/ocr/providers/tesseract.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/chunking.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/optimization/compressor.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/pipeline.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/scripts/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/scripts/post_install.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/citations.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/entities.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/graph.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/relationships.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/topics.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/semantic/verifier.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/builder.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/exporters.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/recovery.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/structure/relationships.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/token_budget.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/__init__.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/layout.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/scorer.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/table.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/text.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/pystreampdf/validation/types.py +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/src/lib.rs +0 -0
- {pystreampdf-2.1.2 → pystreampdf-2.3.0}/python/streampdf/cli.py +0 -0
- {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.
|
|
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.
|
|
724
|
+
version = "2.3.0"
|
|
725
725
|
dependencies = [
|
|
726
726
|
"chrono",
|
|
727
727
|
"pdfium-render",
|
|
@@ -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
|
+
[](https://pypi.org/project/pystreampdf)
|
|
46
|
+
[](https://www.python.org)
|
|
47
|
+
[](./tests)
|
|
48
|
+
[](./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
|
+
[](https://pypi.org/project/pystreampdf)
|
|
11
|
+
[](https://www.python.org)
|
|
12
|
+
[](./tests)
|
|
13
|
+
[](./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
|