libentry 1.23.3__py3-none-any.whl → 1.24__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/mcp/api.py CHANGED
@@ -19,6 +19,12 @@ from pydantic import BaseModel, ConfigDict
19
19
 
20
20
  API_INFO_FIELD = "__api_info__"
21
21
 
22
+ TAG_SUBROUTINE = "subroutine"
23
+ TAG_JSONRPC = "jsonrpc"
24
+ TAG_ENDPOINT = "endpoint"
25
+ TAG_TOOL = "tool"
26
+ TAG_RESOURCE = "resource"
27
+
22
28
 
23
29
  class APIInfo(BaseModel):
24
30
  path: str
@@ -69,7 +75,7 @@ def tool(path=None, name=None, description=None, **kwargs):
69
75
  methods=["POST"],
70
76
  name=name,
71
77
  description=description,
72
- tag="tool",
78
+ tag=TAG_TOOL,
73
79
  **kwargs
74
80
  )
75
81
 
@@ -80,7 +86,7 @@ def resource(uri, name=None, description=None, mime_type=None, size=None, **kwar
80
86
  methods=["POST"],
81
87
  name=name,
82
88
  description=description,
83
- tag="resource",
89
+ tag=TAG_RESOURCE,
84
90
  mimeType=mime_type,
85
91
  size=size,
86
92
  uri=uri,
libentry/mcp/service.py CHANGED
@@ -57,11 +57,24 @@ class SubroutineAdapter:
57
57
 
58
58
  def __call__(
59
59
  self,
60
- request: Dict[str, Any]
60
+ request: Any,
61
+ args: Optional[Dict[str, str]] = None
61
62
  ) -> Union[SubroutineResponse, Iterable[SubroutineResponse]]:
62
63
  if isinstance(request, BaseModel):
63
64
  request = request.model_dump()
64
65
 
66
+ if not isinstance(request, Dict):
67
+ raise ValueError(
68
+ f"Subroutine only accepts JSON object as input. "
69
+ f"Expect \"dict\", got \"{type(request)}\"."
70
+ )
71
+
72
+ if args is not None:
73
+ conflicts = args.keys() & request.keys()
74
+ if len(conflicts) > 0:
75
+ raise ValueError(f"Duplicated fields: \"{conflicts}\".")
76
+ request = {**args, **request}
77
+
65
78
  try:
66
79
  input_model = self.api_signature.input_model
67
80
  if input_model is not None:
@@ -117,16 +130,29 @@ class JSONRPCAdapter:
117
130
 
118
131
  def __call__(
119
132
  self,
120
- request: Union[JSONRPCRequest, JSONRPCNotification, Dict[str, Any]]
133
+ request: Union[JSONRPCRequest, JSONRPCNotification, Dict[str, Any]],
134
+ args: Optional[Dict[str, str]] = None
121
135
  ) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
122
136
  if isinstance(request, Dict):
137
+ if args is not None:
138
+ conflicts = args.keys() & request.keys()
139
+ if len(conflicts) > 0:
140
+ raise ValueError(f"Duplicated fields: \"{conflicts}\".")
141
+ request = {**args, **request}
123
142
  request = self.type_adapter.validate_python(request)
124
143
 
144
+ if isinstance(request, JSONRPCRequest):
145
+ fn = self._apply_request
146
+ elif isinstance(request, JSONRPCNotification):
147
+ fn = self._apply_notification
148
+ else:
149
+ raise ValueError(
150
+ f"JSONRPC only accepts JSONRPCRequest, JSONRPCNotification as input. "
151
+ f"Expect \"JSONRPCRequest\", \"JSONRPCNotification\" or \"dict\", got \"{type(request)}\"."
152
+ )
153
+
125
154
  try:
126
- if isinstance(request, JSONRPCRequest):
127
- return self._apply_request(request)
128
- else:
129
- return self._apply_notification(request)
155
+ return fn(request)
130
156
  except SystemExit as e:
131
157
  raise e
132
158
  except KeyboardInterrupt as e:
@@ -233,44 +259,38 @@ class FlaskHandler:
233
259
 
234
260
  self.subroutine_adapter = SubroutineAdapter(fn, self.api_signature)
235
261
  self.jsonrpc_adapter = JSONRPCAdapter(fn, self.api_signature)
236
- self.default_adapter = self.subroutine_adapter
237
262
 
263
+ adapter_mapping = {
264
+ api.TAG_ENDPOINT: self.fn,
265
+ "free": self.fn,
266
+ "schema_free": self.fn,
267
+ "schema-free": self.fn,
268
+ api.TAG_JSONRPC: self.jsonrpc_adapter,
269
+ "rpc": self.jsonrpc_adapter,
270
+ "mcp": self.jsonrpc_adapter,
271
+ }
238
272
  tag = self.api_info.tag if self.api_info else None
239
- if tag is not None:
240
- tag = tag.lower()
241
- if tag in {"free", "schema_free", "schema-free"}:
242
- self.default_adapter = self.fn
243
- elif tag in {"jsonrpc", "rpc"}:
244
- self.default_adapter = self.jsonrpc_adapter
245
-
246
- # todo: debug info
247
- print(f"{api_info.path}: {type(self.default_adapter)}\t{self.api_signature.input_model}")
273
+ self.default_adapter = adapter_mapping.get(tag, self.subroutine_adapter)
248
274
 
249
275
  def __call__(self):
250
276
  args = flask_request.args
251
277
  data = flask_request.data
252
278
  content_type = flask_request.content_type
253
279
 
254
- json_from_url = {**args}
280
+ args = {**args}
255
281
  if data:
256
282
  if (not content_type) or content_type == MIME.json.value:
257
- json_from_data = json.loads(data)
283
+ raw_request = json.loads(data)
258
284
  else:
259
285
  return self.app.error(f"Unsupported Content-Type: \"{content_type}\".")
260
286
  else:
261
- json_from_data = {}
262
-
263
- conflicts = json_from_url.keys() & json_from_data.keys()
264
- if len(conflicts) > 0:
265
- return self.app.error(f"Duplicated fields: \"{conflicts}\".")
266
-
267
- input_json = {**json_from_url, **json_from_data}
287
+ raw_request = {}
268
288
 
269
289
  ################################################################################
270
290
  # Call method as MCP
271
291
  ################################################################################
272
292
  try:
273
- mcp_response = self.default_adapter(input_json)
293
+ mcp_response = self.default_adapter(raw_request, args)
274
294
  except Exception as e:
275
295
  error = json.dumps(SubroutineError.from_exception(e))
276
296
  return self.app.error(error, mimetype=MIME.json.value)
@@ -369,8 +389,12 @@ class SSEService:
369
389
  self.sse_dict = {}
370
390
 
371
391
  # noinspection PyUnusedLocal
372
- @api.get("/sse", tag="schema_free")
373
- def sse(self, raw_request: Dict[str, Any]) -> Iterable[SSE]:
392
+ @api.get("/sse", tag=api.TAG_ENDPOINT)
393
+ def sse(
394
+ self,
395
+ raw_request: Any,
396
+ args: Optional[Dict[str, str]] = None
397
+ ) -> Iterable[SSE]:
374
398
  session_id = str(uuid.uuid4())
375
399
  queue = Queue(8)
376
400
  with self.lock:
@@ -395,8 +419,24 @@ class SSEService:
395
419
 
396
420
  return _stream()
397
421
 
398
- @api.route("/sse/message", tag="schema_free")
399
- def sse_message(self, raw_request: Dict[str, Any]) -> None:
422
+ @api.route("/sse/message", tag=api.TAG_ENDPOINT)
423
+ def sse_message(
424
+ self,
425
+ raw_request: Any,
426
+ args: Optional[Dict[str, str]] = None
427
+ ) -> None:
428
+ if not isinstance(raw_request, Dict):
429
+ raise ValueError(
430
+ f"/message only accepts JSON object as input. "
431
+ f"Expect \"dict\", got \"{type(raw_request)}\"."
432
+ )
433
+
434
+ if args is not None:
435
+ conflicts = args.keys() & raw_request.keys()
436
+ if len(conflicts) > 0:
437
+ raise ValueError(f"Duplicated fields: \"{conflicts}\".")
438
+ raw_request = {**args, **raw_request}
439
+
400
440
  ################################################################################
401
441
  # session validation
402
442
  ################################################################################
@@ -471,8 +511,27 @@ class JSONRPCService:
471
511
 
472
512
  self.type_adapter = TypeAdapter(Union[JSONRPCRequest, JSONRPCNotification])
473
513
 
474
- @api.route(tag="schema_free")
475
- def message(self, raw_request: Dict[str, Any]) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
514
+ @api.route(tag=api.TAG_ENDPOINT)
515
+ def message(
516
+ self,
517
+ raw_request: Any,
518
+ args: Optional[Dict[str, str]] = None
519
+ ) -> Union[JSONRPCResponse, Iterable[JSONRPCResponse], None]:
520
+ if isinstance(raw_request, List):
521
+ raise ValueError("Batching RCP requests is not supported yet.")
522
+
523
+ if not isinstance(raw_request, Dict):
524
+ raise ValueError(
525
+ f"/message only accepts JSON object as input. "
526
+ f"Expect \"dict\", got \"{type(raw_request)}\"."
527
+ )
528
+
529
+ if args is not None:
530
+ conflicts = args.keys() & raw_request.keys()
531
+ if len(conflicts) > 0:
532
+ raise ValueError(f"Duplicated fields: \"{conflicts}\".")
533
+ raw_request = {**args, **raw_request}
534
+
476
535
  request = self.type_adapter.validate_python(raw_request)
477
536
  path = f"/{request.method}"
478
537
  route = self.service_routes.get(path, self.builtin_routes.get(path))
@@ -502,6 +561,10 @@ class LifeCycleService:
502
561
  serverInfo=Implementation(name="python-libentry", version="1.0.0")
503
562
  )
