unstructured-platform-plugins 0.0.42__tar.gz → 0.0.45__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 (21) hide show
  1. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/PKG-INFO +5 -1
  2. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/README.md +4 -0
  3. unstructured_platform_plugins-0.0.45/unstructured_platform_plugins/__version__.py +1 -0
  4. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/etl_uvicorn/api_generator.py +44 -13
  5. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/etl_uvicorn/main.py +21 -0
  6. unstructured_platform_plugins-0.0.42/unstructured_platform_plugins/__version__.py +0 -1
  7. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/.gitignore +0 -0
  8. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/LICENSE.md +0 -0
  9. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/pyproject.toml +0 -0
  10. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/__init__.py +0 -0
  11. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/etl_uvicorn/__init__.py +0 -0
  12. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/etl_uvicorn/otel.py +0 -0
  13. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/etl_uvicorn/utils.py +0 -0
  14. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/__init__.py +0 -0
  15. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/filedata_meta.py +0 -0
  16. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/json_schema.py +0 -0
  17. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/model.py +0 -0
  18. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/usage.py +0 -0
  19. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/schema/utils.py +0 -0
  20. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/type_hints.py +0 -0
  21. {unstructured_platform_plugins-0.0.42 → unstructured_platform_plugins-0.0.45}/unstructured_platform_plugins/validate_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unstructured_platform_plugins
3
- Version: 0.0.42
3
+ Version: 0.0.45
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
@@ -29,6 +29,10 @@ Description-Content-Type: text/markdown
29
29
  # Unstructured Platform Plugins
