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/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(fig, file):
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 hasattr(fig, 'figure'): # if Axes
63
- fig.figure.savefig(file_path)
64
- elif hasattr(fig, 'save'): # for plotnine.ggplot.ggplot
65
- fig.save(file_path)
66
- else: # if Figure
67
- fig.savefig(file_path)
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(func, fig=None, **kwargs):
74
- "maybe I need to save figure by myself, instead of using scanpy save function..."
75
- fig_dir = Path(os.getcwd()) / "figures"
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
- if func == "rank_genes_groups_dotplot":
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
- if fig is not None:
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
- print("You don't have permission to rename this file")
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
- raise e
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.0
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.0
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,,