scmcp-shared 0.2.0__py3-none-any.whl → 0.3.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.
scmcp_shared/util.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import inspect
2
2
  import os
3
+ from enum import Enum
3
4
  from pathlib import Path
4
5
  from fastmcp.server.dependencies import get_context
5
-
6
+ from fastmcp.exceptions import ToolError
7
+ import asyncio
8
+ import nest_asyncio
6
9
 
7
10
 
8
11
  def get_env(key):
@@ -19,7 +22,7 @@ def filter_args(request, func, **extra_kwargs):
19
22
  return func_kwargs
20
23
 
21
24
 
22
- def add_op_log(adata, func, kwargs):
25
+ def add_op_log(adata, func, kwargs, adinfo):
23
26
  import hashlib
24
27
  import json
25
28
 
@@ -37,7 +40,7 @@ def add_op_log(adata, func, kwargs):
37
40
  func_name = func.__class__.__name__
38
41
  else:
39
42
  func_name = str(func)
40
- new_kwargs = {}
43
+ new_kwargs = {**adinfo.model_dump()}
41
44
  for k,v in kwargs.items():
42
45
  if isinstance(v, tuple):
43
46
  new_kwargs[k] = list(v)
@@ -50,6 +53,7 @@ def add_op_log(adata, func, kwargs):
50
53
  hash_input = f"{func_name}:{kwargs_str}"
51
54
  hash_key = hashlib.md5(hash_input.encode()).hexdigest()
52
55
  adata.uns["operation"]["op"][hash_key] = {func_name: new_kwargs}
56
+ adata.uns["operation"]["opid"] = list(adata.uns["operation"]["opid"])
53
57
  adata.uns["operation"]["opid"].append(hash_key)
54
58
  from .logging_config import setup_logger
55
59
  logger = setup_logger(log_file=get_env("LOG_FILE"))
@@ -57,7 +61,7 @@ def add_op_log(adata, func, kwargs):
57
61
 
58
62
 
59
63
 
60
- def savefig(axes, file):
64
+ def save_fig_path(axes, file):
61
65
  from matplotlib.axes import Axes
62
66
 
63
67
  try:
@@ -83,7 +87,7 @@ def savefig(axes, file):
83
87
  raise e
84
88
 
85
89
 
86
- def set_fig_path(axes, func=None, **kwargs):
90
+ def savefig(axes, func=None, **kwargs):
87
91
  if hasattr(func, "func") and hasattr(func.func, "__name__"):
88
92
  # For partial functions, use the original function name
89
93
  func_name = func.func.__name__
@@ -103,12 +107,12 @@ def set_fig_path(axes, func=None, **kwargs):
103
107
  args.append(f"{k}-{'-'.join([str(i) for i in v])}")
104
108
  else:
105
109
  args.append(f"{k}-{v}")
106
- args_str = "_".join(args)
110
+ args_str = "_".join(args).replace(" ", "")
107
111
  fig_path = fig_dir / f"{func_name}_{args_str}.png"
108
112
  try:
109
- savefig(axes, fig_path)
113
+ save_fig_path(axes, fig_path)
110
114
  except PermissionError:
111
- raise PermissionError("You don't have permission to rename this file")
115
+ raise PermissionError("You don't have permission to save figure")
112
116
  except Exception as e:
113
117
  raise e
114
118
  transport = get_env("TRANSPORT")
@@ -134,13 +138,20 @@ async def get_figure(request):
134
138
  return FileResponse(figure_path)
135
139
 
136
140
 
137
- async def forward_request(func, request, **kwargs):
141
+ def add_figure_route(server):
142
+ from starlette.routing import Route
143
+ server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
144
+
145
+
146
+ async def async_forward_request(func, request, adinfo, **kwargs):
138
147
  from fastmcp import Client
139
148
  forward_url = get_env("FORWARD")
140
149
  request_kwargs = request.model_dump()
141
150
  request_args = request.model_fields_set
142
- func_kwargs = {"request": {k: request_kwargs.get(k) for k in request_args}}
143
- func_kwargs.update({k:v for k,v in kwargs.items() if v is not None})
151
+ func_kwargs = {
152
+ "request": {k: request_kwargs.get(k) for k in request_args},
153
+ "adinfo": adinfo.model_dump()
154
+ }
144
155
  if not forward_url:
145
156
  return None
146
157
 
@@ -151,31 +162,44 @@ async def forward_request(func, request, **kwargs):
151
162
  try:
