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 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
- # noinspection PyTypeChecker
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libentry
3
- Version: 1.11.12
3
+ Version: 1.12
4
4
  Summary: Entries for experimental utilities.
5
5
  Home-page: https://github.com/XoriieInpottn/libentry
6
6
  Author: xi
@@ -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=IWA9GKyuLCcdpuwZxjKXMOy8MCnEWympMyNjHxRXu-Q,12333
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.11.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
- libentry-1.11.12.dist-info/METADATA,sha256=GLIiUoxHfO_byVbTFbyBMar-S8ywTkGzZR_tQpRBmII,794
19
- libentry-1.11.12.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
20
- libentry-1.11.12.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
21
- libentry-1.11.12.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
22
- libentry-1.11.12.dist-info/RECORD,,
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,,