libentry 1.11.12__py3-none-any.whl → 1.12__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.
- libentry/schema.py +252 -0
- libentry/service/flask.py +2 -13
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/METADATA +1 -1
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/RECORD +8 -7
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/LICENSE +0 -0
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/WHEEL +0 -0
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/top_level.txt +0 -0
- {libentry-1.11.12.dist-info → libentry-1.12.dist-info}/zip-safe +0 -0
libentry/schema.py
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
__author__ = "xi"
|
4
|
+
__all__ = [
|
5
|
+
"SchemaField",
|
6
|
+
"Schema",
|
7
|
+
"ParseContext",
|
8
|
+
"parse_type",
|
9
|
+
"QueryAPIOutput",
|
10
|
+
"query_api",
|
11
|
+
]
|
12
|
+
|
13
|
+
import enum
|
14
|
+
from dataclasses import dataclass
|
15
|
+
from inspect import signature
|
16
|
+
from typing import Any, Iterable, List, Literal, Mapping, MutableMapping, NoReturn, Optional, Sequence, Union, get_args, \
|
17
|
+
get_origin
|
18
|
+
|
19
|
+
from pydantic import BaseModel, Field, create_model
|
20
|
+
from pydantic_core import PydanticUndefined
|
21
|
+
|
22
|
+
|
23
|
+
class SchemaField(BaseModel):
|
24
|
+
name: str = Field()
|
25
|
+
type: Union[str, List[str]] = Field("Any")
|
26
|
+
default: Any = Field()
|
27
|
+
is_required: bool = Field(True)
|
28
|
+
title: str = Field()
|
29
|
+
description: Optional[str] = Field(None)
|
30
|
+
|
31
|
+
|
32
|
+
class Schema(BaseModel):
|
33
|
+
name: str = Field()
|
34
|
+
fields: List[SchemaField] = Field(default_factory=list)
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class ParseContext:
|
39
|
+
annotation: Any
|
40
|
+
schemas: MutableMapping[str, Schema]
|
41
|
+
origin: Any
|
42
|
+
|
43
|
+
|
44
|
+
_TYPE_PARSERS = []
|
45
|
+
_GENERIC_PARSERS = []
|
46
|
+
|
47
|
+
|
48
|
+
def parse_type(annotation, context: MutableMapping[str, Schema]) -> Union[str, List[str]]:
|
49
|
+
origin = get_origin(annotation)
|
50
|
+
if origin is None:
|
51
|
+
origin = annotation
|
52
|
+
pc = ParseContext(
|
53
|
+
annotation=annotation,
|
54
|
+
schemas=context,
|
55
|
+
origin=origin
|
56
|
+
)
|
57
|
+
|
58
|
+
parser_list = _TYPE_PARSERS if isinstance(origin, type) else _GENERIC_PARSERS
|
59
|
+
for parser in parser_list:
|
60
|
+
output = parser(pc)
|
61
|
+
if output is not None:
|
62
|
+
return output
|
63
|
+
raise TypeError(f"Unsupported type \"{origin}\".")
|
64
|
+
|
65
|
+
|
66
|
+
def type_parser(fn):
|
67
|
+
_TYPE_PARSERS.append(fn)
|
68
|
+
return fn
|
69
|
+
|
70
|
+
|
71
|
+
@type_parser
|
72
|
+
def _parse_basic_types(context: ParseContext):
|
73
|
+
if context.origin in {int, float, str, bool}:
|
74
|
+
return context.origin.__name__
|
75
|
+
|
76
|
+
|
77
|
+
@type_parser
|
78
|
+
def _parse_dict(context: ParseContext):
|
79
|
+
if issubclass(context.origin, Mapping):
|
80
|
+
dict_args = get_args(context.annotation)
|
81
|
+
if dict_args:
|
82
|
+
key_type = dict_args[0]
|
83
|
+
if key_type is not str:
|
84
|
+
raise TypeError("Only \"str\" can be used as the type of dict keys.")
|
85
|
+
key_type = "str"
|
86
|
+
value_type = parse_type(dict_args[1], context.schemas)
|
87
|
+
if isinstance(value_type, list):
|
88
|
+
raise TypeError("\"Union\" cannot be used as the type of list elements.")
|
89
|
+
return f"Dict[{key_type},{value_type}]"
|
90
|
+
else:
|
91
|
+
return "Dict"
|
92
|
+
|
93
|
+
|
94
|
+
@type_parser
|
95
|
+
def _parse_list(context: ParseContext):
|
96
|
+
if issubclass(context.origin, Sequence):
|
97
|
+
list_args = get_args(context.annotation)
|
98
|
+
if list_args:
|
99
|
+
if len(list_args) > 1:
|
100
|
+
raise TypeError("Only ONE type can be used as the type of list elements.")
|
101
|
+
elem_type = parse_type(list_args[0], context.schemas)
|
102
|
+
if isinstance(elem_type, list):
|
103
|
+
raise TypeError("\"Union\" cannot be used as the type of list elements.")
|
104
|
+
return f"List[{elem_type}]"
|
105
|
+
else:
|
106
|
+
return "List"
|
107
|
+
|
108
|
+
|
109
|
+
@type_parser
|
110
|
+
def _parse_enum(context: ParseContext):
|
111
|
+
if issubclass(context.origin, enum.Enum):
|
112
|
+
return f"Enum[{','.join(e.name for e in context.origin)}]"
|
113
|
+
|
114
|
+
|
115
|
+
@type_parser
|
116
|
+
def _parse_base_model(context: ParseContext):
|
117
|
+
origin = context.origin
|
118
|
+
if issubclass(origin, BaseModel):
|
119
|
+
_module = origin.__module__
|
120
|
+
_name = origin.__name__
|
121
|
+
model_name = _name if (_module is None) else f"{_module}.{_name}"
|
122
|
+
|
123
|
+
is_new_model = model_name not in context.schemas
|
124
|
+
is_not_base_class = origin is not BaseModel
|
125
|
+
if is_new_model and is_not_base_class:
|
126
|
+
schema = Schema(name=model_name)
|
127
|
+
fields = origin.model_fields
|
128
|
+
assert isinstance(fields, Mapping)
|
129
|
+
for name, field in fields.items():
|
130
|
+
|
131
|
+
try:
|
132
|
+
schema.fields.append(SchemaField(
|
133
|
+
name=name,
|
134
|
+
type=parse_type(field.annotation, context.schemas),
|
135
|
+
default=field.default if field.default is not PydanticUndefined else None,
|
136
|
+
is_required=field.is_required(),
|
137
|
+
title="".join(word.capitalize() for word in name.split("_")),
|
138
|
+
description=field.description
|
139
|
+
))
|
140
|
+
except TypeError as e:
|
141
|
+
raise TypeError(f"{name}: {str(e)}")
|
142
|
+
context.schemas[model_name] = schema
|
143
|
+
|
144
|
+
return model_name
|
145
|
+
|
146
|
+
|
147
|
+
@type_parser
|
148
|
+
def _parse_iterable(context: ParseContext):
|
149
|
+
if context.origin.__name__ in {"Iterable", "Generator", "range"} and issubclass(context.origin, Iterable):
|
150
|
+
iter_args = get_args(context.annotation)
|
151
|
+
if len(iter_args) != 1:
|
152
|
+
raise TypeError("Only ONE type can be used as the type of iterable elements.")
|
153
|
+
iter_type = parse_type(iter_args[0], context.schemas)
|
154
|
+
if isinstance(iter_type, list):
|
155
|
+
raise TypeError("\"Union\" cannot be used as the type of iterable elements.")
|
156
|
+
return f"Iter[{iter_type}]"
|
157
|
+
|
158
|
+
|
159
|
+
@type_parser
|
160
|
+
def _parse_none_type(context: ParseContext):
|
161
|
+
origin = context.origin
|
162
|
+
if origin.__module__ == "builtins" and origin.__name__ == "NoneType":
|
163
|
+
return "NoneType"
|
164
|
+
|
165
|
+
|
166
|
+
@type_parser
|
167
|
+
def _parse_ndarray(context: ParseContext):
|
168
|
+
origin = context.origin
|
169
|
+
if origin.__module__ == "numpy" and origin.__name__ == "ndarray":
|
170
|
+
return "numpy.ndarray"
|
171
|
+
|
172
|
+
|
173
|
+
def generic_parser(fn):
|
174
|
+
_GENERIC_PARSERS.append(fn)
|
175
|
+
return fn
|
176
|
+
|
177
|
+
|
178
|
+
@generic_parser
|
179
|
+
def _parse_any(context: ParseContext):
|
180
|
+
if context.origin is Any:
|
181
|
+
return "Any"
|
182
|
+
|
183
|
+
|
184
|
+
@generic_parser
|
185
|
+
def _parse_union(context: ParseContext):
|
186
|
+
if context.origin is Union:
|
187
|
+
return [
|
188
|
+
parse_type(arg, context.schemas)
|
189
|
+
for arg in get_args(context.annotation)
|
190
|
+
]
|
191
|
+
|
192
|
+
|
193
|
+
@generic_parser
|
194
|
+
def _parse_literal(context: ParseContext):
|
195
|
+
if context.origin is Literal:
|
196
|
+
enum_args = get_args(context.annotation)
|
197
|
+
return f"Enum[{','.join(map(str, enum_args))}]"
|
198
|
+
|
199
|
+
|
200
|
+
class QueryAPIOutput(BaseModel):
|
201
|
+
input_schema: str
|
202
|
+
output_schema: str
|
203
|
+
context: Mapping[str, Schema]
|
204
|
+
bundled_input: bool
|
205
|
+
|
206
|
+
|
207
|
+
def query_api(fn) -> QueryAPIOutput:
|
208
|
+
sig = signature(fn)
|
209
|
+
|
210
|
+
fields = {}
|
211
|
+
for name, param in sig.parameters.items():
|
212
|
+
if name in ["self", "cls"]:
|
213
|
+
continue
|
214
|
+
|
215
|
+
annotation = param.annotation
|
216
|
+
if annotation is sig.empty:
|
217
|
+
annotation = Any
|
218
|
+
|
219
|
+
default = param.default
|
220
|
+
field = Field() if default is sig.empty else Field(default)
|
221
|
+
fields[name] = (annotation, field)
|
222
|
+
|
223
|
+
args_model = None
|
224
|
+
if len(fields) == 1:
|
225
|
+
for annotation, _ in fields.values():
|
226
|
+
origin = get_origin(annotation)
|
227
|
+
if origin is None:
|
228
|
+
origin = annotation
|
229
|
+
if isinstance(origin, type) and issubclass(origin, BaseModel):
|
230
|
+
args_model = origin
|
231
|
+
bundle = args_model is None
|
232
|
+
if bundle:
|
233
|
+
name = "".join(word.capitalize() for word in fn.__name__.split("_"))
|
234
|
+
args_model = create_model(f"{name}Request*", **fields)
|
235
|
+
|
236
|
+
context = {}
|
237
|
+
input_schema = parse_type(args_model, context)
|
238
|
+
output_schema = None
|
239
|
+
return_annotation = sig.return_annotation
|
240
|
+
if return_annotation is not None and return_annotation is not NoReturn:
|
241
|
+
if return_annotation is sig.empty:
|
242
|
+
return_annotation = Any
|
243
|
+
output_schema = parse_type(return_annotation, context)
|
244
|
+
if isinstance(output_schema, list):
|
245
|
+
output_schema = output_schema[0]
|
246
|
+
|
247
|
+
return QueryAPIOutput(
|
248
|
+
input_schema=input_schema,
|
249
|
+
output_schema=output_schema,
|
250
|
+
context=context,
|
251
|
+
bundled_input=bundle,
|
252
|
+
)
|
libentry/service/flask.py
CHANGED
@@ -15,11 +15,11 @@ from typing import Any, Callable, Iterable, Optional, Type, Union
|
|
15
15
|
from flask import Flask, request
|
16
16
|
from gunicorn.app.base import BaseApplication
|
17
17
|
from pydantic import BaseModel, Field, create_model
|
18
|
-
from pydantic.json_schema import GenerateJsonSchema
|
19
18
|
|
20
19
|
from libentry import api, json
|
21
20
|
from libentry.api import APIInfo, list_api_info
|
22
21
|
from libentry.logging import logger
|
22
|
+
from libentry.schema import query_api
|
23
23
|
|
24
24
|
|
25
25
|
class JSONDumper:
|
@@ -222,14 +222,6 @@ class FlaskWrapper:
|
|
222
222
|
)
|
223
223
|
|
224
224
|
|
225
|
-
class CustomGenerateJsonSchema(GenerateJsonSchema):
|
226
|
-
|
227
|
-
def handle_invalid_for_json_schema(self, schema, error_info: str):
|
228
|
-
cls = schema.get("cls")
|
229
|
-
cls_name = f"{cls.__module__}.{cls.__name__}" if cls is not None else "UNKNOWN"
|
230
|
-
return {"type": cls_name}
|
231
|
-
|
232
|
-
|
233
225
|
class FlaskServer(Flask):
|
234
226
|
|
235
227
|
def __init__(self, service):
|
@@ -286,10 +278,7 @@ class FlaskServer(Flask):
|
|
286
278
|
|
287
279
|
for fn, api_info in self.api_info_list:
|
288
280
|
if api_info.path == "/" + name:
|
289
|
-
|
290
|
-
dynamic_model = create_model_from_signature(fn)
|
291
|
-
schema = dynamic_model.model_json_schema(schema_generator=CustomGenerateJsonSchema)
|
292
|
-
return schema
|
281
|
+
return query_api(fn).model_dump()
|
293
282
|
|
294
283
|
return f"No API named \"{name}\""
|
295
284
|
|
@@ -6,17 +6,18 @@ libentry/executor.py,sha256=cTV0WxJi0nU1TP-cOwmeodN8DD6L1691M2HIQsJtGrU,6582
|
|
6
6
|
libentry/experiment.py,sha256=ejgAHDXWIe9x4haUzIFuz1WasLY0_aD1z_vyEVGjTu8,4922
|
7
7
|
libentry/json.py,sha256=1-Kv5ZRb5dBrOTU84n6sZtYZV3xE-O6wEt_--ynbSaU,1209
|
8
8
|
libentry/logging.py,sha256=IiYoCUzm8XTK1fduA-NA0FI2Qz_m81NEPV3d3tEfgdI,1349
|
9
|
+
libentry/schema.py,sha256=ZlQnqUrL0YQ5koZnka7RNfOSzaH5llQ5XhQI6ig02xk,7629
|
9
10
|
libentry/server.py,sha256=gYPoZXd0umlDYZf-6ZV0_vJadg3YQvnLDc6JFDJh9jc,1503
|
10
11
|
libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
11
12
|
libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
|
12
|
-
libentry/service/flask.py,sha256=
|
13
|
+
libentry/service/flask.py,sha256=3fTkysQkCZ-flVH7FKWnOiMNmdkGb7kJGmmz1PZqkPE,11850
|
13
14
|
libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
|
14
15
|
libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
|
15
16
|
libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
|
16
17
|
libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
|
17
|
-
libentry-1.
|
18
|
-
libentry-1.
|
19
|
-
libentry-1.
|
20
|
-
libentry-1.
|
21
|
-
libentry-1.
|
22
|
-
libentry-1.
|
18
|
+
libentry-1.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
19
|
+
libentry-1.12.dist-info/METADATA,sha256=42N3OAb8OEdO_DsYaIQgYzC9fvZGTsTzuqj7G-YY69k,791
|
20
|
+
libentry-1.12.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
21
|
+
libentry-1.12.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
|
22
|
+
libentry-1.12.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
23
|
+
libentry-1.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|