152
163
  result = await client.call_tool(func, func_kwargs)
153
164
  return result
165
+ except ToolError as e:
166
+ raise ToolError(e)
154
167
  except Exception as e:
168
+ if hasattr(e, '__context__') and e.__context__:
169
+ raise Exception(f"{str(e.__context__)}")
170
+ else:
171
+ raise e
172
+
173
+
174
+
175
+ def forward_request(func, request, adinfo, **kwargs):
176
+ """Synchronous wrapper for forward_request"""
177
+ try:
178
+ # Apply nest_asyncio to allow nested event loops
179
+ nest_asyncio.apply()
180
+ loop = asyncio.get_event_loop()
181
+ if loop.is_running():
182
+ # If we're in a running event loop, use create_task
183
+ async def _run():
184
+ return await async_forward_request(func, request, adinfo, **kwargs)
185
+ return loop.run_until_complete(_run())
186
+ else:
187
+ # If no event loop is running, use asyncio.run()
188
+ return asyncio.run(async_forward_request(func, request, adinfo, **kwargs))
189
+ except Exception as e:
190
+ if hasattr(e, '__context__') and e.__context__:
191
+ raise Exception(f"{str(e.__context__)}")
192
+ else:
155
193
  raise e
156
194
 
195
+
157
196
  def obsm2adata(adata, obsm_key):
158
197
  from anndata import AnnData
159
198
 
160
199
  if obsm_key not in adata.obsm_keys():
161
200
  raise ValueError(f"key {obsm_key} not found in adata.obsm")
162
201
  else:
163
- return AnnData(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm)
164
-
165
-
166
- async def get_figure(request):
167
- figure_name = request.path_params["figure_name"]
168
- figure_path = f"./figures/{figure_name}"
169
-
170
- if not os.path.isfile(figure_path):
171
- return Response(content={"error": "figure not found"}, media_type="application/json")
172
-
173
- return FileResponse(figure_path)
174
-
175
-
176
- def add_figure_route(server):
177
- from starlette.routing import Route
178
- server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
202
+ return AnnData(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm, uns=adata.uns)
179
203
 
180
204
 
181
205
  def get_ads():
@@ -184,26 +208,69 @@ def get_ads():
184
208
  return ads
185
209
 
186
210
 
187
- def generate_msg(request, adata, ads):
188
- kwargs = request.model_dump()
189
- sampleid = kwargs.get("sampleid")
190
- dtype = kwargs.get("dtype", "exp")
191
- return {"sampleid": sampleid or ads.active_id, "dtype": dtype, "adata": adata}
211
+ def generate_msg(adinfo, adata, ads):
212
+ return {"sampleid": adinfo.sampleid or ads.active_id, "dtype": adinfo.adtype, "adata": adata}
192
213
 
193
214
 
194
- def sc_like_plot(plot_func, adata, request, **kwargs):
215
+ def sc_like_plot(plot_func, adata, request, adinfo, **kwargs):
216
+ from matplotlib import pyplot as plt
217
+
195
218
  func_kwargs = filter_args(request, plot_func, show=False, save=False)
196
219
  axes = plot_func(adata, **func_kwargs)
197
- fig_path = set_fig_path(axes, plot_func, **func_kwargs)
198
- add_op_log(adata, plot_func, func_kwargs)
220
+ if axes is None:
221
+ axes = plt.gca()
222
+ fig_path = savefig(axes, plot_func, **func_kwargs)
223
+ add_op_log(adata, plot_func, func_kwargs, adinfo)
199
224
  return fig_path
200
225
 
201
226
 
