unstructured-platform-plugins 0.0.0__py3-none-any.whl

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 (34) hide show
  1. test/__init__.py +0 -0
  2. test/assets/__init__.py +0 -0
  3. test/assets/async_typed_dict_response.py +10 -0
  4. test/assets/dataclass_response.py +37 -0
  5. test/assets/empty_input_and_response.py +8 -0
  6. test/assets/hash_function.py +2 -0
  7. test/assets/improper_function.py +2 -0
  8. test/assets/pydantic_response_class_method.py +21 -0
  9. test/assets/simple_hash_class.py +6 -0
  10. test/assets/simple_hash_lambda.py +1 -0
  11. test/assets/simple_hash_value.py +1 -0
  12. test/assets/typed_dict_response.py +17 -0
  13. test/test_schema.py +660 -0
  14. test/test_utils.py +140 -0
  15. unstructured_platform_plugins/__init__.py +0 -0
  16. unstructured_platform_plugins/__version__.py +1 -0
  17. unstructured_platform_plugins/etl_uvicorn/__init__.py +0 -0
  18. unstructured_platform_plugins/etl_uvicorn/api_generator.py +173 -0
  19. unstructured_platform_plugins/etl_uvicorn/main.py +121 -0
  20. unstructured_platform_plugins/etl_uvicorn/utils.py +115 -0
  21. unstructured_platform_plugins/schema/__init__.py +0 -0
  22. unstructured_platform_plugins/schema/json_schema.py +344 -0
  23. unstructured_platform_plugins/schema/model.py +101 -0
  24. unstructured_platform_plugins/schema/usage.py +7 -0
  25. unstructured_platform_plugins/schema/utils.py +31 -0
  26. unstructured_platform_plugins/type_hints.py +106 -0
  27. unstructured_platform_plugins/validate_api.py +110 -0
  28. unstructured_platform_plugins-0.0.0.dist-info/LICENSE +201 -0
  29. unstructured_platform_plugins-0.0.0.dist-info/LICENSE.md +201 -0
  30. unstructured_platform_plugins-0.0.0.dist-info/METADATA +106 -0
  31. unstructured_platform_plugins-0.0.0.dist-info/RECORD +34 -0
  32. unstructured_platform_plugins-0.0.0.dist-info/WHEEL +5 -0
  33. unstructured_platform_plugins-0.0.0.dist-info/entry_points.txt +3 -0
  34. unstructured_platform_plugins-0.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,344 @@