30
30
  ![CI](https://github.com/Unstructured-IO/unstructured-platform-plugins/actions/workflows/ci.yml/badge.svg?branch=main)
31
31
 
32
+ > **Deprecation notice:** `wrap_in_fastapi` (and the `etl-uvicorn` CLI it backs)
33
+ > is deprecated. New plugins should build a FastAPI app directly with explicit
34
+ > handlers for the plugin contract routes (`/invoke`, `/schema`, `/id`, etc.).
35
+
32
36
  Information about how to build custom plugins to integrate with Unstructured Platform.
33
37
 
34
38
  ## Plugin Development
@@ -1,6 +1,10 @@
1
1
  # Unstructured Platform Plugins
2
2
  ![CI](https://github.com/Unstructured-IO/unstructured-platform-plugins/actions/workflows/ci.yml/badge.svg?branch=main)
3
3
 
4
+ > **Deprecation notice:** `wrap_in_fastapi` (and the `etl-uvicorn` CLI it backs)
5
+ > is deprecated. New plugins should build a FastAPI app directly with explicit
6
+ > handlers for the plugin contract routes (`/invoke`, `/schema`, `/id`, etc.).
7
+
4
8
  Information about how to build custom plugins to integrate with Unstructured Platform.
5
9
 
6
10
  ## Plugin Development
@@ -0,0 +1 @@
1
+ __version__ = "0.0.45" # pragma: no cover
@@ -11,6 +11,7 @@ from fastapi.responses import StreamingResponse
11
11
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
12
12
  from pydantic import BaseModel, Field, create_model
13
13
  from starlette.responses import RedirectResponse
14
+ from typing_extensions import deprecated
14
15
  from unstructured_ingest.data_types.file_data import BatchFileData, FileData, file_data_from_dict
15
16
  from unstructured_ingest.error import UnstructuredIngestError
16
17
  from uvicorn.config import LOG_LEVELS
@@ -108,6 +109,10 @@ def update_filedata_model(new_type) -> BaseModel:
108
109
  return new_filedata_model
109
110
 
110
111
 
112
+ @deprecated(
113
+ "wrap_in_fastapi is deprecated; build a FastAPI app directly with "
114
+ "explicit handlers for the plugin contract routes."
115
+ )
111
116
  def wrap_in_fastapi(
112
117
  func: Callable,
113
118
  plugin_id: str,
@@ -249,22 +254,48 @@ def _wrap_in_fastapi(
249
254
  file_data=request_dict.get("file_data", None),
250
255
  )
251
256
 
252
- if input_schema_model.model_fields:
257
+ async def run_job_with_body(request: BaseModel) -> ResponseType:
258
+ log_func_and_body(func=func, body=request.json())
259
+ # Create dictionary from pydantic model while preserving underlying types
260
+ request_dict = {f: getattr(request, f) for f in request.model_fields}
261
+ # Make sure nested classes get instantiated correctly. `file_data` can legitimately be None
262
+ # -- a plugin may declare it optional, and then an absent or partial body leaves it unset --
263
+ # so convert only a real value. Calling `.model_dump()` on None would raise before `wrap_fn`
264
+ # runs, turning the optional-body contract into a 500.
265
+ file_data = request_dict.get("file_data")
266
+ if file_data is not None:
267
+ request_dict["file_data"] = file_data_from_dict(file_data.model_dump())
268
+ map_inputs(func=func, raw_inputs=request_dict)
269
+ if logger.level == LOG_LEVELS.get("trace", logging.NOTSET):
270
+ logger.log(level=logger.level, msg=f"passing inputs to function: {request_dict}")
271
+ return await wrap_fn(func=func, kwargs=request_dict)
272
+
273
+ # A pydantic body parameter with no default is mandatory even when every field inside the model
274
+ # is optional. So a plugin whose parameters are ALL optional would demand a body that no caller
275
+ # has a reason to populate -- and before it grew those parameters that same plugin accepted no
276
+ # body at all, so adding one flips the HTTP contract while looking backward-compatible. Default
277
+ # the body in that case: an absent body resolves each field to its own default, which is exactly
278
+ # what the function signature already promises.
279
+ #
280
+ # A plugin with at least one required field keeps a mandatory body, so an indexer that needs
281
+ # `file_data` still fails validation rather than silently receiving None.
282
+ body_is_optional = input_schema_model.model_fields and not any(
283
+ field.is_required() for field in input_schema_model.model_fields.values()
284
+ )
285
+
286
+ if body_is_optional:
287
+
288
+ @fastapi_app.post("/invoke", response_model=InvokeResponse)
289
+ 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
+ )
293
+
294
+ elif input_schema_model.model_fields:
253
295
 
254
296
  @fastapi_app.post("/invoke", response_model=InvokeResponse)
255
297
  async def run_job(request: input_schema_model) -> ResponseType:
256
- log_func_and_body(func=func, body=request.json())
257
- # Create dictionary from pydantic model while preserving underlying types
258
- request_dict = {f: getattr(request, f) for f in request.model_fields}
259
- # Make sure nested classes get instantiated correctly
260
- if "file_data" in request_dict:
261
- request_dict["file_data"] = file_data_from_dict(
262
- request_dict["file_data"].model_dump()
263
- )
264
- map_inputs(func=func, raw_inputs=request_dict)
265
- if logger.level == LOG_LEVELS.get("trace", logging.NOTSET):
266
- logger.log(level=logger.level, msg=f"passing inputs to function: {request_dict}")
267
- return await wrap_fn(func=func, kwargs=request_dict)
298
+ return await run_job_with_body(request)
268
299
 
269
300
  else:
270
301
 
@@ -1,13 +1,34 @@
1
+ import asyncio
2
+ import signal
3
+ import threading
1
4
  from dataclasses import dataclass, field
2
5
  from typing import IO, Any, Optional
3
6
 
4
7
  import click
8
+ import uvicorn
5
9
  from uvicorn.config import LOGGING_CONFIG, Config, RawConfigParser
6
10
  from uvicorn.main import main, run
7
11
 
8
12
  from unstructured_platform_plugins.etl_uvicorn.api_generator import generate_fast_api
9
13
 
10
14
 
15
+ def _install_signal_handlers_ignoring_sigterm(self: uvicorn.Server) -> None:
16
+ # uvicorn's default load-sheds 504 on SIGTERM, which races with controllers
17
+ # trying to drain in-flight work during pod shutdown. Plugin webservers are
18
+ # expected to outlive their controller container so SIGKILL — not SIGTERM —
19
+ # ends the process. SIGINT is preserved so local Ctrl-C still works.
20
+ if threading.current_thread() is not threading.main_thread():
21
+ return
22
+ try:
23
+ loop = asyncio.get_event_loop()
24
+ loop.add_signal_handler(signal.SIGINT, self.handle_exit, signal.SIGINT, None)
25
+ except NotImplementedError:
26
+ signal.signal(signal.SIGINT, self.handle_exit)
27
+
28
+
29
+ uvicorn.Server.install_signal_handlers = _install_signal_handlers_ignoring_sigterm
30
+
31
+
11
32
  @dataclass
12
33
  class CustomConfig:
13
34
  log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = field(
@@ -1 +0,0 @@
1
- __version__ = "0.0.42" # pragma: no cover