202
- async def filter_tools(mcp, include_tools=None, exclude_tools=None):
203
- tools = await mcp.get_tools()
204
- for tool in tools:
205
- if exclude_tools and tool in exclude_tools:
206
- mcp.remove_tool(tool)
207
- if include_tools and tool not in include_tools:
208
- mcp.remove_tool(tool)
209
- return mcp
227
+ def filter_tools(mcp, include_tools=None, exclude_tools=None):
228
+ import asyncio
229
+ import copy
230
+ mcp = copy.deepcopy(mcp)
231
+ async def _filter_tools(mcp, include_tools=None, exclude_tools=None):
232
+ tools = await mcp.get_tools()
233
+ for tool in tools:
234
+ if exclude_tools and tool in exclude_tools:
235
+ mcp.remove_tool(tool)
236
+ if include_tools and tool not in include_tools:
237
+ mcp.remove_tool(tool)
238
+ return mcp
239
+ return asyncio.run(_filter_tools(mcp, include_tools, exclude_tools))
240
+
241
+
242
+
243
+ def set_env(log_file, forward, transport, host, port):
244
+ if log_file is not None:
245
+ os.environ['SCMCP_LOG_FILE'] = log_file
246
+ if forward is not None:
247
+ os.environ['SCMCP_FORWARD'] = forward
248
+ os.environ['SCMCP_TRANSPORT'] = transport
249
+ os.environ['SCMCP_HOST'] = host
250
+ os.environ['SCMCP_PORT'] = str(port)
251
+
252
+
253
+
254
+ def setup_mcp(mcp, sub_mcp_dic, modules=None):
255
+ import asyncio
256
+ if modules is None or modules == "all":
257
+ modules = sub_mcp_dic.keys()
258
+ for module in modules:
259
+ asyncio.run(mcp.import_server(module, sub_mcp_dic[module]))
260
+ return mcp
261
+
262
+ def _update_args(mcp, func, args_dic : dict):
263
+ defs = mcp._tool_manager._tools[func].parameters['$defs']
264
+ model_names = list(defs.keys())
265
+ args_model = model_names[0] if model_names[0] != "AdataModel" else model_names[1]
266
+ for args, property_dic in args_dic.items():
267
+ for pk, v in property_dic.items():
268
+ for model in model_names:
269
+ if args in mcp._tool_manager._tools[func].parameters['$defs'][model]["properties"]:
270
+ mcp._tool_manager._tools[func].parameters['$defs'][model]["properties"][args][pk] = v
271
+
272
+
273
+ def update_mcp_args(mcp, tool_args : dict):
274
+ tools = mcp._tool_manager._tools.keys()
275
+ for tool in tool_args:
276
+ _update_args(mcp, tool, tool_args[tool])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scmcp_shared
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A shared function libray for scmcphub
5
5
  Author-email: shuang <hsh-me@outlook.com>
6
6
  License: BSD 3-Clause License
