structx 0.6.2__tar.gz → 0.6.4__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 (54) hide show
  1. {structx-0.6.2/structx.egg-info → structx-0.6.4}/PKG-INFO +33 -2
  2. {structx-0.6.2 → structx-0.6.4}/README.md +26 -1
  3. {structx-0.6.2 → structx-0.6.4}/pyproject.toml +16 -2
  4. {structx-0.6.2 → structx-0.6.4}/structx/__init__.py +3 -1
  5. {structx-0.6.2 → structx-0.6.4}/structx/core/input.py +21 -0
  6. {structx-0.6.2 → structx-0.6.4}/structx/extraction/extractor.py +27 -9
  7. {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/input_processor.py +17 -10
  8. structx-0.6.4/structx/measurement.py +408 -0
  9. structx-0.6.4/structx/utils/measurement_ocr.py +92 -0
  10. {structx-0.6.2 → structx-0.6.4/structx.egg-info}/PKG-INFO +33 -2
  11. {structx-0.6.2 → structx-0.6.4}/structx.egg-info/SOURCES.txt +3 -0
  12. {structx-0.6.2 → structx-0.6.4}/structx.egg-info/requires.txt +8 -0
  13. {structx-0.6.2 → structx-0.6.4}/tests/test_extractor_async.py +29 -0
  14. {structx-0.6.2 → structx-0.6.4}/tests/test_extractor_flow.py +44 -1
  15. {structx-0.6.2 → structx-0.6.4}/tests/test_input_and_batch_processors.py +25 -0
  16. structx-0.6.4/tests/test_measurement.py +464 -0
  17. {structx-0.6.2 → structx-0.6.4}/LICENSE +0 -0
  18. {structx-0.6.2 → structx-0.6.4}/setup.cfg +0 -0
  19. {structx-0.6.2 → structx-0.6.4}/structx/core/__init__.py +0 -0
  20. {structx-0.6.2 → structx-0.6.4}/structx/core/config.py +0 -0
  21. {structx-0.6.2 → structx-0.6.4}/structx/core/exceptions.py +0 -0
  22. {structx-0.6.2 → structx-0.6.4}/structx/core/models.py +0 -0
  23. {structx-0.6.2 → structx-0.6.4}/structx/core/type_system.py +0 -0
  24. {structx-0.6.2 → structx-0.6.4}/structx/extraction/__init__.py +0 -0
  25. {structx-0.6.2 → structx-0.6.4}/structx/extraction/core/__init__.py +0 -0
  26. {structx-0.6.2 → structx-0.6.4}/structx/extraction/core/llm_core.py +0 -0
  27. {structx-0.6.2 → structx-0.6.4}/structx/extraction/engines/__init__.py +0 -0
  28. {structx-0.6.2 → structx-0.6.4}/structx/extraction/engines/extraction_engine.py +0 -0
  29. {structx-0.6.2 → structx-0.6.4}/structx/extraction/generator.py +0 -0
  30. {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/__init__.py +0 -0
  31. {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/batch_processor.py +0 -0
  32. {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/content_analyzer.py +0 -0
  33. {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/model_operations.py +0 -0
  34. {structx-0.6.2 → structx-0.6.4}/structx/extraction/result_manager.py +0 -0
  35. {structx-0.6.2 → structx-0.6.4}/structx/schema.py +0 -0
  36. {structx-0.6.2 → structx-0.6.4}/structx/utils/__init__.py +0 -0
  37. {structx-0.6.2 → structx-0.6.4}/structx/utils/file_reader.py +0 -0
  38. {structx-0.6.2 → structx-0.6.4}/structx/utils/helpers.py +0 -0
  39. {structx-0.6.2 → structx-0.6.4}/structx/utils/prompts.py +0 -0
  40. {structx-0.6.2 → structx-0.6.4}/structx/utils/types.py +0 -0
  41. {structx-0.6.2 → structx-0.6.4}/structx/utils/usage.py +0 -0
  42. {structx-0.6.2 → structx-0.6.4}/structx.egg-info/dependency_links.txt +0 -0
  43. {structx-0.6.2 → structx-0.6.4}/structx.egg-info/top_level.txt +0 -0
  44. {structx-0.6.2 → structx-0.6.4}/tests/test_config_and_model_utils.py +0 -0
  45. {structx-0.6.2 → structx-0.6.4}/tests/test_content_analyzer.py +0 -0
  46. {structx-0.6.2 → structx-0.6.4}/tests/test_document_conversion_integration.py +0 -0
  47. {structx-0.6.2 → structx-0.6.4}/tests/test_extraction_engine.py +0 -0
  48. {structx-0.6.2 → structx-0.6.4}/tests/test_file_reader.py +0 -0
  49. {structx-0.6.2 → structx-0.6.4}/tests/test_live_llm.py +0 -0
  50. {structx-0.6.2 → structx-0.6.4}/tests/test_llm_configuration.py +0 -0
  51. {structx-0.6.2 → structx-0.6.4}/tests/test_result_manager_and_helpers.py +0 -0
  52. {structx-0.6.2 → structx-0.6.4}/tests/test_schema_contract.py +0 -0
  53. {structx-0.6.2 → structx-0.6.4}/tests/test_type_system_and_planning.py +0 -0
  54. {structx-0.6.2 → structx-0.6.4}/tests/test_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structx
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Structured data extraction from text using LLMs and dynamic model generation
5
5
  Author-email: blacksuan19 <py@aolabs.dev>
6
6
  Classifier: Development Status :: 5 - Production/Stable
@@ -49,6 +49,12 @@ Requires-Dist: docling-slim[convert-core,format-docx,format-html,format-latex,fo
49
49
  Requires-Dist: torch>=2.13.0; extra == "docs"
50
50
  Requires-Dist: torchvision>=0.28.0; extra == "docs"
51
51
  Requires-Dist: weasyprint>=69.0; extra == "docs"
52
+ Provides-Extra: measurement
53
+ Requires-Dist: pypdfium2>=5.12.1; extra == "measurement"
54
+ Provides-Extra: measurement-ocr
55
+ Requires-Dist: pypdfium2>=5.12.1; extra == "measurement-ocr"
56
+ Requires-Dist: rapidocr>=3.9.0; extra == "measurement-ocr"
57
+ Requires-Dist: onnxruntime>=1.22.0; extra == "measurement-ocr"
52
58
  Provides-Extra: mkdocs
53
59
  Requires-Dist: mkdocs>=1.6.1; extra == "mkdocs"
54
60
  Requires-Dist: mkdocs-material>=9.7.6; extra == "mkdocs"
@@ -64,7 +70,7 @@ Advanced structured data extraction from any document using LLMs with multimodal
64
70
  support.
65
71
 
66
72
  [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg?style=for-the-badge)](https://structx.aolabs.dev "Documentation")
67
- [![PyPI](https://img.shields.io/badge/PyPi-0.6.2-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
73
+ [![PyPI](https://img.shields.io/badge/PyPi-0.6.4-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
68
74
  [![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)](# "Build with GitHub Actions")
69
75
 
70
76
  See the [project roadmap](docs/ROADMAP.md) for planned portable business rules
@@ -140,12 +146,21 @@ non-PDF document formats:
140
146
  pip install "structx[docs]"
141
147
  ```
142
148
 
149
+ For estimating PDF page and text counts before extraction, without running an
150
+ extraction:
151
+
152
+ ```bash
153
+ pip install "structx[measurement]" # embedded text only
154
+ pip install "structx[measurement-ocr]" # adds local OCR for scanned pages
155
+ ```
156
+
143
157
  ### 🔧 What The Package Provides
144
158
 
145
159
  - Structured readers for CSV, Excel, JSON, Parquet, and Feather
146
160
  - Instructor multimodal vision support
147
161
  - Optional Docling document parsing with CPU-only PyTorch resolution for uv on Linux
148
162
  - Optional WeasyPrint PDF rendering for non-PDF document formats
163
+ - Optional standalone document measurement, separate from the extraction path
149
164
 
150
165
  ## Quick Start
151
166
 
@@ -193,6 +208,22 @@ result = extractor.extract(
193
208
  )
194
209
  ```
195
210
 
211
+ ### 📏 Document Measurement Before Extraction
212
+
213
+ Install `structx[measurement]` to size a PDF without extracting it. This path is
214
+ independent of extraction: it never converts or replaces the input file.
215
+
216
+ ```python
217
+ from structx.measurement import DocumentMeasurer
218
+
219
+ measurement = DocumentMeasurer(ocr_mode="auto").measure(
220
+ "scripts/example_input/S0305SampleInvoice.pdf"
221
+ )
222
+
223
+ # Estimates, plus whether every page could actually be measured
224
+ print(measurement.page_count, measurement.character_count, measurement.status)
225
+ ```
226
+
196
227
  ### 📊 Token Usage Monitoring
197
228
 
198
229
  ```python
@@ -4,7 +4,7 @@ Advanced structured data extraction from any document using LLMs with multimodal
4
4
  support.
5
5
 
6
6
  [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg?style=for-the-badge)](https://structx.aolabs.dev "Documentation")
7
- [![PyPI](https://img.shields.io/badge/PyPi-0.6.2-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
7
+ [![PyPI](https://img.shields.io/badge/PyPi-0.6.4-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
8
8
  [![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)](# "Build with GitHub Actions")
9
9
 
10
10
  See the [project roadmap](docs/ROADMAP.md) for planned portable business rules
@@ -80,12 +80,21 @@ non-PDF document formats:
80
80
  pip install "structx[docs]"
81
81
  ```
82
82
 
83
+ For estimating PDF page and text counts before extraction, without running an
84
+ extraction:
85
+
86
+ ```bash
87
+ pip install "structx[measurement]" # embedded text only
88
+ pip install "structx[measurement-ocr]" # adds local OCR for scanned pages
89
+ ```
90
+
83
91
  ### 🔧 What The Package Provides
84
92
 
85
93
  - Structured readers for CSV, Excel, JSON, Parquet, and Feather
86
94
  - Instructor multimodal vision support
87
95
  - Optional Docling document parsing with CPU-only PyTorch resolution for uv on Linux
88
96
  - Optional WeasyPrint PDF rendering for non-PDF document formats
97
+ - Optional standalone document measurement, separate from the extraction path
89
98
 
90
99
  ## Quick Start
91
100
 
@@ -133,6 +142,22 @@ result = extractor.extract(
133
142
  )
134
143
  ```
135
144
 
145
+ ### 📏 Document Measurement Before Extraction
146
+
147
+ Install `structx[measurement]` to size a PDF without extracting it. This path is
148
+ independent of extraction: it never converts or replaces the input file.
149
+
150
+ ```python
151
+ from structx.measurement import DocumentMeasurer
152
+
153
+ measurement = DocumentMeasurer(ocr_mode="auto").measure(
154
+ "scripts/example_input/S0305SampleInvoice.pdf"
155
+ )
156
+
157
+ # Estimates, plus whether every page could actually be measured
158
+ print(measurement.page_count, measurement.character_count, measurement.status)
159
+ ```
160
+
136
161
  ### 📊 Token Usage Monitoring
137
162
 
138
163
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structx"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Structured data extraction from text using LLMs and dynamic model generation"
9
9
  authors = [{ name = "blacksuan19", email = "py@aolabs.dev" }]
10
10
  license-files = ["LICENSE"]
@@ -68,6 +68,20 @@ docs = [
68
68
  "weasyprint>=69.0",
69
69
  ]
70
70
 
71
+ # Optional standalone document measurement (structx.measurement). Independent
72
+ # of extraction: it estimates page and text counts for callers that need a
73
+ # size estimate before running an extraction.
74
+ measurement = [
75
+ "pypdfium2>=5.12.1",
76
+ ]
77
+
78
+ # Local OCR for measuring pages that have no embedded text layer.
79
+ measurement-ocr = [
80
+ "pypdfium2>=5.12.1",
81
+ "rapidocr>=3.9.0",
82
+ "onnxruntime>=1.22.0",
83
+ ]
84
+
71
85
  # Documentation site tooling.
72
86
  mkdocs = [
73
87
  "mkdocs>=1.6.1",
@@ -92,7 +106,7 @@ torchvision = [
92
106
  ]
93
107
 
94
108
  [tool.bumpver]
95
- current_version = "0.6.2"
109
+ current_version = "0.6.4"
96
110
  version_pattern = "MAJOR.MINOR.PATCH"
97
111
  commit_message = "chore(release): bump version {old_version} -> {new_version}"
98
112
  commit = true
@@ -3,6 +3,7 @@ structx: Structured data extraction using LLMs
3
3
  """
4
4
 
5
5
  from structx.core.config import ExtractionConfig, StepConfig
6
+ from structx.core.input import PreparedInput
6
7
  from structx.core.models import (
7
8
  ExtractionPlan,
8
9
  ExtractionRequest,
@@ -21,11 +22,12 @@ from structx.schema import (
21
22
  model_to_extraction_request,
22
23
  )
23
24
 
24
- __version__ = "0.6.2"
25
+ __version__ = "0.6.4"
25
26
  __all__ = [
26
27
  "Extractor",
27
28
  "ExtractionConfig",
28
29
  "StepConfig",
30
+ "PreparedInput",
29
31
  "ModelField",
30
32
  "ExtractionPlan",
31
33
  "ExtractionRequest",
@@ -47,11 +47,32 @@ class PreparedInput:
47
47
  pdf_rows: Dict[int, PdfRow] = field(default_factory=dict)
48
48
  planning_sample: Optional[str] = None
49
49
  owned_paths: List[Path] = field(default_factory=list)
50
+ _closed: bool = field(default=False, init=False, repr=False)
51
+
52
+ @property
53
+ def closed(self) -> bool:
54
+ """Whether temporary resources owned by this input were released."""
55
+ return self._closed
56
+
57
+ def ensure_open(self) -> None:
58
+ """Reject reuse after explicit cleanup."""
59
+ if self._closed:
60
+ raise RuntimeError("Prepared input is closed")
61
+
62
+ def close(self) -> None:
63
+ """Release owned temporary resources. This operation is idempotent."""
64
+ if self._closed:
65
+ return
66
+ for path in self.owned_paths:
67
+ Path(path).unlink(missing_ok=True)
68
+ self.owned_paths.clear()
69
+ self._closed = True
50
70
 
51
71
  def row_payload(
52
72
  self, position: int, row: pd.Series, target_columns: List[str]
53
73
  ) -> RowPayload:
54
74
  """Build the text or PDF payload for one positional input row."""
75
+ self.ensure_open()
55
76
  pdf_row = self.pdf_rows.get(position)
56
77
  if pdf_row is not None:
57
78
  return pdf_row
@@ -1,12 +1,12 @@
1
1
  """Public orchestration for structured extraction operations."""
2
2
 
3
3
  import asyncio
4
+ from contextlib import asynccontextmanager, contextmanager
4
5
  from dataclasses import dataclass
5
6
  from functools import partial
6
7
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional, Type, Union
8
+ from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Type, Union
8
9
 
9
- import pandas as pd
10
10
  from instructor import AsyncInstructor, Instructor
11
11
  from loguru import logger
12
12
  from pydantic import BaseModel
@@ -19,7 +19,7 @@ from structx.extraction.core.llm_core import LLMCore
19
19
  from structx.extraction.engines.extraction_engine import ExtractionEngine
20
20
  from structx.extraction.processors.batch_processor import BatchProcessor
21
21
  from structx.extraction.processors.content_analyzer import ContentAnalyzer
22
- from structx.extraction.processors.input_processor import InputProcessor
22
+ from structx.extraction.processors.input_processor import InputData, InputProcessor
23
23
  from structx.extraction.processors.model_operations import ModelOperations
24
24
  from structx.extraction.result_manager import ResultCollector
25
25
  from structx.utils.helpers import handle_errors
@@ -138,6 +138,24 @@ class Extractor:
138
138
  raise ValueError("queries must not contain duplicates")
139
139
  return validated
140
140
 
141
+ @contextmanager
142
+ def prepare_input(
143
+ self, *, data: InputData, **kwargs: Any
144
+ ) -> Generator[PreparedInput, None, None]:
145
+ """Prepare input once and release owned resources after the context."""
146
+ with self.input_processor.prepared(data, **kwargs) as prepared_input:
147
+ yield prepared_input
148
+
149
+ @asynccontextmanager
150
+ async def prepare_input_async(
151
+ self, *, data: InputData, **kwargs: Any
152
+ ) -> AsyncGenerator[PreparedInput, None]:
153
+ """Prepare input off-loop and release resources after the async context."""
154
+ async with self.input_processor.prepared_async(
155
+ data, **kwargs
156
+ ) as prepared_input:
157
+ yield prepared_input
158
+
141
159
  def _build_strategy(
142
160
  self,
143
161
  prepared_input: PreparedInput,
@@ -377,7 +395,7 @@ class Extractor:
377
395
  def extract(
378
396
  self,
379
397
  *,
380
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
398
+ data: InputData,
381
399
  query: str,
382
400
  model: Optional[Type[BaseModel]] = None,
383
401
  return_df: bool = False,
@@ -407,7 +425,7 @@ class Extractor:
407
425
  async def extract_async(
408
426
  self,
409
427
  *,
410
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
428
+ data: InputData,
411
429
  query: str,
412
430
  model: Optional[Type[BaseModel]] = None,
413
431
  return_df: bool = False,
@@ -443,7 +461,7 @@ class Extractor:
443
461
  def extract_queries(
444
462
  self,
445
463
  *,
446
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
464
+ data: InputData,
447
465
  queries: List[str],
448
466
  return_df: bool = True,
449
467
  expand_nested: bool = False,
@@ -478,7 +496,7 @@ class Extractor:
478
496
  async def extract_queries_async(
479
497
  self,
480
498
  *,
481
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
499
+ data: InputData,
482
500
  queries: List[str],
483
501
  return_df: bool = True,
484
502
  expand_nested: bool = False,
@@ -518,7 +536,7 @@ class Extractor:
518
536
  def get_schema(
519
537
  self,
520
538
  *,
521
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
539
+ data: InputData,
522
540
  query: str,
523
541
  **kwargs: Any,
524
542
  ) -> Type[BaseModel]:
@@ -557,7 +575,7 @@ class Extractor:
557
575
  async def get_schema_async(
558
576
  self,
559
577
  *,
560
- data: Union[str, Path, pd.DataFrame, List[Dict[str, str]]],
578
+ data: InputData,
561
579
  query: str,
562
580
  **kwargs: Any,
563
581
  ) -> Type[BaseModel]:
@@ -3,7 +3,7 @@
3
3
  import asyncio
4
4
  from contextlib import asynccontextmanager, contextmanager
5
5
  from pathlib import Path
6
- from typing import Any, AsyncIterator, Dict, Iterator, List, Union
6
+ from typing import Any, AsyncGenerator, Dict, Generator, List, Union
7
7
 
8
8
  import pandas as pd
9
9
 
@@ -11,35 +11,44 @@ from structx.core.exceptions import FileError
11
11
  from structx.core.input import PreparedInput
12
12
  from structx.utils.file_reader import FileReader
13
13
 
14
- InputData = Union[str, Path, pd.DataFrame, List[Dict[str, str]]]
14
+ InputData = Union[str, Path, pd.DataFrame, List[Dict[str, str]], PreparedInput]
15
15
 
16
16
 
17
17
  class InputProcessor:
18
18
  """Prepare supported inputs and own any temporary files they create."""
19
19
 
20
20
  @contextmanager
21
- def prepared(self, data: InputData, **kwargs: Any) -> Iterator[PreparedInput]:
21
+ def prepared(
22
+ self, data: InputData, **kwargs: Any
23
+ ) -> Generator[PreparedInput, None, None]:
22
24
  """Yield normalized data and always release owned temporary files."""
25
+ owns_prepared_input = not isinstance(data, PreparedInput)
23
26
  prepared_input = self.prepare(data, **kwargs)
24
27
  try:
25
28
  yield prepared_input
26
29
  finally:
27
- self.cleanup(prepared_input)
30
+ if owns_prepared_input:
31
+ self.cleanup(prepared_input)
28
32
 
29
33
  @asynccontextmanager
30
34
  async def prepared_async(
31
35
  self, data: InputData, **kwargs: Any
32
- ) -> AsyncIterator[PreparedInput]:
36
+ ) -> AsyncGenerator[PreparedInput, None]:
33
37
  """Prepare blocking file inputs off-loop and own their lifetime."""
38
+ owns_prepared_input = not isinstance(data, PreparedInput)
34
39
  prepared_input = await asyncio.to_thread(self.prepare, data, **kwargs)
35
40
  try:
36
41
  yield prepared_input
37
42
  finally:
38
- await asyncio.to_thread(self.cleanup, prepared_input)
43
+ if owns_prepared_input:
44
+ await asyncio.to_thread(self.cleanup, prepared_input)
39
45
 
40
46
  def prepare(self, data: InputData, **kwargs: Any) -> PreparedInput:
41
47
  """Normalize a supported input and retain its explicit resource metadata."""
42
- if isinstance(data, pd.DataFrame):
48
+ if isinstance(data, PreparedInput):
49
+ data.ensure_open()
50
+ prepared_input = data
51
+ elif isinstance(data, pd.DataFrame):
43
52
  prepared_input = PreparedInput(dataframe=data)
44
53
  elif isinstance(data, list) and all(isinstance(item, dict) for item in data):
45
54
  prepared_input = PreparedInput(dataframe=pd.DataFrame(data))
@@ -102,6 +111,4 @@ class InputProcessor:
102
111
  @staticmethod
103
112
  def cleanup(prepared_input: PreparedInput) -> None:
104
113
  """Remove temporary artifacts owned by a prepared input."""
105
- for path in prepared_input.owned_paths:
106
- Path(path).unlink(missing_ok=True)
107
- prepared_input.owned_paths.clear()
114
+ prepared_input.close()