unstructured-platform-plugins 0.0.45__tar.gz → 0.0.46__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.
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/PKG-INFO +2 -2
- unstructured_platform_plugins-0.0.46/unstructured_platform_plugins/__version__.py +1 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/api_generator.py +89 -30
- unstructured_platform_plugins-0.0.45/unstructured_platform_plugins/__version__.py +0 -1
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/.gitignore +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/LICENSE.md +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/README.md +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/pyproject.toml +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/main.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/otel.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/utils.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/filedata_meta.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/json_schema.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/model.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/usage.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/utils.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/type_hints.py +0 -0
- {unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/validate_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: unstructured_platform_plugins
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.46
|
|
4
4
|
Summary: Wrapper to convert arbitrary code into a uvicorn/fastapi implementation for Unstructured Platform
|
|
5
5
|
License-File: LICENSE.md
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.46" # pragma: no cover
|
|
@@ -3,8 +3,7 @@ import hashlib
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
-
from
|
|
7
|
-
from typing import Any, Callable, Optional, Union
|
|
6
|
+
from typing import Any, Callable, Optional, Union, get_origin
|
|
8
7
|
|
|
9
8
|
from fastapi import FastAPI, HTTPException, status
|
|
10
9
|
from fastapi.responses import StreamingResponse
|
|
@@ -62,21 +61,74 @@ def log_func_and_body(func: Callable, body: Optional[str] = None) -> None:
|
|
|
62
61
|
logger.log(level=logger.level, msg=msg)
|
|
63
62
|
|
|
64
63
|
|
|
64
|
+
def _error_attr(error: BaseException, name: str) -> Any:
|
|
65
|
+
"""Read an attribute off a raised error, treating a raising property as absent.
|
|
66
|
+
|
|
67
|
+
Runs while an exception handler is building the sanitized response; an
|
|
68
|
+
attribute access that itself raises must not replace that response with a
|
|
69
|
+
raw 500.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return getattr(error, name, None)
|
|
73
|
+
except Exception:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _safe_str(value: object) -> str:
|
|
78
|
+
"""str() on a plugin-supplied error can itself raise; never let that escape the handler.
|
|
79
|
+
|
|
80
|
+
An escape replaces the sanitized envelope with a raw HTTP 500, which the
|
|
81
|
+
controller's preflight reads as fail-open — a plugin-reported failure would
|
|
82
|
+
silently become a proceed.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
return str(value)
|
|
86
|
+
except Exception:
|
|
87
|
+
return "<unrenderable error>"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def failure_category_of(error: BaseException) -> Optional[str]:
|
|
91
|
+
"""Return the error's failure_category only when it is a plain string."""
|
|
92
|
+
category = _error_attr(error, "failure_category")
|
|
93
|
+
return category if isinstance(category, str) else None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def status_code_of(error: BaseException) -> int:
|
|
97
|
+
"""Return the error's status_code only when it is an int in the HTTP range, else 500."""
|
|
98
|
+
status_code = _error_attr(error, "status_code")
|
|
99
|
+
if (
|
|
100
|
+
isinstance(status_code, int)
|
|
101
|
+
and not isinstance(status_code, bool)
|
|
102
|
+
and 100 <= status_code <= 599
|
|
103
|
+
):
|
|
104
|
+
return status_code
|
|
105
|
+
return status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
106
|
+
|
|
107
|
+
|
|
65
108
|
async def invoke_func(func: Callable, kwargs: Optional[dict[str, Any]] = None) -> Any:
|
|
66
109
|
kwargs = kwargs or {}
|
|
67
110
|
if inspect.iscoroutinefunction(func):
|
|
68
111
|
return await func(**kwargs)
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
# to_thread copies contextvars into the worker thread, so OpenTelemetry
|
|
113
|
+
# context (and any wide-event adoption inside func) survives the hop;
|
|
114
|
+
# run_in_executor does not.
|
|
115
|
+
return await asyncio.to_thread(func, **kwargs)
|
|
71
116
|
|
|
72
117
|
|
|
73
118
|
def check_precheck_func(precheck_func: Callable):
|
|
74
|
-
|
|
75
|
-
|
|
119
|
+
try:
|
|
120
|
+
# eval_str resolves postponed/string annotations ('list', 'None')
|
|
121
|
+
sig = inspect.signature(precheck_func, eval_str=True)
|
|
122
|
+
except (NameError, TypeError):
|
|
123
|
+
sig = inspect.signature(precheck_func)
|
|
124
|
+
inputs = list(sig.parameters.values())
|
|
76
125
|
outputs = sig.return_annotation
|
|
77
126
|
if len(inputs) == 1:
|
|
78
127
|
i = inputs[0]
|
|
79
|
-
|
|
128
|
+
annotation_is_list = (
|
|
129
|
+
i.annotation is sig.empty or i.annotation is list or get_origin(i.annotation) is list
|
|
130
|
+
)
|
|
131
|
+
if i.name != "usage" or not annotation_is_list:
|
|
80
132
|
raise ValueError("the only input available for precheck is usage which must be a list")
|
|
81
133
|
if outputs not in [None, sig.empty]:
|
|
82
134
|
raise ValueError(f"no output should exist for precheck function, found: {outputs}")
|
|
@@ -135,6 +187,9 @@ def _wrap_in_fastapi(
|
|
|
135
187
|
|
|
136
188
|
logger.debug(f"set static id response to: {plugin_id}")
|
|
137
189
|
|
|
190
|
+
if "usage" not in inspect.signature(func).parameters:
|
|
191
|
+
logger.warning("usage data not an expected parameter, omitting")
|
|
192
|
+
|
|
138
193
|
fastapi_app = FastAPI()
|
|
139
194
|
|
|
140
195
|
response_type = get_output_sig(func)
|
|
@@ -146,6 +201,7 @@ def _wrap_in_fastapi(
|
|
|
146
201
|
file_data: Optional[FileDataType] = None
|
|
147
202
|
filedata_meta: Optional[filedata_meta_model] = None
|
|
148
203
|
status_code_text: Optional[str] = None
|
|
204
|
+
failure_category: Optional[str] = None
|
|
149
205
|
output: Optional[response_type] = None
|
|
150
206
|
message_channels: MessageChannels = Field(default_factory=MessageChannels)
|
|
151
207
|
|
|
@@ -161,13 +217,12 @@ def _wrap_in_fastapi(
|
|
|
161
217
|
filedata_meta = FileDataMeta()
|
|
162
218
|
message_channels = MessageChannels()
|
|
163
219
|
request_dict = kwargs if kwargs else {}
|
|
164
|
-
|
|
220
|
+
params = inspect.signature(func).parameters
|
|
221
|
+
if "usage" in params:
|
|
165
222
|
request_dict["usage"] = usage
|
|
166
|
-
|
|
167
|
-
logger.warning("usage data not an expected parameter, omitting")
|
|
168
|
-
if "message_channels" in inspect.signature(func).parameters:
|
|
223
|
+
if "message_channels" in params:
|
|
169
224
|
request_dict["message_channels"] = message_channels
|
|
170
|
-
if "filedata_meta" in
|
|
225
|
+
if "filedata_meta" in params:
|
|
171
226
|
request_dict["filedata_meta"] = filedata_meta
|
|
172
227
|
try:
|
|
173
228
|
if inspect.isasyncgenfunction(func):
|
|
@@ -189,7 +244,7 @@ def _wrap_in_fastapi(
|
|
|
189
244
|
+ "\n"
|
|
190
245
|
)
|
|
191
246
|
except Exception as e:
|
|
192
|
-
logger.error(f"Failure streaming response: {e}", exc_info=True)
|
|
247
|
+
logger.error(f"Failure streaming response: {_safe_str(e)}", exc_info=True)
|
|
193
248
|
yield (
|
|
194
249
|
InvokeResponse(
|
|
195
250
|
usage=usage,
|
|
@@ -197,9 +252,9 @@ def _wrap_in_fastapi(
|
|
|
197
252
|
filedata_meta=filedata_meta_model.model_validate(
|
|
198
253
|
filedata_meta.model_dump()
|
|
199
254
|
),
|
|
200
|
-
status_code=
|
|
201
|
-
|
|
202
|
-
|
|
255
|
+
status_code=status_code_of(e),
|
|
256
|
+
status_code_text=f"[{type(e).__name__}] {_safe_str(e)}",
|
|
257
|
+
failure_category=failure_category_of(e),
|
|
203
258
|
).model_dump_json()
|
|
204
259
|
+ "\n"
|
|
205
260
|
)
|
|
@@ -217,40 +272,44 @@ def _wrap_in_fastapi(
|
|
|
217
272
|
)
|
|
218
273
|
except HTTPException as exc:
|
|
219
274
|
logger.error(
|
|
220
|
-
f"HTTPException: {exc.detail} (status_code={exc.status_code})",
|
|
275
|
+
f"HTTPException: {_safe_str(exc.detail)} (status_code={exc.status_code})",
|
|
276
|
+
exc_info=True,
|
|
221
277
|
)
|
|
222
278
|
return InvokeResponse(
|
|
223
279
|
usage=usage,
|
|
224
280
|
message_channels=message_channels,
|
|
225
281
|
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
|
|
226
282
|
status_code=exc.status_code,
|
|
227
|
-
status_code_text=
|
|
228
|
-
if isinstance(exc.detail,
|
|
229
|
-
else exc.detail,
|
|
283
|
+
status_code_text=exc.detail
|
|
284
|
+
if isinstance(exc.detail, str)
|
|
285
|
+
else json.dumps(exc.detail, default=_safe_str),
|
|
286
|
+
failure_category=failure_category_of(exc),
|
|
230
287
|
file_data=request_dict.get("file_data", None),
|
|
231
288
|
)
|
|
232
289
|
except UnstructuredIngestError as exc:
|
|
233
290
|
logger.error(
|
|
234
|
-
f"UnstructuredIngestError: {
|
|
291
|
+
f"UnstructuredIngestError: {_safe_str(exc)} "
|
|
292
|
+
f"(status_code={_error_attr(exc, 'status_code')})",
|
|
235
293
|
exc_info=True,
|
|
236
294
|
)
|
|
237
295
|
return InvokeResponse(
|
|
238
296
|
usage=usage,
|
|
239
297
|
message_channels=message_channels,
|
|
240
298
|
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
|
|
241
|
-
status_code=exc
|
|
242
|
-
status_code_text=
|
|
299
|
+
status_code=status_code_of(exc),
|
|
300
|
+
status_code_text=_safe_str(exc),
|
|
301
|
+
failure_category=failure_category_of(exc),
|
|
243
302
|
file_data=request_dict.get("file_data", None),
|
|
244
303
|
)
|
|
245
304
|
except Exception as invoke_error:
|
|
246
|
-
logger.error(f"failed to invoke plugin: {invoke_error}", exc_info=True)
|
|
305
|
+
logger.error(f"failed to invoke plugin: {_safe_str(invoke_error)}", exc_info=True)
|
|
247
306
|
return InvokeResponse(
|
|
248
307
|
usage=usage,
|
|
249
308
|
message_channels=message_channels,
|
|
250
309
|
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
|
|
251
|
-
status_code=
|
|
252
|
-
|
|
253
|
-
|
|
310
|
+
status_code=status_code_of(invoke_error),
|
|
311
|
+
status_code_text=f"[{type(invoke_error).__name__}] {_safe_str(invoke_error)}",
|
|
312
|
+
failure_category=failure_category_of(invoke_error),
|
|
254
313
|
file_data=request_dict.get("file_data", None),
|
|
255
314
|
)
|
|
256
315
|
|
|
@@ -287,9 +346,7 @@ def _wrap_in_fastapi(
|
|
|
287
346
|
|
|
288
347
|
@fastapi_app.post("/invoke", response_model=InvokeResponse)
|
|
289
348
|
async def run_job(request: Optional[input_schema_model] = None) -> ResponseType:
|
|
290
|
-
return await run_job_with_body(
|
|
291
|
-
request if request is not None else input_schema_model()
|
|
292
|
-
)
|
|
349
|
+
return await run_job_with_body(request if request is not None else input_schema_model())
|
|
293
350
|
|
|
294
351
|
elif input_schema_model.model_fields:
|
|
295
352
|
|
|
@@ -318,6 +375,7 @@ def _wrap_in_fastapi(
|
|
|
318
375
|
usage: list[UsageData]
|
|
319
376
|
status_code: int
|
|
320
377
|
status_code_text: Optional[str] = None
|
|
378
|
+
failure_category: Optional[str] = None
|
|
321
379
|
|
|
322
380
|
@fastapi_app.get("/schema")
|
|
323
381
|
async def get_schema() -> SchemaOutputResponse:
|
|
@@ -332,6 +390,7 @@ def _wrap_in_fastapi(
|
|
|
332
390
|
return InvokePrecheckResponse(
|
|
333
391
|
status_code=fn_response.status_code,
|
|
334
392
|
status_code_text=fn_response.status_code_text,
|
|
393
|
+
failure_category=fn_response.failure_category,
|
|
335
394
|
usage=fn_response.usage,
|
|
336
395
|
)
|
|
337
396
|
else:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.45" # pragma: no cover
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unstructured_platform_plugins-0.0.45 → unstructured_platform_plugins-0.0.46}/pyproject.toml
RENAMED
|
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
|