1
+ import inspect
2
+ from dataclasses import MISSING, fields, is_dataclass
3
+ from enum import Enum, EnumMeta
4
+ from inspect import Parameter
5
+ from pathlib import Path
6
+ from types import GenericAlias, NoneType, UnionType
7
+ from typing import Any, Optional, Type, Union, _UnionGenericAlias
8
+
9
+ from pydantic import BaseModel, create_model
10
+ from pydantic.fields import FieldInfo, PydanticUndefined
11
+ from unstructured.ingest.v2.interfaces import FileData
12
+
13
+ from unstructured_platform_plugins.schema.utils import TypedParameter
14
+ from unstructured_platform_plugins.type_hints import get_type_hints
15
+
16
+ # https://json-schema.org/understanding-json-schema/reference/type
17
+ types_map: dict[Type, str] = {
18
+ str: "string",
19
+ bool: "boolean",
20
+ int: "integer",
21
+ float: "number",
22
+ dict: "object",
23
+ list: "array",
24
+ }
25
+
26
+ typed_map_reverse: dict[str, Type] = {v: k for k, v in types_map.items()}
27
+
28
+
29
+ def is_generic_alias(val: Any) -> bool:
30
+ return hasattr(val, "__origin__") and hasattr(val, "__args__")
31
+
32
+
33
+ def is_typed_dict(val: Any) -> bool:
34
+ return (
35
+ inspect.isclass(val) and dict in inspect.getmro(val) and inspect.getmro(val)[0] is not dict
36
+ )
37
+
38
+
39
+ def type_to_json_schema(t: Type, args: Optional[tuple[Any, ...]] = None) -> dict:
40
+ resp = {"type": types_map[t]}
41
+ if t is list and args:
42
+ list_type = args[0]
43
+ resp["items"] = to_json_schema(list_type)
44
+ if t is dict and args:
45
+ key = args[0]
46
+ value = args[1]
47
+ resp["items"] = {
48
+ "key": to_json_schema(key),
49
+ "value": to_json_schema(value),
50
+ }
51
+ return resp
52
+
53
+
54
+ def path_to_json_schema(path: Path) -> dict:
55
+ return {"type": "string", "is_path": True}
56
+
57
+
58
+ def enum_to_json_schema(e: EnumMeta) -> dict:
59
+ values = [i.value for i in e]
60
+ value_types = [type(value) for value in values]
61
+ unique_value_types = list(set(value_types))
62
+ if len(unique_value_types) > 1:
63
+ raise ValueError(
64
+ "enum must have consistent types, found mixes: {}".format(
65
+ ", ".join([e.__name__ for e in unique_value_types])
66
+ )
67
+ )
68
+ value_types = unique_value_types[0]
69
+ type_string = types_map[value_types]
70
+ return {"type": type_string, "enum": values}
71
+
72
+
73
+ def generic_alias_to_json_schema(t: GenericAlias) -> dict:
74
+ origin = t.__origin__
75
+ if origin is Union:
76
+ types = t.__args__
77
+ if len(types) == 1:
78
+ return to_json_schema(types[0])
79
+ else:
80
+ return {"anyOf": [to_json_schema(t) for t in types]}
81
+ return type_to_json_schema(t=origin, args=t.__args__)
82
+
83
+
84
+ def union_type_to_json_schema(t: UnionType) -> dict:
85
+ types = t.__args__
86
+ if len(types) == 1:
87
+ return to_json_schema(types[0])
88
+ else:
89
+ return {"anyOf": [to_json_schema(t) for t in types]}
90
+
91
+
92
+ def dataclass_to_json_schema(class_or_instance: Any) -> dict:
93
+ resp = {"type": "object"}
94
+ is_filedata_instance = isinstance(class_or_instance, FileData)
95
+ is_filedata_class = class_or_instance is FileData
96
+ if is_filedata_instance or is_filedata_class:
97
+ resp["is_file_data"] = True
98
+ fs = fields(class_or_instance)
99
+ if not fs:
100
+ return resp
101
+ properties = {}
102
+ required = []
103
+ type_hints = get_type_hints(class_or_instance)
104
+ for f in fs:
105
+ t = type_hints[f.name]
106
+ f_resp = to_json_schema(t)
107
+ if f.default is not MISSING:
108
+ f_resp["default"] = f.default.value if isinstance(f.default, Enum) else f.default
109
+ else:
110
+ required.append(f.name)
111
+ properties[f.name] = f_resp
112
+ resp["properties"] = properties
113
+ resp["required"] = required
114
+ return resp
115
+
116
+
117
+ def pydantic_base_model_to_json_schema(model: Type[BaseModel]) -> dict:
118
+ resp = {"type": "object"}
119
+ fs: dict[str, FieldInfo] = model.model_fields
120
+ if not fs:
121
+ return resp
122
+ properties = {}
123
+ required = []
124
+ for name, f in fs.items():
125
+ t = f.annotation
126
+ f_resp = to_json_schema(t)
127
+ if f.default != PydanticUndefined:
128
+ f_resp["default"] = f.default
129
+ else:
130
+ required.append(name)
131
+ properties[name] = f_resp
132
+ resp["properties"] = properties
133
+ resp["required"] = required
134
+ return resp
135
+
136
+
137
+ def typed_dict_to_json_schem(typed_dict_class) -> dict:
138
+ resp = {"type": "object"}
139
+ fs = typed_dict_class.__annotations__
140
+ if not fs:
141
+ return resp
142
+ properties = {}
143
+ required = []
144
+ type_hints = get_type_hints(typed_dict_class)
145
+ for name in fs:
146
+ t = type_hints[name]
147
+ f_resp = to_json_schema(t)
148
+ properties[name] = f_resp
149
+ required.append(name)
150
+ resp["properties"] = properties
151
+ resp["required"] = required
152
+ return resp
153
+
154
+
155
+ def parameter_to_json_schema(parameter: Parameter) -> dict:
156
+ annotation = parameter.annotation
157
+ resp = to_json_schema(annotation)
158
+ if parameter.default != Parameter.empty:
159
+ resp["default"] = parameter.default
160
+ return resp
161
+
162
+
163
+ def typed_parameter_to_json_schema(parameter: TypedParameter) -> dict:
164
+ param_type = parameter.param_type
165
+ resp = to_json_schema(param_type)
166
+ if parameter.default != Parameter.empty:
167
+ resp["default"] = parameter.default
168
+ return resp
169
+
170
+
171
+ def to_json_schema(val: Any) -> dict:
172
+ if val in [None, NoneType]:
173
+ return {"type": "null"}
174
+ if val is Any:
175
+ return {}
176
+ if isinstance(val, TypedParameter):
177
+ return typed_parameter_to_json_schema(parameter=val)
178
+ if isinstance(val, Parameter):
179
+ return parameter_to_json_schema(parameter=val)
180
+ if isinstance(val, UnionType):
181
+ return union_type_to_json_schema(t=val)
182
+ if isinstance(val, EnumMeta):
183
+ return enum_to_json_schema(e=val)
184
+ if is_generic_alias(val=val):
185
+ return generic_alias_to_json_schema(t=val)
186
+ if val is Type:
187
+ return type_to_json_schema(t=val)
188
+ if is_dataclass(val):
189
+ return dataclass_to_json_schema(val)
190
+ if val is Path:
191
+ return path_to_json_schema(val)
192
+ if val in types_map:
193
+ return type_to_json_schema(t=val)
194
+ if inspect.isclass(val) and issubclass(val, BaseModel):
195
+ return pydantic_base_model_to_json_schema(model=val)
196
+ if is_typed_dict(val):
197
+ return typed_dict_to_json_schem(val)
198
+ raise ValueError(f"Unsupported type: ({type(val).__name__}) {val}")
199
+
200
+
201
+ def run_input_checks(parameters: list[Parameter]):
202
+ # Any variable positional or keyword args are not allowed (i.e. *args, **kwargs)
203
+ var_positional = [str(p) for p in parameters if p.kind == Parameter.VAR_POSITIONAL]
204
+ if var_positional:
205
+ raise TypeError(
206
+ "function has variable positional arguments, which are not allowed: {}".format(
207
+ ", ".join(var_positional)
208
+ )
209
+ )
210
+ var_keywords = [str(p) for p in parameters if p.kind == Parameter.VAR_KEYWORD]
211
+ if var_keywords:
212
+ raise TypeError(
213
+ "function has variable keyword arguments, which are not allowed: {}".format(
214
+ ", ".join(var_keywords)
215
+ )
216
+ )
217
+
218
+
219
+ def parameters_to_json_schema(parameters: list[Parameter]) -> dict:
220
+ if not parameters:
221
+ return {"type": "null"}
222
+ run_input_checks(parameters=parameters)
223
+ resp = {"type": "object"}
224
+ properties = {}
225
+ required_fields = []
226
+ for p in parameters:
227
+ schema = to_json_schema(val=p)
228
+ properties[p.name] = schema
229
+ if p.default == Parameter.empty:
230
+ required_fields.append(p.name)
231
+ if required_fields:
232
+ resp["required"] = required_fields
233
+ if properties:
234
+ resp["properties"] = properties
235
+ return resp
236
+
237
+
238
+ def run_output_checks(return_annotation: Any):
239
+ if is_generic_alias(val=return_annotation): # noqa: SIM102
240
+ if return_annotation.__origin__ in [Union, list]:
241
+ for arg in return_annotation.__args__:
242
+ run_output_checks(arg)
243
+ return
244
+ if is_typed_dict(return_annotation):
245
+ return
246
+ if is_dataclass(return_annotation):
247
+ return
248
+ try:
249
+ if inspect.isclass(return_annotation) and issubclass(return_annotation, BaseModel):
250
+ return
251
+ except TypeError:
252
+ pass
253
+ if return_annotation is None:
254
+ return
255
+ if return_annotation is NoneType:
256
+ return
257
+ raise ValueError(f"Unsupported response type: {return_annotation}")
258
+
259
+
260
+ def response_to_json_schema(return_annotation: Any) -> dict:
261
+ run_output_checks(return_annotation=return_annotation)
262
+ return to_json_schema(val=return_annotation)
263
+
264
+
265
+ def schema_to_base_model_type(json_type_name, name: str, type_info: dict) -> Type:
266
+ t = typed_map_reverse[json_type_name]
267
+ if t is dict and type_info.get("is_file_data", False):
268
+ return FileData
269
+ if t is str and type_info.get("is_path", False):
270
+ return Path
271
+ if t is dict and "properties" in type_info:
272
+ t = schema_to_base_model(
273
+ schema=type_info,
274
+ name=name,
275
+ )
276
+ if t is dict and "items" in type_info and isinstance(type_info["items"], dict):
277
+ items = type_info["items"]
278
+ if "key" in items and "value" in items:
279
+ key_info = items["key"]
280
+ key_type_name = key_info["type"]
281
+ key_subtype = schema_to_base_model_type(
282
+ json_type_name=key_type_name, name=f"{name}_key", type_info=key_info
283
+ )
284
+ value_info = items["value"]
285
+ if not value_info:
286
+ value_subtype = Any
287
+ else:
288
+ value_type_name = items["value"]["type"]
289
+ value_subtype = schema_to_base_model_type(
290
+ json_type_name=value_type_name, name=f"{name}_value", type_info=value_info
291
+ )
292
+ t = dict[key_subtype, value_subtype]
293
+ if t is list and "items" in type_info and isinstance(type_info["items"], dict):
294
+ items = type_info["items"]
295
+ item_type_name = items["type"]
296
+ subtype = schema_to_base_model_type(
297
+ json_type_name=item_type_name, name=f"{name}_type", type_info=items
298
+ )
299
+ t = list[subtype]
300
+ if "enum" in type_info and isinstance(type_info["enum"], list):
301
+ enum_content = type_info["enum"]
302
+ t = Enum(f"{name}_enum", {v: v for v in enum_content})
303
+ return t
304
+
305
+
306
+ def schema_to_base_model(schema: dict, name: str = "reconstructed_model") -> Type[BaseModel]:
307
+ inputs = {}
308
+ properties = schema["properties"]
309
+
310
+ for k, v in properties.items():
311
+ optional = False
312
+ if "anyOf" in v:
313
+ any_of_entries = v["anyOf"]
314
+ if "null" in [entry["type"] for entry in any_of_entries]:
315
+ optional = True
316
+ any_of_entries = [entry for entry in any_of_entries if entry["type"] != "null"]
317
+ if len(any_of_entries) > 1:
318
+ type_info = [
319
+ schema_to_base_model_type(
320
+ type_info["type"], name=f"{k}_{index}", type_info=type_info
321
+ )
322
+ for index, type_info in enumerate(any_of_entries)
323
+ ]
324
+ # To support python3.10, unpacking not supported
325
+ # t = Union[*type_info]
326
+ t = _UnionGenericAlias(Union, tuple(type_info))
327
+ else:
328
+ entry_info = any_of_entries[0]
329
+ json_type_name = entry_info["type"]
330
+ t = schema_to_base_model_type(
331
+ json_type_name=json_type_name, name=k, type_info=entry_info
332
+ )
333
+ else:
334
+ json_type_name = v["type"]
335
+ t = schema_to_base_model_type(json_type_name=json_type_name, name=k, type_info=v)
336
+ if optional:
337
+ t = Optional[t]
338
+ resp = [t]
339
+ if "default" in v:
340
+ resp.append(v["default"])
341
+ else:
342
+ resp.append(Ellipsis)
343
+ inputs[k] = tuple(resp)
344
+ return create_model(name, **inputs)
@@ -0,0 +1,101 @@
1
+ import inspect
2
+ from types import UnionType
3
+ from typing import Any, Literal, Optional, Union, get_args
4
+
5
+ from pydantic import BaseModel, Field, ValidationError
6
+ from typing_extensions import Annotated
7
+
8
+ AnnotatedType = type(Annotated[str, str])
9
+
10
+
11
+ class StringEntrySchema(BaseModel):
12
+ type: Literal["string"]
13
+ default: Optional[str] = None
14
+
15
+
16
+ class BooleanEntrySchema(BaseModel):
17
+ type: Literal["boolean"]
18
+ default: Optional[bool] = None
19
+
20
+
21
+ class IntegerEntrySchema(BaseModel):
22
+ type: Literal["integer"]
23
+ default: Optional[int] = None
24
+
25
+
26
+ class NumberEntrySchema(BaseModel):
27
+ type: Literal["number"]
28
+ default: Optional[float] = None
29
+
30
+
31
+ class ArrayEntrySchema(BaseModel):
32
+ type: Literal["array"]
33
+ items: Optional["AnyEntry"] = None
34
+
35
+
36
+ class ObjectEntrySchema(BaseModel):
37
+ type: Literal["object"]
38
+ properties: Optional[dict[str, "AnyEntry"]] = None
39
+ required: Optional[list[str]] = None
40
+
41
+
42
+ class NullEntrySchema(BaseModel):
43
+ type: Literal["null"]
44
+
45
+
46
+ class AnyOfEntrySchema(BaseModel):
47
+ anyOf: list["AnyEntry"]
48
+
49
+
50
+ TypedAnyEntry = Union[
51
+ StringEntrySchema,
52
+ BooleanEntrySchema,
53
+ IntegerEntrySchema,
54
+ ArrayEntrySchema,
55
+ ObjectEntrySchema,
56
+ NumberEntrySchema,
57
+ NullEntrySchema,
58
+ ]
59
+ TypedAnyEntryTyping = Annotated[TypedAnyEntry, Field(discriminator="type")]
60
+ AnyEntry = Union[TypedAnyEntryTyping, AnyOfEntrySchema]
61
+
62
+
63
+ def is_valid_input_dict(schema: dict) -> bool:
64
+ # Support both null and object cases
65
+ try:
66
+ NullEntrySchema.model_validate(schema, strict=True)
67
+ return True
68
+ except ValidationError:
69
+ pass
70
+ try:
71
+ ObjectEntrySchema.model_validate(schema, strict=True)
72
+ return True
73
+ except ValidationError:
74
+ pass
75
+ return False
76
+
77
+
78
+ def decompose_union(tp: Any) -> list[BaseModel]:
79
+ args = get_args(tp)
80
+ decomposed_types = []
81
+ for arg in args:
82
+ if inspect.isclass(arg) and issubclass(arg, BaseModel):
83
+ decomposed_types.append(arg)
84
+ elif isinstance(arg, UnionType):
85
+ decomposed_types.extend(decompose_union(tp=arg))
86
+ elif isinstance(arg, AnnotatedType):
87
+ annotated_args = get_args(arg)
88
+ type_arg = annotated_args[0]
89
+ decomposed_types.extend(decompose_union(tp=type_arg))
90
+ return decomposed_types
91
+
92
+
93
+ def is_valid_response_dict(schema: dict) -> bool:
94
+ decomposed_models = decompose_union(tp=AnyEntry)
95
+ for tp in decomposed_models:
96
+ try:
97
+ tp.model_validate(schema, strict=True)
98
+ return True
99
+ except ValidationError:
100
+ pass
101
+ return False
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ # This is a stand in until a supported schema is published external to this repo
5
+ class UsageData(BaseModel):
6
+ value: int
7
+ name: str
@@ -0,0 +1,31 @@
1
+ import inspect
2
+ from inspect import Parameter, _empty
3
+ from typing import Callable
4
+
5
+ from unstructured_platform_plugins.type_hints import get_type_hints
6
+
7
+
8
+ class TypedParameter(Parameter):
9
+ def __init__(self, *args, param_type=_empty, **kwargs):
10
+ super().__init__(*args, **kwargs)
11
+ self.param_type = param_type
12
+
13
+ @classmethod
14
+ def from_parameter(cls, param: Parameter) -> "TypedParameter":
15
+ return cls(
16
+ name=param.name, default=param.default, annotation=param.annotation, kind=param.kind
17
+ )
18
+
19
+
20
+ def get_typed_parameters(fn: Callable) -> list[TypedParameter]:
21
+ type_hints = get_type_hints(fn)
22
+ parameters = list(inspect.signature(fn).parameters.values())
23
+ typed_params = []
24
+ for p in parameters:
25
+ typed_param = TypedParameter.from_parameter(param=p)
26
+ if isinstance(typed_param.annotation, str):
27
+ typed_param.param_type = type_hints[typed_param.name]
28
+ else:
29
+ typed_param.param_type = typed_param.annotation
30
+ typed_params.append(typed_param)
31
+ return typed_params
@@ -0,0 +1,106 @@
1
+ import sys
2
+ from typing import ForwardRef, _allowed_types, _eval_type, _strip_annotations, types
3
+
4
+ """
5
+ python3.10 behavior breaks python3.12 behavior, injecting Optional[] into the type
6
+ if the field has a default value set. Below is the 3.12 behavior, pulling the same logic in when
7
+ 3.10 is used.
8
+ """
9
+
10
+
11
+ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
12
+ """Return type hints for an object.
13
+
14
+ This is often the same as obj.__annotations__, but it handles
15
+ forward references encoded as string literals and recursively replaces all
16
+ 'Annotated[T, ...]' with 'T' (unless 'include_extras=True').
17
+
18
+ The argument may be a module, class, method, or function. The annotations
19
+ are returned as a dictionary. For classes, annotations include also
20
+ inherited members.
21
+
22
+ TypeError is raised if the argument is not of a type that can contain
23
+ annotations, and an empty dictionary is returned if no annotations are
24
+ present.
25
+
26
+ BEWARE -- the behavior of globalns and localns is counterintuitive
27
+ (unless you are familiar with how eval() and exec() work). The
28
+ search order is locals first, then globals.
29
+
30
+ - If no dict arguments are passed, an attempt is made to use the
31
+ globals from obj (or the respective module's globals for classes),
32
+ and these are also used as the locals. If the object does not appear
33
+ to have globals, an empty dictionary is used. For classes, the search
34
+ order is globals first then locals.
35
+
36
+ - If one dict argument is passed, it is used for both globals and
37
+ locals.
38
+
39
+ - If two dict arguments are passed, they specify globals and
40
+ locals, respectively.
41
+ """
42
+ if getattr(obj, "__no_type_check__", None):
43
+ return {}
44
+ # Classes require a special treatment.
45
+ if isinstance(obj, type):
46
+ hints = {}
47
+ for base in reversed(obj.__mro__):
48
+ if globalns is None:
49
+ base_globals = getattr(sys.modules.get(base.__module__, None), "__dict__", {})
50
+ else:
51
+ base_globals = globalns
52
+ ann = base.__dict__.get("__annotations__", {})
53
+ if isinstance(ann, types.GetSetDescriptorType):
54
+ ann = {}
55
+ base_locals = dict(vars(base)) if localns is None else localns
56
+ if localns is None and globalns is None:
57
+ # This is surprising, but required. Before Python 3.10,
58
+ # get_type_hints only evaluated the globalns of
59
+ # a class. To maintain backwards compatibility, we reverse
60
+ # the globalns and localns order so that eval() looks into
61
+ # *base_globals* first rather than *base_locals*.
62
+ # This only affects ForwardRefs.
63
+ base_globals, base_locals = base_locals, base_globals
64
+ for name, value in ann.items():
65
+ if value is None:
66
+ value = type(None)
67
+ if isinstance(value, str):
68
+ value = ForwardRef(value, is_argument=False, is_class=True)
69
+ value = _eval_type(value, base_globals, base_locals)
70
+ hints[name] = value
71
+ return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
72
+
73
+ if globalns is None:
74
+ if isinstance(obj, types.ModuleType):
75
+ globalns = obj.__dict__
76
+ else:
77
+ nsobj = obj
78
+ # Find globalns for the unwrapped object.
79
+ while hasattr(nsobj, "__wrapped__"):
80
+ nsobj = nsobj.__wrapped__
81
+ globalns = getattr(nsobj, "__globals__", {})
82
+ if localns is None:
83
+ localns = globalns
84
+ elif localns is None:
85
+ localns = globalns
86
+ hints = getattr(obj, "__annotations__", None)
87
+ if hints is None:
88
+ # Return empty annotations for something that _could_ have them.
89
+ if isinstance(obj, _allowed_types):
90
+ return {}
91
+ else:
92
+ raise TypeError(f"{obj!r} is not a module, class, method, " "or function.")
93
+ hints = dict(hints)
94
+ for name, value in hints.items():
95
+ if value is None:
96
+ value = type(None)
97
+ if isinstance(value, str):
98
+ # class-level forward refs were handled above, this must be either
99
+ # a module-level annotation or a function argument annotation
100
+ value = ForwardRef(
101
+ value,
102
+ is_argument=not isinstance(obj, types.ModuleType),
103
+ is_class=False,
104
+ )
105
+ hints[name] = _eval_type(value, globalns, localns)
106
+ return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()}
@@ -0,0 +1,110 @@
1
+ from urllib.parse import urljoin
2
+
3
+ import click
4
+ from pydantic import HttpUrl
5
+ from requests import Session
6
+
7
+ from unstructured_platform_plugins.schema.model import is_valid_input_dict
8
+
9
+
10
+ class ApiSession(Session):
11
+ def __init__(self, base_url: HttpUrl = None):
12
+ super().__init__()
13
+ self.base_url = str(base_url)
14
+
15
+ def request(self, method, url, *args, **kwargs):
16
+ joined_url = urljoin(self.base_url, url)
17
+ return super().request(method, joined_url, *args, **kwargs)
18
+
19
+ def get_url(self, url: str) -> str:
20
+ return urljoin(self.base_url, url)
21
+
22
+
23
+ class ValidationError(Exception):
24
+ pass
25
+
26
+
27
+ def check_endpoint_exists(api_session: ApiSession, endpoint: str):
28
+ try:
29
+ api_session.head(endpoint)
30
+ except Exception as e:
31
+ raise ValidationError(
32
+ f"failed to validate that url exists: {api_session.get_url(url=endpoint)}"
33
+ ) from e
34
+
35
+
36
+ def check_schema_response(api_session: ApiSession):
37
+ resp = api_session.get("/schema")
38
+ try:
39
+ resp.raise_for_status()
40
+ except Exception as e:
41
+ raise ValidationError(
42
+ "failed to validate response from schema "
43
+ "endpoint {}: {}".format(api_session.get_url(url="/schema"), e)
44
+ ) from e
45
+ contents = resp.json()
46
+ if not is_valid_input_dict(contents):
47
+ raise ValidationError("schema response don't conform to expected format")
48
+
49
+
50
+ def check_id_response(api_session: ApiSession):
51
+ resp = api_session.get("/id")
52
+ try:
53
+ resp.raise_for_status()
54
+ contents = resp.text
55
+ assert contents.strip() != ""
56
+ except Exception as e:
57
+ raise ValidationError(
58
+ "failed to validate response from id endpoint "
59
+ "{}: {}".format(api_session.get_url(url="/id"), e)
60
+ ) from e
61
+
62
+
63
+ def create_report(api_session: ApiSession) -> dict[str, str]:
64
+ report = {}
65
+ try:
66
+ check_endpoint_exists(api_session=api_session, endpoint="/invoke")
67
+ except ValidationError as e:
68
+ report["/invoke endpoint existence"] = str(e)
69
+
70
+ try:
71
+ check_endpoint_exists(api_session=api_session, endpoint="/schema")
72
+
73
+ try:
74
+ check_schema_response(api_session=api_session)
75
+ except ValidationError as e:
76
+ report["/schema endpoint response"] = str(e)
77
+
78
+ except ValidationError as e:
79
+ report["/schema endpoint existence"] = str(e)
80
+
81
+ try:
82
+ check_endpoint_exists(api_session=api_session, endpoint="/id")
83
+
84
+ try:
85
+ check_id_response(api_session=api_session)
86
+ except ValidationError as e:
87
+ report["/id endpoint response"] = str(e)
88
+ except ValidationError as e:
89
+ report["/id endpoint existence"] = str(e)
90
+
91
+ return report
92
+
93
+
94
+ @click.command()
95
+ @click.option("--api-url", type=HttpUrl, required=True, help="API URL to run validation against")
96
+ def validate_api(api_url: HttpUrl) -> None:
97
+ api_session = ApiSession(base_url=api_url)
98
+ report = create_report(api_session=api_session)
99
+ if report:
100
+ print("Api validation failed:")
101
+ for k, v in report.items():
102
+ print(f"{k}: {v}")
103
+
104
+ exit(1)
105
+ print("Api validation successful")
106
+ exit(0)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ validate_api()