scmcp-shared 0.1.0__py3-none-any.whl → 0.2.1__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/schema/__init__.py +1 -1
- scmcp_shared/schema/base.py +11 -0
- scmcp_shared/schema/io.py +5 -8
- scmcp_shared/schema/pl.py +3 -3
- scmcp_shared/schema/pp.py +14 -70
- scmcp_shared/schema/tl.py +69 -18
- scmcp_shared/schema/util.py +18 -11
- scmcp_shared/server/__init__.py +51 -1
- scmcp_shared/server/io.py +28 -29
- scmcp_shared/server/pl.py +324 -0
- scmcp_shared/server/pp.py +368 -0
- scmcp_shared/server/tl.py +432 -0
- scmcp_shared/server/util.py +251 -0
- scmcp_shared/util.py +79 -49
- {scmcp_shared-0.1.0.dist-info → scmcp_shared-0.2.1.dist-info}/METADATA +2 -2
- scmcp_shared-0.2.1.dist-info/RECORD +20 -0
- scmcp_shared-0.1.0.dist-info/RECORD +0 -15
- {scmcp_shared-0.1.0.dist-info → scmcp_shared-0.2.1.dist-info}/WHEEL +0 -0
- {scmcp_shared-0.1.0.dist-info → scmcp_shared-0.2.1.dist-info}/licenses/LICENSE +0 -0
scmcp_shared/util.py
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
import inspect
|
2
2
|
import os
|
3
3
|
from pathlib import Path
|
4
|
-
|
4
|
+
from fastmcp.server.dependencies import get_context
|
5
|
+
from fastmcp.exceptions import ToolError
|
5
6
|
|
6
7
|
|
7
8
|
def get_env(key):
|
8
9
|
return os.environ.get(f"SCMCP_{key.upper()}")
|
9
10
|
|
10
11
|
|
11
|
-
def filter_args(request, func):
|
12
|
-
# sometime,it is a bit redundant, but I think it adds robustness in case the function parameters change
|
12
|
+
def filter_args(request, func, **extra_kwargs):
|
13
13
|
kwargs = request.model_dump()
|
14
14
|
args = request.model_fields_set
|
15
15
|
parameters = inspect.signature(func).parameters
|
16
|
+
extra_kwargs = {k: extra_kwargs[k] for k in extra_kwargs if k in parameters}
|
16
17
|
func_kwargs = {k: kwargs.get(k) for k in args if k in parameters}
|
18
|
+
func_kwargs.update(extra_kwargs)
|
17
19
|
return func_kwargs
|
18
20
|
|
19
21
|
|
@@ -48,6 +50,7 @@ def add_op_log(adata, func, kwargs):
|
|
48
50
|
hash_input = f"{func_name}:{kwargs_str}"
|
49
51
|
hash_key = hashlib.md5(hash_input.encode()).hexdigest()
|
50
52
|
adata.uns["operation"]["op"][hash_key] = {func_name: new_kwargs}
|
53
|
+
adata.uns["operation"]["opid"] = list(adata.uns["operation"]["opid"])
|
51
54
|
adata.uns["operation"]["opid"].append(hash_key)
|
52
55
|
from .logging_config import setup_logger
|
53
56
|
logger = setup_logger(log_file=get_env("LOG_FILE"))
|
@@ -55,25 +58,44 @@ def add_op_log(adata, func, kwargs):
|
|
55
58
|
|
56
59
|
|
57
60
|
|
58
|
-
def savefig(
|
61
|
+
def savefig(axes, file):
|
62
|
+
from matplotlib.axes import Axes
|
63
|
+
|
59
64
|
try:
|
60
65
|
file_path = Path(file)
|
61
66
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
62
|
-
if
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
67
|
+
if isinstance(axes, list):
|
68
|
+
if isinstance(axes[0], Axes):
|
69
|
+
axes[0].figure.savefig(file_path)
|
70
|
+
elif isinstance(axes, dict):
|
71
|
+
ax = list(axes.values())[0]
|
72
|
+
if isinstance(ax, Axes):
|
73
|
+
ax.figure.savefig(file_path)
|
74
|
+
elif isinstance(axes, Axes):
|
75
|
+
axes.figure.savefig(file_path)
|
76
|
+
elif hasattr(axes, 'savefig'): # if Figure
|
77
|
+
axes.savefig(file_path)
|
78
|
+
elif hasattr(axes, 'save'): # for plotnine.ggplot.ggplot
|
79
|
+
axes.save(file_path)
|
80
|
+
else:
|
81
|
+
raise ValueError(f"axes must be a Axes or plotnine object, but got {type(axes)}")
|
68
82
|
return file_path
|
69
83
|
except Exception as e:
|
70
84
|
raise e
|
71
85
|
|
72
86
|
|
73
|
-
def set_fig_path(
|
74
|
-
|
75
|
-
|
87
|
+
def set_fig_path(axes, func=None, **kwargs):
|
88
|
+
if hasattr(func, "func") and hasattr(func.func, "__name__"):
|
89
|
+
# For partial functions, use the original function name
|
90
|
+
func_name = func.func.__name__
|
91
|
+
elif hasattr(func, "__name__"):
|
92
|
+
func_name = func.__name__
|
93
|
+
elif hasattr(func, "__class__"):
|
94
|
+
func_name = func.__class__.__name__
|
95
|
+
else:
|
96
|
+
func_name = str(func)
|
76
97
|
|
98
|
+
fig_dir = Path(os.getcwd()) / "figures"
|
77
99
|
kwargs.pop("save", None)
|
78
100
|
kwargs.pop("show", None)
|
79
101
|
args = []
|
@@ -83,40 +105,13 @@ def set_fig_path(func, fig=None, **kwargs):
|
|
83
105
|
else:
|
84
106
|
args.append(f"{k}-{v}")
|
85
107
|
args_str = "_".join(args)
|
86
|
-
|
87
|
-
old_path = fig_dir / 'dotplot_.png'
|
88
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
89
|
-
elif func in ["scatter", "embedding"]:
|
90
|
-
if "basis" in kwargs and kwargs['basis'] is not None:
|
91
|
-
old_path = fig_dir / f"{kwargs['basis']}.png"
|
92
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
93
|
-
else:
|
94
|
-
old_path = fig_dir / f"{func}.png"
|
95
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
96
|
-
elif func == "highly_variable_genes":
|
97
|
-
old_path = fig_dir / 'filter_genes_dispersion.png'
|
98
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
99
|
-
elif func == "scvelo_projection":
|
100
|
-
old_path = fig_dir / f"scvelo_{kwargs['kernel']}.png"
|
101
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
102
|
-
else:
|
103
|
-
if (fig_dir / f"{func}_.png").is_file():
|
104
|
-
old_path = fig_dir / f"{func}_.png"
|
105
|
-
else:
|
106
|
-
old_path = fig_dir / f"{func}.png"
|
107
|
-
fig_path = fig_dir / f"{func}_{args_str}.png"
|
108
|
+
fig_path = fig_dir / f"{func_name}_{args_str}.png"
|
108
109
|
try:
|
109
|
-
|
110
|
-
savefig(fig, fig_path)
|
111
|
-
else:
|
112
|
-
os.rename(old_path, fig_path)
|
113
|
-
return fig_path
|
114
|
-
except FileNotFoundError:
|
115
|
-
print(f"The file {old_path} does not exist")
|
116
|
-
except FileExistsError:
|
117
|
-
print(f"The file {fig_path} already exists")
|
110
|
+
savefig(axes, fig_path)
|
118
111
|
except PermissionError:
|
119
|
-
|
112
|
+
raise PermissionError("You don't have permission to rename this file")
|
113
|
+
except Exception as e:
|
114
|
+
raise e
|
120
115
|
transport = get_env("TRANSPORT")
|
121
116
|
if transport == "stdio":
|
122
117
|
return fig_path
|
@@ -134,7 +129,6 @@ async def get_figure(request):
|
|
134
129
|
figure_name = request.path_params["figure_name"]
|
135
130
|
figure_path = f"./figures/{figure_name}"
|
136
131
|
|
137
|
-
# 检查文件是否存在
|
138
132
|
if not os.path.isfile(figure_path):
|
139
133
|
return Response(content={"error": "figure not found"}, media_type="application/json")
|
140
134
|
|
@@ -158,8 +152,14 @@ async def forward_request(func, request, **kwargs):
|
|
158
152
|
try:
|
159
153
|
result = await client.call_tool(func, func_kwargs)
|
160
154
|
return result
|
155
|
+
except ToolError as e:
|
156
|
+
raise ToolError(e)
|
161
157
|
except Exception as e:
|
162
|
-
|
158
|
+
if hasattr(e, '__context__') and e.__context__:
|
159
|
+
raise Exception(f"{str(e.__context__)}")
|
160
|
+
else:
|
161
|
+
raise e
|
162
|
+
|
163
163
|
|
164
164
|
def obsm2adata(adata, obsm_key):
|
165
165
|
from anndata import AnnData
|
@@ -174,7 +174,6 @@ async def get_figure(request):
|
|
174
174
|
figure_name = request.path_params["figure_name"]
|
175
175
|
figure_path = f"./figures/{figure_name}"
|
176
176
|
|
177
|
-
# 检查文件是否存在
|
178
177
|
if not os.path.isfile(figure_path):
|
179
178
|
return Response(content={"error": "figure not found"}, media_type="application/json")
|
180
179
|
|
@@ -183,4 +182,35 @@ async def get_figure(request):
|
|
183
182
|
|
184
183
|
def add_figure_route(server):
|
185
184
|
from starlette.routing import Route
|
186
|
-
server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
|
185
|
+
server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
|
186
|
+
|
187
|
+
|
188
|
+
def get_ads():
|
189
|
+
ctx = get_context()
|
190
|
+
ads = ctx.request_context.lifespan_context
|
191
|
+
return ads
|
192
|
+
|
193
|
+
|
194
|
+
def generate_msg(request, adata, ads):
|
195
|
+
kwargs = request.model_dump()
|
196
|
+
sampleid = kwargs.get("sampleid")
|
197
|
+
dtype = kwargs.get("dtype", "exp")
|
198
|
+
return {"sampleid": sampleid or ads.active_id, "dtype": dtype, "adata": adata}
|
199
|
+
|
200
|
+
|
201
|
+
def sc_like_plot(plot_func, adata, request, **kwargs):
|
202
|
+
func_kwargs = filter_args(request, plot_func, show=False, save=False)
|
203
|
+
axes = plot_func(adata, **func_kwargs)
|
204
|
+
fig_path = set_fig_path(axes, plot_func, **func_kwargs)
|
205
|
+
add_op_log(adata, plot_func, func_kwargs)
|
206
|
+
return fig_path
|
207
|
+
|
208
|
+
|
209
|
+
async def filter_tools(mcp, include_tools=None, exclude_tools=None):
|
210
|
+
tools = await mcp.get_tools()
|
211
|
+
for tool in tools:
|
212
|
+
if exclude_tools and tool in exclude_tools:
|
213
|
+
mcp.remove_tool(tool)
|
214
|
+
if include_tools and tool not in include_tools:
|
215
|
+
mcp.remove_tool(tool)
|
216
|
+
return mcp
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: scmcp_shared
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
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
|
@@ -33,7 +33,7 @@ License: BSD 3-Clause License
|
|
33
33
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
34
|
License-File: LICENSE
|
35
35
|
Requires-Python: >=3.10
|
36
|
-
Requires-Dist: fastmcp>=2.3.
|
36
|
+
Requires-Dist: fastmcp>=2.3.4
|
37
37
|
Requires-Dist: mcp>=1.8.0
|
38
38
|
Requires-Dist: pydantic
|
39
39
|
Requires-Dist: scanpy
|
@@ -0,0 +1,20 @@
|
|
1
|
+
scmcp_shared/__init__.py,sha256=_oNJ0-RgmWF_9YXGOzXxULVblXWDY-ijzApYQs3nBNU,24
|
2
|
+
scmcp_shared/logging_config.py,sha256=eCuLuyxMmbj8A1E0VqYWoKA5JPTSbo6cmjS4LOyd0RQ,872
|
3
|
+
scmcp_shared/util.py,sha256=yQpVLqzvDNMRGF9sywh0l5M0XH_u3gbi8MC8aQHj-CQ,7130
|
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=zT9HBVMhIznm7im22ixGtlqFQSUyiengjgSk2Hi6WYI,2568
|
13
|
+
scmcp_shared/server/pl.py,sha256=TbgfOPxh0b01Er7qVN0QrPd54HF2Dcf7LBgGG7DtNaA,11414
|
14
|
+
scmcp_shared/server/pp.py,sha256=iVCHhQJKNPAboJmb1vvjbFfLOH8yibFRm8mCk2-JhHs,12382
|
15
|
+
scmcp_shared/server/tl.py,sha256=fFTfE0_kXQ6e21pj-sS8vwkyCp8pqe2Mhe1avA9wjZ8,14024
|
16
|
+
scmcp_shared/server/util.py,sha256=LSdsBpk_deQZPT33nedyvILdHKcPFaHsr8s-oREh9Sc,9175
|
17
|
+
scmcp_shared-0.2.1.dist-info/METADATA,sha256=u91DFN3s3GzOmnB8MrZWSSnlRf9zbRFpGRogLzM-G7o,2099
|
18
|
+
scmcp_shared-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
19
|
+
scmcp_shared-0.2.1.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
|
20
|
+
scmcp_shared-0.2.1.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
scmcp_shared/__init__.py,sha256=2Ff5FiaFaDmrGrcJagyK7JKuraTfI0CnRahAnXLS53w,24
|
2
|
-
scmcp_shared/logging_config.py,sha256=eCuLuyxMmbj8A1E0VqYWoKA5JPTSbo6cmjS4LOyd0RQ,872
|
3
|
-
scmcp_shared/util.py,sha256=jY-HbMzpqKQpzm0UhiyrUwtjtx31_KKUmewlyZYpfZA,6300
|
4
|
-
scmcp_shared/schema/__init__.py,sha256=uEIJaRLssrbI9KYOH3RxtMRU63Bes9SkLBskxNBt0hA,18
|
5
|
-
scmcp_shared/schema/io.py,sha256=V6Bv6No_FH0Ps2E6VkkHBu7z2EQku9XcG4iWkxMPjiU,4965
|
6
|
-
scmcp_shared/schema/pl.py,sha256=yP9XnotdEvY6iPKfvz0otfJFyDJh7FkaIJx7FG4Em44,29548
|
7
|
-
scmcp_shared/schema/pp.py,sha256=aAHECfM1LSCRDZkgtPpoMxLXurEyVdG_9dyhj0HSOlw,23507
|
8
|
-
scmcp_shared/schema/tl.py,sha256=VNoNBf7hFF_4SG31vNYX52i0M38Bj6ryBJpCJYtedVw,32848
|
9
|
-
scmcp_shared/schema/util.py,sha256=rMb29eCu2hVhkggYeOPA2b72Aa8VPDq58nd2Rz8OmoI,4652
|
10
|
-
scmcp_shared/server/__init__.py,sha256=V4eLId2ZvwoPTE_53uimF02hLnY8X9iXlA4ri-WfvjM,22
|
11
|
-
scmcp_shared/server/io.py,sha256=AQ90QtQdfv2IFo_IPU6dmfk1redF3HPt30DHHv5_3qo,2815
|
12
|
-
scmcp_shared-0.1.0.dist-info/METADATA,sha256=VpPvS7tbQQJLtsCNbCJfIAi-iiDf2mm9VCBdqaelcbg,2099
|
13
|
-
scmcp_shared-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
-
scmcp_shared-0.1.0.dist-info/licenses/LICENSE,sha256=YNr1hpea195yq-wGtB8j-2dGtt7A5G00WENmxa7JGco,1495
|
15
|
-
scmcp_shared-0.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|