504
563
 
564
+ @api.route()
565
+ def ping(self):
566
+ return None
567
+
505
568
 
506
569
  class NotificationsService:
507
570
 
@@ -521,7 +584,7 @@ class ToolsService:
521
584
  self._tool_routes = {}
522
585
  for route in self.service_routes.values():
523
586
  api_info = route.api_info
524
- if api_info.tag != "tool":
587
+ if api_info.tag != api.TAG_TOOL:
525
588
  continue
526
589
  self._tool_routes[api_info.name] = route
527
590
  return self._tool_routes
@@ -636,7 +699,7 @@ class ResourcesService:
636
699
  self._resource_routes = {}
637
700
  for route in self.service_routes.values():
638
701
  api_info = route.api_info
639
- if api_info.tag != "resource":
702
+ if api_info.tag != api.TAG_RESOURCE:
640
703
  continue
641
704
  uri = api_info.model_extra.get("uri", api_info.path)
642
705
  self._resource_routes[uri] = route
@@ -771,10 +834,15 @@ class FlaskServer(Flask):
771
834
  logger.error(f"Async function \"{fn.__name__}\" is not supported.")
772
835
  continue
773
836
 
774
- logger.info(f"Serving {path} as {', '.join(methods)}.")
775
-
776
837
  handler = FlaskHandler(fn, api_info, self)
