structx 0.6.1__tar.gz → 0.6.3__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 (51) hide show
  1. {structx-0.6.1/structx.egg-info → structx-0.6.3}/PKG-INFO +2 -2
  2. {structx-0.6.1 → structx-0.6.3}/README.md +1 -1
  3. {structx-0.6.1 → structx-0.6.3}/pyproject.toml +3 -3
  4. {structx-0.6.1 → structx-0.6.3}/structx/__init__.py +3 -1
  5. {structx-0.6.1 → structx-0.6.3}/structx/core/input.py +21 -0
  6. {structx-0.6.1 → structx-0.6.3}/structx/extraction/extractor.py +27 -9
  7. {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/input_processor.py +17 -10
  8. {structx-0.6.1 → structx-0.6.3/structx.egg-info}/PKG-INFO +2 -2
  9. {structx-0.6.1 → structx-0.6.3}/tests/test_extractor_async.py +29 -0
  10. {structx-0.6.1 → structx-0.6.3}/tests/test_extractor_flow.py +44 -1
  11. {structx-0.6.1 → structx-0.6.3}/tests/test_input_and_batch_processors.py +25 -0
  12. {structx-0.6.1 → structx-0.6.3}/LICENSE +0 -0
  13. {structx-0.6.1 → structx-0.6.3}/setup.cfg +0 -0
  14. {structx-0.6.1 → structx-0.6.3}/structx/core/__init__.py +0 -0
  15. {structx-0.6.1 → structx-0.6.3}/structx/core/config.py +0 -0
  16. {structx-0.6.1 → structx-0.6.3}/structx/core/exceptions.py +0 -0
  17. {structx-0.6.1 → structx-0.6.3}/structx/core/models.py +0 -0
  18. {structx-0.6.1 → structx-0.6.3}/structx/core/type_system.py +0 -0
  19. {structx-0.6.1 → structx-0.6.3}/structx/extraction/__init__.py +0 -0
  20. {structx-0.6.1 → structx-0.6.3}/structx/extraction/core/__init__.py +0 -0
  21. {structx-0.6.1 → structx-0.6.3}/structx/extraction/core/llm_core.py +0 -0
  22. {structx-0.6.1 → structx-0.6.3}/structx/extraction/engines/__init__.py +0 -0
  23. {structx-0.6.1 → structx-0.6.3}/structx/extraction/engines/extraction_engine.py +0 -0
  24. {structx-0.6.1 → structx-0.6.3}/structx/extraction/generator.py +0 -0
  25. {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/__init__.py +0 -0
  26. {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/batch_processor.py +0 -0
  27. {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/content_analyzer.py +0 -0
  28. {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/model_operations.py +0 -0
  29. {structx-0.6.1 → structx-0.6.3}/structx/extraction/result_manager.py +0 -0
  30. {structx-0.6.1 → structx-0.6.3}/structx/schema.py +0 -0
  31. {structx-0.6.1 → structx-0.6.3}/structx/utils/__init__.py +0 -0
  32. {structx-0.6.1 → structx-0.6.3}/structx/utils/file_reader.py +0 -0
  33. {structx-0.6.1 → structx-0.6.3}/structx/utils/helpers.py +0 -0
  34. {structx-0.6.1 → structx-0.6.3}/structx/utils/prompts.py +0 -0
  35. {structx-0.6.1 → structx-0.6.3}/structx/utils/types.py +0 -0
  36. {structx-0.6.1 → structx-0.6.3}/structx/utils/usage.py +0 -0
  37. {structx-0.6.1 → structx-0.6.3}/structx.egg-info/SOURCES.txt +0 -0
  38. {structx-0.6.1 → structx-0.6.3}/structx.egg-info/dependency_links.txt +0 -0
  39. {structx-0.6.1 → structx-0.6.3}/structx.egg-info/requires.txt +0 -0
  40. {structx-0.6.1 → structx-0.6.3}/structx.egg-info/top_level.txt +0 -0
  41. {structx-0.6.1 → structx-0.6.3}/tests/test_config_and_model_utils.py +0 -0
  42. {structx-0.6.1 → structx-0.6.3}/tests/test_content_analyzer.py +0 -0
  43. {structx-0.6.1 → structx-0.6.3}/tests/test_document_conversion_integration.py +0 -0
  44. {structx-0.6.1 → structx-0.6.3}/tests/test_extraction_engine.py +0 -0
  45. {structx-0.6.1 → structx-0.6.3}/tests/test_file_reader.py +0 -0
  46. {structx-0.6.1 → structx-0.6.3}/tests/test_live_llm.py +0 -0
  47. {structx-0.6.1 → structx-0.6.3}/tests/test_llm_configuration.py +0 -0
  48. {structx-0.6.1 → structx-0.6.3}/tests/test_result_manager_and_helpers.py +0 -0
  49. {structx-0.6.1 → structx-0.6.3}/tests/test_schema_contract.py +0 -0
  50. {structx-0.6.1 → structx-0.6.3}/tests/test_type_system_and_planning.py +0 -0
  51. {structx-0.6.1 → structx-0.6.3}/tests/test_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structx
3
- Version: 0.6.1
3
+ Version: 0.6.3
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
@@ -64,7 +64,7 @@ Advanced structured data extraction from any document using LLMs with multimodal
64
64
  support.
65
65
 
66
66
  [![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.1-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
67
+ [![PyPI](https://img.shields.io/badge/PyPi-0.6.3-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
68
68
  [![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)](# "Build with GitHub Actions")
69
69
 
70
70
  See the [project roadmap](docs/ROADMAP.md) for planned portable business rules
@@ -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.1-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
7
+ [![PyPI](https://img.shields.io/badge/PyPi-0.6.3-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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structx"
7
- version = "0.6.1"
7
+ version = "0.6.3"
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"]
@@ -92,9 +92,9 @@ torchvision = [
92
92
  ]
93
93
 
94
94
  [tool.bumpver]
95
- current_version = "0.6.1"
95
+ current_version = "0.6.3"
96
96
  version_pattern = "MAJOR.MINOR.PATCH"
97
- commit_message = "Bump version {old_version} -> {new_version}"
97
+ commit_message = "chore(release): bump version {old_version} -> {new_version}"
98
98
  commit = true
99
99
  tag = true
100
100
  push = 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.1"
25
+ __version__ = "0.6.3"
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structx
3
- Version: 0.6.1
3
+ Version: 0.6.3
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
@@ -64,7 +64,7 @@ Advanced structured data extraction from any document using LLMs with multimodal
64
64
  support.
65
65
 
66
66
  [![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.1-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
67
+ [![PyPI](https://img.shields.io/badge/PyPi-0.6.3-blue?style=for-the-badge)](https://pypi.org/project/structx "Package")
68
68
  [![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white)](# "Build with GitHub Actions")
69
69
 
70
70
  See the [project roadmap](docs/ROADMAP.md) for planned portable business rules
@@ -85,3 +85,32 @@ def test_extract_async_requires_an_async_client_for_provider_calls():
85
85
  model=AsyncRecord,
86
86
  )
87
87
  )
88
+
89
+
90
+ def test_explicit_prepared_input_can_be_inspected_and_reused_async():
91
+ completions = TrackingAsyncCompletions()
92
+ extractor = Extractor(
93
+ client=SimpleNamespace(),
94
+ async_client=SimpleNamespace(chat=SimpleNamespace(completions=completions)),
95
+ model_name="provider/model",
96
+ max_threads=1,
97
+ max_retries=0,
98
+ )
99
+
100
+ async def prepare_and_extract():
101
+ async with extractor.prepare_input_async(
102
+ data=[{"text": "row-0"}]
103
+ ) as prepared_input:
104
+ assert prepared_input.dataframe.shape == (1, 1)
105
+ result = await extractor.extract_async(
106
+ data=prepared_input,
107
+ query="extract value",
108
+ model=AsyncRecord,
109
+ )
110
+ assert not prepared_input.closed
111
+ return result, prepared_input
112
+
113
+ result, prepared_input = asyncio.run(prepare_and_extract())
114
+
115
+ assert [item.value for item in result.data] == ["row-0"]
116
+ assert prepared_input.closed
@@ -1,8 +1,9 @@
1
- from types import SimpleNamespace
2
1
  from contextlib import contextmanager
2
+ from types import SimpleNamespace
3
3
 
4
4
  import pandas as pd
5
5
  import pytest
6
+ from pydantic import BaseModel
6
7
 
7
8
  from structx.core.exceptions import ConfigurationError, ExtractionError
8
9
  from structx.core.input import PreparedInput
@@ -85,3 +86,45 @@ def test_extract_queries_prepares_and_cleans_data_once(monkeypatch):
85
86
  "cleanup": 1,
86
87
  "process": ["payment", "termination"],
87
88
  }
89
+
90
+
91
+ def test_get_schema_accepts_caller_owned_prepared_input(monkeypatch):
92
+ class GeneratedRecord(BaseModel):
93
+ value: str
94
+
95
+ extractor = Extractor(client=_unused_client(), model_name="provider/model")
96
+ monkeypatch.setattr(
97
+ extractor.model_operations,
98
+ "generate_extraction_plan",
99
+ lambda **kwargs: SimpleNamespace(extraction_schema=object()),
100
+ )
101
+ monkeypatch.setattr(
102
+ extractor.model_operations,
103
+ "create_model_from_schema",
104
+ lambda schema: GeneratedRecord,
105
+ )
106
+
107
+ with extractor.prepare_input(
108
+ data=pd.DataFrame({"text": ["agreement"]})
109
+ ) as prepared_input:
110
+ model = extractor.get_schema(
111
+ data=prepared_input,
112
+ query="extract value",
113
+ )
114
+
115
+ assert model is GeneratedRecord
116
+ assert not prepared_input.closed
117
+
118
+ assert prepared_input.closed
119
+
120
+
121
+ def test_prepare_input_context_cleans_up_after_an_error():
122
+ extractor = Extractor(client=_unused_client(), model_name="provider/model")
123
+
124
+ with pytest.raises(RuntimeError, match="stop inspection"):
125
+ with extractor.prepare_input(
126
+ data=pd.DataFrame({"text": ["agreement"]})
127
+ ) as prepared_input:
128
+ raise RuntimeError("stop inspection")
129
+
130
+ assert prepared_input.closed
@@ -161,3 +161,28 @@ def test_cleanup_data_removes_all_owned_temporary_paths(tmp_path):
161
161
 
162
162
  assert all(not path.exists() for path in paths)
163
163
  assert prepared_input.owned_paths == []
164
+ assert prepared_input.closed
165
+
166
+
167
+ def test_explicitly_prepared_input_is_reused_without_automatic_cleanup(tmp_path):
168
+ owned_path = tmp_path / "rendered.pdf"
169
+ owned_path.write_text("temporary", encoding="utf-8")
170
+ prepared_input = PreparedInput(
171
+ dataframe=pd.DataFrame({"source": ["document.docx"]}),
172
+ owned_paths=[owned_path],
173
+ )
174
+ processor = InputProcessor()
175
+
176
+ with processor.prepared(prepared_input) as reused:
177
+ assert reused is prepared_input
178
+
179
+ assert owned_path.exists()
180
+ assert not prepared_input.closed
181
+
182
+ prepared_input.close()
183
+ prepared_input.close()
184
+
185
+ assert not owned_path.exists()
186
+ assert prepared_input.closed
187
+ with pytest.raises(RuntimeError, match="closed"):
188
+ processor.prepare(prepared_input)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes