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.
- {structx-0.6.1/structx.egg-info → structx-0.6.3}/PKG-INFO +2 -2
- {structx-0.6.1 → structx-0.6.3}/README.md +1 -1
- {structx-0.6.1 → structx-0.6.3}/pyproject.toml +3 -3
- {structx-0.6.1 → structx-0.6.3}/structx/__init__.py +3 -1
- {structx-0.6.1 → structx-0.6.3}/structx/core/input.py +21 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/extractor.py +27 -9
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/input_processor.py +17 -10
- {structx-0.6.1 → structx-0.6.3/structx.egg-info}/PKG-INFO +2 -2
- {structx-0.6.1 → structx-0.6.3}/tests/test_extractor_async.py +29 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_extractor_flow.py +44 -1
- {structx-0.6.1 → structx-0.6.3}/tests/test_input_and_batch_processors.py +25 -0
- {structx-0.6.1 → structx-0.6.3}/LICENSE +0 -0
- {structx-0.6.1 → structx-0.6.3}/setup.cfg +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/core/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/core/config.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/core/exceptions.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/core/models.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/core/type_system.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/core/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/core/llm_core.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/engines/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/engines/extraction_engine.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/generator.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/batch_processor.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/content_analyzer.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/processors/model_operations.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/extraction/result_manager.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/schema.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/__init__.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/file_reader.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/helpers.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/prompts.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/types.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx/utils/usage.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx.egg-info/SOURCES.txt +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx.egg-info/dependency_links.txt +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx.egg-info/requires.txt +0 -0
- {structx-0.6.1 → structx-0.6.3}/structx.egg-info/top_level.txt +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_config_and_model_utils.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_content_analyzer.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_document_conversion_integration.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_extraction_engine.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_file_reader.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_live_llm.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_llm_configuration.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_result_manager_and_helpers.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_schema_contract.py +0 -0
- {structx-0.6.1 → structx-0.6.3}/tests/test_type_system_and_planning.py +0 -0
- {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.
|
|
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
|
[](https://structx.aolabs.dev "Documentation")
|
|
67
|
-
[](https://pypi.org/project/structx "Package")
|
|
68
68
|
[](# "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
|
[](https://structx.aolabs.dev "Documentation")
|
|
7
|
-
[](https://pypi.org/project/structx "Package")
|
|
8
8
|
[](# "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.
|
|
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.
|
|
95
|
+
current_version = "0.6.3"
|
|
96
96
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
97
|
-
commit_message = "
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
[](https://structx.aolabs.dev "Documentation")
|
|
67
|
-
[](https://pypi.org/project/structx "Package")
|
|
68
68
|
[](# "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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|