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/__init__.py +1 -1
- scmcp_shared/cli.py +96 -0
- scmcp_shared/schema/__init__.py +22 -1
- scmcp_shared/schema/io.py +4 -8
- scmcp_shared/schema/pl.py +2 -3
- scmcp_shared/schema/pp.py +15 -15
- scmcp_shared/schema/tl.py +18 -24
- scmcp_shared/schema/util.py +11 -11
- scmcp_shared/server/__init__.py +11 -49
- scmcp_shared/server/base.py +153 -0
- scmcp_shared/server/io.py +71 -61
- scmcp_shared/server/pl.py +288 -294
- scmcp_shared/server/pp.py +320 -358
- scmcp_shared/server/tl.py +393 -402
- scmcp_shared/server/util.py +241 -240
- scmcp_shared/util.py +110 -43
- {scmcp_shared-0.2.0.dist-info → scmcp_shared-0.3.0.dist-info}/METADATA +1 -1
- scmcp_shared-0.3.0.dist-info/RECORD +21 -0
- scmcp_shared/schema/base.py +0 -11
- scmcp_shared-0.2.0.dist-info/RECORD +0 -20
- {scmcp_shared-0.2.0.dist-info → scmcp_shared-0.3.0.dist-info}/WHEEL +0 -0
- {scmcp_shared-0.2.0.dist-info → scmcp_shared-0.3.0.dist-info}/licenses/LICENSE +0 -0
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
|
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
|
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
|
-
|
113
|
+
save_fig_path(axes, fig_path)
|
110
114
|
except PermissionError:
|
111
|
-
raise PermissionError("You don't have permission to
|
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
|
-
|
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 = {
|
143
|
-
|
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(
|
188
|
-
|
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
|
-
|
198
|
-
|
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
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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])
|
@@ -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,,
|
scmcp_shared/schema/base.py
DELETED
@@ -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,,
|
File without changes
|
File without changes
|