777
838
  routes[path] = Route(api_info=api_info, fn=fn, handler=handler)
839
+
840
+ mode = api.TAG_ENDPOINT
841
+ if isinstance(handler.default_adapter, SubroutineAdapter):
842
+ mode = api.TAG_SUBROUTINE
843
+ elif isinstance(handler.default_adapter, JSONRPCAdapter):
844
+ mode = api.TAG_JSONRPC
845
+ logger.info(f"{mode.capitalize()}:\tmethod={'|'.join(methods)}\tpath={path}")
778
846
  return routes
779
847
 
780
848
  def ok(self, body: Union[str, Iterable[str], None], mimetype: str):
libentry/resource.py ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+
3
+ __author__ = "xi"
4
+ __all__ = [
5
+ "AbstractResourceManager",
6
+ "JSONResourceManager",
7
+ "RemoteResourceManager",
8
+ "ResourceManager",
9
+ ]
10
+
11
+ import abc
12
+ from typing import Any, Dict, List
13
+ from urllib.parse import urlparse
14
+
15
+ import yaml
16
+
17
+
18
+ class AbstractResourceManager(abc.ABC):
19
+
20
+ @abc.abstractmethod
21
+ def list(self) -> List[str]:
22
+ pass
23
+
24
+ @abc.abstractmethod
25
+ def get(self, name: str) -> Any:
26
+ pass
27
+
28
+ @abc.abstractmethod
29
+ def set(self, name: str, value: Any) -> Any:
30
+ pass
31
+
32
+
33
+ class JSONResourceManager(AbstractResourceManager):
34
+
35
+ def __init__(self, path: str):
36
+ with open(path, "r") as f:
37
+ self.resource_obj = yaml.load(f, yaml.SafeLoader)
38
+
39
+ if not isinstance(self.resource_obj, Dict):
40
+ raise RuntimeError(
41
+ f"Expect a JSON object, "
42
+ f"got {type(self.resource_obj)}."
43
+ )
44
+
45
+ def list(self) -> List[str]:
46
+ return [*self.resource_obj.keys()]
47
+
48
+ def get(self, name: str) -> Any:
49
+ return self.resource_obj.get(name)
50
+
51
+ def set(self, name: str, value: Any) -> Any:
52
+ raise NotImplementedError("\"set\" is not supported by JSONResourceClient.")
53
+
54
+
55
+ class RemoteResourceManager(AbstractResourceManager):
56
+
57
+ def __init__(self, url: str):
58
+ pass
59
+
60
+
61
+ class ResourceManager:
62
+
63
+ def __new__(cls, uri: str) -> AbstractResourceManager:
64
+ parsed_uri = urlparse(uri)
65
+ if parsed_uri.scheme in {"", "json", "yaml", "yml"}:
66
+ return JSONResourceManager(parsed_uri.path)
67
+ else:
68
+ raise NotImplementedError(f"Scheme \"{parsed_uri.scheme}\" is not supported.")
69
+
70
+
71
+ def _test():
72
+ client = ResourceManager("config_test.yml")
73
+ print(client.get("qwen_2.5"))
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(_test())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: libentry
3
- Version: 1.23.3
3
+ Version: 1.24
4
4
  Summary: Entries for experimental utilities.
