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.
- {structx-0.6.2/structx.egg-info → structx-0.6.4}/PKG-INFO +33 -2
- {structx-0.6.2 → structx-0.6.4}/README.md +26 -1
- {structx-0.6.2 → structx-0.6.4}/pyproject.toml +16 -2
- {structx-0.6.2 → structx-0.6.4}/structx/__init__.py +3 -1
- {structx-0.6.2 → structx-0.6.4}/structx/core/input.py +21 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/extractor.py +27 -9
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/input_processor.py +17 -10
- structx-0.6.4/structx/measurement.py +408 -0
- structx-0.6.4/structx/utils/measurement_ocr.py +92 -0
- {structx-0.6.2 → structx-0.6.4/structx.egg-info}/PKG-INFO +33 -2
- {structx-0.6.2 → structx-0.6.4}/structx.egg-info/SOURCES.txt +3 -0
- {structx-0.6.2 → structx-0.6.4}/structx.egg-info/requires.txt +8 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_extractor_async.py +29 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_extractor_flow.py +44 -1
- {structx-0.6.2 → structx-0.6.4}/tests/test_input_and_batch_processors.py +25 -0
- structx-0.6.4/tests/test_measurement.py +464 -0
- {structx-0.6.2 → structx-0.6.4}/LICENSE +0 -0
- {structx-0.6.2 → structx-0.6.4}/setup.cfg +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/core/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/core/config.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/core/exceptions.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/core/models.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/core/type_system.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/core/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/core/llm_core.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/engines/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/engines/extraction_engine.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/generator.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/batch_processor.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/content_analyzer.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/processors/model_operations.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/extraction/result_manager.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/schema.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/__init__.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/file_reader.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/helpers.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/prompts.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/types.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx/utils/usage.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx.egg-info/dependency_links.txt +0 -0
- {structx-0.6.2 → structx-0.6.4}/structx.egg-info/top_level.txt +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_config_and_model_utils.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_content_analyzer.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_document_conversion_integration.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_extraction_engine.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_file_reader.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_live_llm.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_llm_configuration.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_result_manager_and_helpers.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_schema_contract.py +0 -0
- {structx-0.6.2 → structx-0.6.4}/tests/test_type_system_and_planning.py +0 -0
- {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.
|
|
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
|
[](https://structx.aolabs.dev "Documentation")
|
|
67
|
-
[](https://pypi.org/project/structx "Package")
|
|
68
74
|
[](# "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
|
[](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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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:
|
|
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()
|