unstructured-platform-plugins 0.0.44__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.44 → 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.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/api_generator.py +125 -40
- unstructured_platform_plugins-0.0.44/unstructured_platform_plugins/__version__.py +0 -1
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/.gitignore +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/LICENSE.md +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/README.md +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/pyproject.toml +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/main.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/otel.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/etl_uvicorn/utils.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/__init__.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/filedata_meta.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/json_schema.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/model.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/usage.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/schema/utils.py +0 -0
- {unstructured_platform_plugins-0.0.44 → unstructured_platform_plugins-0.0.46}/unstructured_platform_plugins/type_hints.py +0 -0
- {unstructured_platform_plugins-0.0.44 → 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,59 +272,87 @@ 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
|
|
|
257
|
-
|
|
316
|
+
async def run_job_with_body(request: BaseModel) -> ResponseType:
|
|
317
|
+
log_func_and_body(func=func, body=request.json())
|
|
318
|
+
# Create dictionary from pydantic model while preserving underlying types
|
|
319
|
+
request_dict = {f: getattr(request, f) for f in request.model_fields}
|
|
320
|
+
# Make sure nested classes get instantiated correctly. `file_data` can legitimately be None
|
|
321
|
+
# -- a plugin may declare it optional, and then an absent or partial body leaves it unset --
|
|
322
|
+
# so convert only a real value. Calling `.model_dump()` on None would raise before `wrap_fn`
|
|
323
|
+
# runs, turning the optional-body contract into a 500.
|
|
324
|
+
file_data = request_dict.get("file_data")
|
|
325
|
+
if file_data is not None:
|
|
326
|
+
request_dict["file_data"] = file_data_from_dict(file_data.model_dump())
|
|
327
|
+
map_inputs(func=func, raw_inputs=request_dict)
|
|
328
|
+
if logger.level == LOG_LEVELS.get("trace", logging.NOTSET):
|
|
329
|
+
logger.log(level=logger.level, msg=f"passing inputs to function: {request_dict}")
|
|
330
|
+
return await wrap_fn(func=func, kwargs=request_dict)
|
|
331
|
+
|
|
332
|
+
# A pydantic body parameter with no default is mandatory even when every field inside the model
|
|
333
|
+
# is optional. So a plugin whose parameters are ALL optional would demand a body that no caller
|
|
334
|
+
# has a reason to populate -- and before it grew those parameters that same plugin accepted no
|
|
335
|
+
# body at all, so adding one flips the HTTP contract while looking backward-compatible. Default
|
|
336
|
+
# the body in that case: an absent body resolves each field to its own default, which is exactly
|
|
337
|
+
# what the function signature already promises.
|
|
338
|
+
#
|
|
339
|
+
# A plugin with at least one required field keeps a mandatory body, so an indexer that needs
|
|
340
|
+
# `file_data` still fails validation rather than silently receiving None.
|
|
341
|
+
body_is_optional = input_schema_model.model_fields and not any(
|
|
342
|
+
field.is_required() for field in input_schema_model.model_fields.values()
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if body_is_optional:
|
|
346
|
+
|
|
347
|
+
@fastapi_app.post("/invoke", response_model=InvokeResponse)
|
|
348
|
+
async def run_job(request: Optional[input_schema_model] = None) -> ResponseType:
|
|
349
|
+
return await run_job_with_body(request if request is not None else input_schema_model())
|
|
350
|
+
|
|
351
|
+
elif input_schema_model.model_fields:
|
|
258
352
|
|
|
259
353
|
@fastapi_app.post("/invoke", response_model=InvokeResponse)
|
|
260
354
|
async def run_job(request: input_schema_model) -> ResponseType:
|
|
261
|
-
|
|
262
|
-
# Create dictionary from pydantic model while preserving underlying types
|
|
263
|
-
request_dict = {f: getattr(request, f) for f in request.model_fields}
|
|
264
|
-
# Make sure nested classes get instantiated correctly
|
|
265
|
-
if "file_data" in request_dict:
|
|
266
|
-
request_dict["file_data"] = file_data_from_dict(
|
|
267
|
-
request_dict["file_data"].model_dump()
|
|
268
|
-
)
|
|
269
|
-
map_inputs(func=func, raw_inputs=request_dict)
|
|
270
|
-
if logger.level == LOG_LEVELS.get("trace", logging.NOTSET):
|
|
271
|
-
logger.log(level=logger.level, msg=f"passing inputs to function: {request_dict}")
|
|
272
|
-
return await wrap_fn(func=func, kwargs=request_dict)
|
|
355
|
+
return await run_job_with_body(request)
|
|
273
356
|
|
|
274
357
|
else:
|
|
275
358
|
|
|
@@ -292,6 +375,7 @@ def _wrap_in_fastapi(
|
|
|
292
375
|
usage: list[UsageData]
|
|
293
376
|
status_code: int
|
|
294
377
|
status_code_text: Optional[str] = None
|
|
378
|
+
failure_category: Optional[str] = None
|
|
295
379
|
|
|
296
380
|
@fastapi_app.get("/schema")
|
|
297
381
|
async def get_schema() -> SchemaOutputResponse:
|
|
@@ -306,6 +390,7 @@ def _wrap_in_fastapi(
|
|
|
306
390
|
return InvokePrecheckResponse(
|
|
307
391
|
status_code=fn_response.status_code,
|
|
308
392
|
status_code_text=fn_response.status_code_text,
|
|
393
|
+
failure_category=fn_response.failure_category,
|
|
309
394
|
usage=fn_response.usage,
|
|
310
395
|
)
|
|
311
396
|
else:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.44" # pragma: no cover
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unstructured_platform_plugins-0.0.44 → 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
|