@@ -0,0 +1,21 @@
1
+ scmcp_shared/__init__.py,sha256=nSn3YFzkE-BAmUSmif33CxdohNxkgmLE9cYFNbjEu_E,24
2
+ scmcp_shared/cli.py,sha256=NU31Vv-xev8yGtl1KHZhVswJNbK18x2AVzhhH9MR03M,3325
3
+ scmcp_shared/logging_config.py,sha256=eCuLuyxMmbj8A1E0VqYWoKA5JPTSbo6cmjS4LOyd0RQ,872
4
+ scmcp_shared/util.py,sha256=vxmR6_iOBa5jaBNBWl3QBysJ14kXRrqrENDRl3_TChQ,9339
5
+ scmcp_shared/schema/__init__.py,sha256=Kwkc7kPLjExOlxt1sWEy_5qa96MvOS8sNCMlZa6yRg8,737
6
+ scmcp_shared/schema/io.py,sha256=ZKJpKkKazDE3_ZX3GtMIT08kSaiNmy0qVaxivhN7Dx4,4744
7
+ scmcp_shared/schema/pl.py,sha256=rzE09wHMY3JR56HZc-QfIUUM0fGXRKd-7Dh3CrQrFB0,29547
8
+ scmcp_shared/schema/pp.py,sha256=48F6oKf-I8IZuNQDfq_Lpp3fLLKA4PruqRje_ZrtTyw,21664
9
+ scmcp_shared/schema/tl.py,sha256=0HlZ_WQkgnc93LfIYapRCijWBBaFAdM5SV3Ioht_1wI,34281
10
+ scmcp_shared/schema/util.py,sha256=x_2GPsmliHabi9V5C6YEv_M8ZHJsinDZJ6ePWrLPmcI,4815
11
+ scmcp_shared/server/__init__.py,sha256=4KE2Y_gDenF0ZyTGicQW0fTgJfMIQYZfpRP4hQ4rFYs,416
12
+ scmcp_shared/server/base.py,sha256=kaOfi4yHLWr_AdBdpKzDrJiwRmbHF2jbsfI5g2qqgRA,6576
13
+ scmcp_shared/server/io.py,sha256=yrQXdkAsPvk_62xQk5-SRmc5-jAvI-d3sE0QqHDXfMM,3424
14
+ scmcp_shared/server/pl.py,sha256=HdhjrZfEx-IzCkZ03IGCW-mjUItfNvKdSOFbJ2_-2XQ,14854
15
+ scmcp_shared/server/pp.py,sha256=neyRV3yEZ2a08eO-COI-t-PFFvntRzNmu19SQ77do_4,15406
16
+ scmcp_shared/server/tl.py,sha256=2vfl5FrWQgQlPTB4-QLFMuMrtCux6ppmFttKOrryhF0,18320
17
+ scmcp_shared/server/util.py,sha256=L51fFL7Nb84JCQrFYyZIGsfmNFlA9uGzTV93hvc9mfI,12025
18
+ scmcp_shared-0.3.0.dist-info/METADATA,sha256=M8NokyJHPjAVnN-JYmprlFhAfDq_XvAAU1l-kiH8LOA,2099
19
+ scmcp_shared-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ scmcp_shared-0.3.0.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
21
+ scmcp_shared-0.3.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- from pydantic import Field, BaseModel,ConfigDict
2
-
3
-
4
- class AdataModel(BaseModel):
5
- """Input schema for the adata tool."""
6
- sampleid: str = Field(default=None, description="adata sampleid")
7
- adtype: str = Field(default="exp", description="adata.X data type")
8
-
9
- model_config = ConfigDict(
10
- extra="ignore"
11
- )
@@ -1,20 +0,0 @@
1
- scmcp_shared/__init__.py,sha256=ZJMoFGVU2-tXepjhh5Fhy8ZyNHtOIGZWbJygHmqwmCA,24
2
- scmcp_shared/logging_config.py,sha256=eCuLuyxMmbj8A1E0VqYWoKA5JPTSbo6cmjS4LOyd0RQ,872
3
- scmcp_shared/util.py,sha256=Ul88pY0aHFN42tiWwexO5LfQJgNslyOD4Jb9HTMy3uI,6814
4
- scmcp_shared/schema/__init__.py,sha256=fwMJT4mQ3rvRJpSz9ruwwZU1GbXsUYPVQRpP56Az_JM,28
5
- scmcp_shared/schema/base.py,sha256=hWh0534xon4gyIxwXUBVR067uFiW73nZccSs6DkcwYc,326
6
- scmcp_shared/schema/io.py,sha256=yxa0BGQXuHCUvv6ZGQApPqMOlyJuC-eQNk7jlRGsMJ8,4913
7
- scmcp_shared/schema/pl.py,sha256=I9SCJgjmfB0VTNio4uNnP26ajWCnsHPsDoO-yMuzYVo,29578
8
- scmcp_shared/schema/pp.py,sha256=Uhe28zD71UDZbziesH6bgQXIRXQz_HHrr8-Hb4RQsKI,21696
9
- scmcp_shared/schema/tl.py,sha256=QV0dP-5ZtEKUDmSV9m5ryx7uQub4rw4in7PYsxId5nU,34485
10
- scmcp_shared/schema/util.py,sha256=R0MThHKKGYGGJu-hMc-i7fomW-6ugaoQqPi_hbKrB5A,4844
11
- scmcp_shared/server/__init__.py,sha256=vGYGUpLt8XHRbJI84Ox6WnK4ntzsbTal99Uu14nav60,1796
12
- scmcp_shared/server/io.py,sha256=agioBQeTREqOX8qJsP2aOTkaU3dT04uvObWhUSfOPTg,2422
13
- scmcp_shared/server/pl.py,sha256=0KIB9n-h5L3F4TOO_ezPAEX7TVCVBOsiZd6bqD5WbGQ,11126
14
- scmcp_shared/server/pp.py,sha256=iS-GrU2-Fyw0phLI4A44g2Y3LpFfYF7AWU5ueyzLFxY,12116
15
- scmcp_shared/server/tl.py,sha256=k8GCTmrFh63jI5REO4-BmfCgyvcGgNpvtsxrn_0K67k,13228
16
- scmcp_shared/server/util.py,sha256=eBkwWUSrndTb6RHujgXGZYU1aitJEocEuV6U5IOLsws,9030
17
- scmcp_shared-0.2.0.dist-info/METADATA,sha256=Ayh--1Js_wfAHx1Wep-tw0fxihV9ikUYRWttSwAZL88,2099
18
- scmcp_shared-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
- scmcp_shared-0.2.0.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
20
- scmcp_shared-0.2.0.dist-info/RECORD,,