5
5
  Home-page: https://github.com/XoriieInpottn/libentry
6
6
  Author: xi
@@ -6,13 +6,14 @@ libentry/executor.py,sha256=cTV0WxJi0nU1TP-cOwmeodN8DD6L1691M2HIQsJtGrU,6582
6
6
  libentry/experiment.py,sha256=ejgAHDXWIe9x4haUzIFuz1WasLY0_aD1z_vyEVGjTu8,4922
7
7
  libentry/json.py,sha256=CubUUu29h7idLaC4d66vKhjBgVHKN1rZOv-Tw2qM17k,1916
8
8
  libentry/logging.py,sha256=IiYoCUzm8XTK1fduA-NA0FI2Qz_m81NEPV3d3tEfgdI,1349
9
+ libentry/resource.py,sha256=qAPXF7RzRYeCM4xWft6XR3SYk7lEiQ6ymOHbpNoi0mQ,1821
9
10
  libentry/schema.py,sha256=40SOhCF_eytWOF47MWKCRHKHl_lCaQVetx1Af62PkiI,10439
10
11
  libentry/test_api.py,sha256=Xw7B7sH6g1iCTV5sFzyBF3JAJzeOr9xg0AyezTNsnIk,4452
11
12
  libentry/utils.py,sha256=O7P6GadtUIjq0N2IZH7PhHZDUM3NebzcqyDqytet7CM,683
12
13
  libentry/mcp/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
13
- libentry/mcp/api.py,sha256=uoGBYCesMj6umlJpRulKZNS3trm9oG3LUSg1otPDS_8,2362
14
+ libentry/mcp/api.py,sha256=KSvz6_TNGKQQoFJPNH1XIc8CMji0I2_CqC9VCSOPRfc,2491
14
15
  libentry/mcp/client.py,sha256=tEJbMpXiMKQ0qDgLGyavE2A4jpoXliqbrVj_TMfOGXI,22488
15
- libentry/mcp/service.py,sha256=KDpEUhHuyVXjc_J5Z9_aciJbTcEy9dYA44rpdgAAwGE,32322
16
+ libentry/mcp/service.py,sha256=VBacwSN9xiV3wk5Ta0LHxRAor1cwR04ZavlKLTdTuWw,34721
16
17
  libentry/mcp/types.py,sha256=xTQCnKAgeJNss4klJ33MrWHGCzG_LeR3urizO_Z9q9U,12239
17
18
  libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
18
19
  libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
@@ -21,10 +22,10 @@ libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
21
22
  libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
22
23
  libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
23
24
  libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
24
- libentry-1.23.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
- libentry-1.23.3.dist-info/METADATA,sha256=5AIjlPrO1Q8nWDiCZNN5S4qo8jZCqNXD_twACQuw0T0,1135
26
- libentry-1.23.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
27
- libentry-1.23.3.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
28
- libentry-1.23.3.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
29
- libentry-1.23.3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
30
- libentry-1.23.3.dist-info/RECORD,,
25
+ libentry-1.24.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
26
+ libentry-1.24.dist-info/METADATA,sha256=zvGz_iBvBEMl_1HH4odL37ySnpBaLTTo7dAsxdwNrB4,1133
27
+ libentry-1.24.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
28
+ libentry-1.24.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
29
+ libentry-1.24.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
30
+ libentry-1.24.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
+ libentry-1.24.dist-info/RECORD,,