scmcp-shared 0.1.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 +3 -0
- scmcp_shared/logging_config.py +31 -0
- scmcp_shared/schema/__init__.py +1 -0
- scmcp_shared/schema/io.py +120 -0
- scmcp_shared/schema/pl.py +948 -0
- scmcp_shared/schema/pp.py +707 -0
- scmcp_shared/schema/tl.py +902 -0
- scmcp_shared/schema/util.py +131 -0
- scmcp_shared/server/__init__.py +1 -0
- scmcp_shared/server/io.py +80 -0
- scmcp_shared/util.py +186 -0
- scmcp_shared-0.1.0.dist-info/METADATA +44 -0
- scmcp_shared-0.1.0.dist-info/RECORD +15 -0
- scmcp_shared-0.1.0.dist-info/WHEEL +4 -0
- scmcp_shared-0.1.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
from pydantic import (
|
3
|
+
BaseModel,
|
4
|
+
Field,
|
5
|
+
ValidationInfo,
|
6
|
+
computed_field,
|
7
|
+
field_validator,
|
8
|
+
model_validator,
|
9
|
+
)
|
10
|
+
from typing import Optional, Union, List, Dict, Any, Callable, Collection, Literal
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
class MarkVarModel(BaseModel):
|
15
|
+
"""Determine or mark if each gene meets specific conditions and store results in adata.var as boolean values"""
|
16
|
+
|
17
|
+
var_name: str = Field(
|
18
|
+
default=None,
|
19
|
+
description="Column name that will be added to adata.var, do not set if user does not ask"
|
20
|
+
)
|
21
|
+
pattern_type: Optional[Literal["startswith", "endswith", "contains"]] = Field(
|
22
|
+
default=None,
|
23
|
+
description="Pattern matching type (startswith/endswith/contains), it should be None when gene_class is not None"
|
24
|
+
)
|
25
|
+
patterns: Optional[str] = Field(
|
26
|
+
default=None,
|
27
|
+
description="gene pattern to match, must be a string, it should be None when gene_class is not None"
|
28
|
+
)
|
29
|
+
|
30
|
+
gene_class: Optional[Literal["mitochondrion", "ribosomal", "hemoglobin"]] = Field(
|
31
|
+
default=None,
|
32
|
+
description="Gene class type (Mitochondrion/Ribosomal/Hemoglobin)"
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
class ListVarModel(BaseModel):
|
37
|
+
"""ListVarModel"""
|
38
|
+
pass
|
39
|
+
|
40
|
+
class ListObsModel(BaseModel):
|
41
|
+
"""ListObsModel"""
|
42
|
+
pass
|
43
|
+
|
44
|
+
class VarNamesModel(BaseModel):
|
45
|
+
"""ListObsModel"""
|
46
|
+
var_names: List[str] = Field(
|
47
|
+
default=None,
|
48
|
+
description="gene names."
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
class ConcatAdataModel(BaseModel):
|
53
|
+
"""Model for concatenating AnnData objects"""
|
54
|
+
|
55
|
+
axis: Literal['obs', 0, 'var', 1] = Field(
|
56
|
+
default='obs',
|
57
|
+
description="Which axis to concatenate along. 'obs' or 0 for observations, 'var' or 1 for variables."
|
58
|
+
)
|
59
|
+
join: Literal['inner', 'outer'] = Field(
|
60
|
+
default='inner',
|
61
|
+
description="How to align values when concatenating. If 'outer', the union of the other axis is taken. If 'inner', the intersection."
|
62
|
+
)
|
63
|
+
merge: Optional[Literal['same', 'unique', 'first', 'only']] = Field(
|
64
|
+
default=None,
|
65
|
+
description="How elements not aligned to the axis being concatenated along are selected."
|
66
|
+
)
|
67
|
+
uns_merge: Optional[Literal['same', 'unique', 'first', 'only']] = Field(
|
68
|
+
default=None,
|
69
|
+
description="How the elements of .uns are selected. Uses the same set of strategies as the merge argument, except applied recursively."
|
70
|
+
)
|
71
|
+
label: Optional[str] = Field(
|
72
|
+
default=None,
|
73
|
+
description="label different adata, Column in axis annotation (i.e. .obs or .var) to place batch information in. "
|
74
|
+
)
|
75
|
+
keys: Optional[List[str]] = Field(
|
76
|
+
default=None,
|
77
|
+
description="Names for each object being added. These values are used for column values for label or appended to the index if index_unique is not None."
|
78
|
+
)
|
79
|
+
index_unique: Optional[str] = Field(
|
80
|
+
default=None,
|
81
|
+
description="Whether to make the index unique by using the keys. If provided, this is the delimiter between '{orig_idx}{index_unique}{key}'."
|
82
|
+
)
|
83
|
+
fill_value: Optional[Any] = Field(
|
84
|
+
default=None,
|
85
|
+
description="When join='outer', this is the value that will be used to fill the introduced indices."
|
86
|
+
)
|
87
|
+
pairwise: bool = Field(
|
88
|
+
default=False,
|
89
|
+
description="Whether pairwise elements along the concatenated dimension should be included."
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
class DPTIROOTModel(BaseModel):
|
94
|
+
"""Input schema for setting the root cell for diffusion pseudotime."""
|
95
|
+
diffmap_key: str = Field(
|
96
|
+
default="X_diffmap",
|
97
|
+
description="Key for diffusion map coordinates stored in adata.obsm."
|
98
|
+
)
|
99
|
+
dimension: int = Field(
|
100
|
+
description="Dimension index to use for finding the root cell."
|
101
|
+
)
|
102
|
+
direction: Literal["min", "max"] = Field(
|
103
|
+
description="use the minimum or maximum value along the selected dimension to identify the root cell."
|
104
|
+
)
|
105
|
+
|
106
|
+
|
107
|
+
class CelltypeMapCellTypeModel(BaseModel):
|
108
|
+
"""Input schema for mapping cluster IDs to cell type names."""
|
109
|
+
cluster_key: str = Field(
|
110
|
+
description="Key in adata.obs containing cluster IDs."
|
111
|
+
)
|
112
|
+
added_key: str = Field(
|
113
|
+
description="Key to add to adata.obs for cell type names."
|
114
|
+
)
|
115
|
+
mapping: Dict[str, str] = Field(
|
116
|
+
default=None,
|
117
|
+
description="Mapping Dictionary from cluster IDs to cell type names."
|
118
|
+
)
|
119
|
+
new_names: Optional[List[str]] = Field(
|
120
|
+
default=None,
|
121
|
+
description="a list of new cell type names."
|
122
|
+
)
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
class AddLayerModel(BaseModel):
|
127
|
+
"""Input schema for adding a layer to AnnData object."""
|
128
|
+
layer_name: str = Field(
|
129
|
+
description="Name of the layer to add to adata.layers."
|
130
|
+
)
|
131
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
from .io import io_mcp
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import os
|
2
|
+
import inspect
|
3
|
+
from pathlib import Path
|
4
|
+
import scanpy as sc
|
5
|
+
from fastmcp import FastMCP , Context
|
6
|
+
from ..schema.io import *
|
7
|
+
from ..util import filter_args, forward_request
|
8
|
+
|
9
|
+
|
10
|
+
io_mcp = FastMCP("SCMCP-IO-Server")
|
11
|
+
|
12
|
+
|
13
|
+
@io_mcp.tool()
|
14
|
+
async def read(
|
15
|
+
request: ReadModel,
|
16
|
+
ctx: Context,
|
17
|
+
sampleid: str = Field(default=None, description="adata sampleid"),
|
18
|
+
dtype: str = Field(default="exp", description="adata.X data type")
|
19
|
+
):
|
20
|
+
"""
|
21
|
+
Read data from various file formats (h5ad, 10x, text files, etc.) or directory path.
|
22
|
+
"""
|
23
|
+
try:
|
24
|
+
result = await forward_request("io_read", request, sampleid=sampleid, dtype=dtype)
|
25
|
+
if result is not None:
|
26
|
+
return result
|
27
|
+
kwargs = request.model_dump()
|
28
|
+
ads = ctx.request_context.lifespan_context
|
29
|
+
if sampleid is not None:
|
30
|
+
ads.active_id = sampleid
|
31
|
+
else:
|
32
|
+
ads.active_id = f"adata{len(ads.adata_dic[dtype])}"
|
33
|
+
|
34
|
+
file = Path(kwargs.get("filename", None))
|
35
|
+
if file.is_dir():
|
36
|
+
kwargs["path"] = kwargs["filename"]
|
37
|
+
func_kwargs = filter_args(request, sc.read_10x_mtx)
|
38
|
+
adata = sc.read_10x_mtx(kwargs["path"], **func_kwargs)
|
39
|
+
elif file.is_file():
|
40
|
+
func_kwargs = filter_args(request, sc.read)
|
41
|
+
adata = sc.read(**func_kwargs)
|
42
|
+
if not kwargs.get("first_column_obs", True):
|
43
|
+
adata = adata.T
|
44
|
+
else:
|
45
|
+
raise FileNotFoundError(f"{kwargs['filename']} does not exist")
|
46
|
+
adata.layers["counts"] = adata.X
|
47
|
+
adata.var_names_make_unique()
|
48
|
+
adata.obs_names_make_unique()
|
49
|
+
ads.set_adata(adata, sampleid=sampleid, sdtype=dtype)
|
50
|
+
return {"sampleid": sampleid or ads.active_id, "dtype": dtype, "adata": adata}
|
51
|
+
except Exception as e:
|
52
|
+
if hasattr(e, '__context__') and e.__context__:
|
53
|
+
raise Exception(f"{str(e.__context__)}")
|
54
|
+
else:
|
55
|
+
raise e
|
56
|
+
|
57
|
+
|
58
|
+
@io_mcp.tool()
|
59
|
+
async def write(
|
60
|
+
request: WriteModel,
|
61
|
+
ctx: Context,
|
62
|
+
sampleid: str = Field(default=None, description="adata sampleid"),
|
63
|
+
dtype: str = Field(default="exp", description="adata.X data type")
|
64
|
+
):
|
65
|
+
"""save adata into a file.
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
result = await forward_request("io_write", request, sampleid=sampleid, dtype=dtype)
|
69
|
+
if result is not None:
|
70
|
+
return result
|
71
|
+
ads = ctx.request_context.lifespan_context
|
72
|
+
adata = ads.get_adata(sampleid=sampleid, dtype=dtype)
|
73
|
+
kwargs = request.model_dump()
|
74
|
+
sc.write(kwargs["filename"], adata)
|
75
|
+
return {"filename": kwargs["filename"], "msg": "success to save file"}
|
76
|
+
except Exception as e:
|
77
|
+
if hasattr(e, '__context__') and e.__context__:
|
78
|
+
raise Exception(f"{str(e.__context__)}")
|
79
|
+
else:
|
80
|
+
raise e
|
scmcp_shared/util.py
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
import inspect
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
def get_env(key):
|
8
|
+
return os.environ.get(f"SCMCP_{key.upper()}")
|
9
|
+
|
10
|
+
|
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
|
13
|
+
kwargs = request.model_dump()
|
14
|
+
args = request.model_fields_set
|
15
|
+
parameters = inspect.signature(func).parameters
|
16
|
+
func_kwargs = {k: kwargs.get(k) for k in args if k in parameters}
|
17
|
+
return func_kwargs
|
18
|
+
|
19
|
+
|
20
|
+
def add_op_log(adata, func, kwargs):
|
21
|
+
import hashlib
|
22
|
+
import json
|
23
|
+
|
24
|
+
if "operation" not in adata.uns:
|
25
|
+
adata.uns["operation"] = {}
|
26
|
+
adata.uns["operation"]["op"] = {}
|
27
|
+
adata.uns["operation"]["opid"] = []
|
28
|
+
# Handle different function types to get the function name
|
29
|
+
if hasattr(func, "func") and hasattr(func.func, "__name__"):
|
30
|
+
# For partial functions, use the original function name
|
31
|
+
func_name = func.func.__name__
|
32
|
+
elif hasattr(func, "__name__"):
|
33
|
+
func_name = func.__name__
|
34
|
+
elif hasattr(func, "__class__"):
|
35
|
+
func_name = func.__class__.__name__
|
36
|
+
else:
|
37
|
+
func_name = str(func)
|
38
|
+
new_kwargs = {}
|
39
|
+
for k,v in kwargs.items():
|
40
|
+
if isinstance(v, tuple):
|
41
|
+
new_kwargs[k] = list(v)
|
42
|
+
else:
|
43
|
+
new_kwargs[k] = v
|
44
|
+
try:
|
45
|
+
kwargs_str = json.dumps(new_kwargs, sort_keys=True)
|
46
|
+
except:
|
47
|
+
kwargs_str = str(new_kwargs)
|
48
|
+
hash_input = f"{func_name}:{kwargs_str}"
|
49
|
+
hash_key = hashlib.md5(hash_input.encode()).hexdigest()
|
50
|
+
adata.uns["operation"]["op"][hash_key] = {func_name: new_kwargs}
|
51
|
+
adata.uns["operation"]["opid"].append(hash_key)
|
52
|
+
from .logging_config import setup_logger
|
53
|
+
logger = setup_logger(log_file=get_env("LOG_FILE"))
|
54
|
+
logger.info(f"{func}: {new_kwargs}")
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
def savefig(fig, file):
|
59
|
+
try:
|
60
|
+
file_path = Path(file)
|
61
|
+
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)
|
68
|
+
return file_path
|
69
|
+
except Exception as e:
|
70
|
+
raise e
|
71
|
+
|
72
|
+
|
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"
|
76
|
+
|
77
|
+
kwargs.pop("save", None)
|
78
|
+
kwargs.pop("show", None)
|
79
|
+
args = []
|
80
|
+
for k,v in kwargs.items():
|
81
|
+
if isinstance(v, (tuple, list, set)):
|
82
|
+
args.append(f"{k}-{'-'.join([str(i) for i in v])}")
|
83
|
+
else:
|
84
|
+
args.append(f"{k}-{v}")
|
85
|
+
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
|
+
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")
|
118
|
+
except PermissionError:
|
119
|
+
print("You don't have permission to rename this file")
|
120
|
+
transport = get_env("TRANSPORT")
|
121
|
+
if transport == "stdio":
|
122
|
+
return fig_path
|
123
|
+
else:
|
124
|
+
host = get_env("HOST")
|
125
|
+
port = get_env("PORT")
|
126
|
+
fig_path = f"http://{host}:{port}/figures/{Path(fig_path).name}"
|
127
|
+
return fig_path
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
async def get_figure(request):
|
132
|
+
from starlette.responses import FileResponse, Response
|
133
|
+
|
134
|
+
figure_name = request.path_params["figure_name"]
|
135
|
+
figure_path = f"./figures/{figure_name}"
|
136
|
+
|
137
|
+
# 检查文件是否存在
|
138
|
+
if not os.path.isfile(figure_path):
|
139
|
+
return Response(content={"error": "figure not found"}, media_type="application/json")
|
140
|
+
|
141
|
+
return FileResponse(figure_path)
|
142
|
+
|
143
|
+
|
144
|
+
async def forward_request(func, request, **kwargs):
|
145
|
+
from fastmcp import Client
|
146
|
+
forward_url = get_env("FORWARD")
|
147
|
+
request_kwargs = request.model_dump()
|
148
|
+
request_args = request.model_fields_set
|
149
|
+
func_kwargs = {"request": {k: request_kwargs.get(k) for k in request_args}}
|
150
|
+
func_kwargs.update({k:v for k,v in kwargs.items() if v is not None})
|
151
|
+
if not forward_url:
|
152
|
+
return None
|
153
|
+
|
154
|
+
client = Client(forward_url)
|
155
|
+
async with client:
|
156
|
+
tools = await client.list_tools()
|
157
|
+
func = [t.name for t in tools if t.name.endswith(func)][0]
|
158
|
+
try:
|
159
|
+
result = await client.call_tool(func, func_kwargs)
|
160
|
+
return result
|
161
|
+
except Exception as e:
|
162
|
+
raise e
|
163
|
+
|
164
|
+
def obsm2adata(adata, obsm_key):
|
165
|
+
from anndata import AnnData
|
166
|
+
|
167
|
+
if obsm_key not in adata.obsm_keys():
|
168
|
+
raise ValueError(f"key {obsm_key} not found in adata.obsm")
|
169
|
+
else:
|
170
|
+
return AnnData(adata.obsm[obsm_key], obs=adata.obs, obsm=adata.obsm)
|
171
|
+
|
172
|
+
|
173
|
+
async def get_figure(request):
|
174
|
+
figure_name = request.path_params["figure_name"]
|
175
|
+
figure_path = f"./figures/{figure_name}"
|
176
|
+
|
177
|
+
# 检查文件是否存在
|
178
|
+
if not os.path.isfile(figure_path):
|
179
|
+
return Response(content={"error": "figure not found"}, media_type="application/json")
|
180
|
+
|
181
|
+
return FileResponse(figure_path)
|
182
|
+
|
183
|
+
|
184
|
+
def add_figure_route(server):
|
185
|
+
from starlette.routing import Route
|
186
|
+
server._additional_http_routes = [Route("/figures/{figure_name}", endpoint=get_figure)]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: scmcp_shared
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A shared function libray for scmcphub
|
5
|
+
Author-email: shuang <hsh-me@outlook.com>
|
6
|
+
License: BSD 3-Clause License
|
7
|
+
|
8
|
+
Copyright (c) 2025, scmcphub
|
9
|
+
|
10
|
+
Redistribution and use in source and binary forms, with or without
|
11
|
+
modification, are permitted provided that the following conditions are met:
|
12
|
+
|
13
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
14
|
+
list of conditions and the following disclaimer.
|
15
|
+
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
18
|
+
and/or other materials provided with the distribution.
|
19
|
+
|
20
|
+
3. Neither the name of the copyright holder nor the names of its
|
21
|
+
contributors may be used to endorse or promote products derived from
|
22
|
+
this software without specific prior written permission.
|
23
|
+
|
24
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
25
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
26
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
27
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
28
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
29
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
30
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
31
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
32
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
33
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
License-File: LICENSE
|
35
|
+
Requires-Python: >=3.10
|
36
|
+
Requires-Dist: fastmcp>=2.3.0
|
37
|
+
Requires-Dist: mcp>=1.8.0
|
38
|
+
Requires-Dist: pydantic
|
39
|
+
Requires-Dist: scanpy
|
40
|
+
Description-Content-Type: text/markdown
|
41
|
+
|
42
|
+
# scmcp-shared
|
43
|
+
|
44
|
+
Shared functions for scmcphub
|
@@ -0,0 +1,15 @@
|
|
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,,
|
@@ -0,0 +1,28 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2025, scmcphub
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
13
|
+
and/or other materials provided with the distribution.
|
14
|
+
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
16
|
+
contributors may be used to endorse or promote products derived from
|
17
|
+
this software without specific prior written permission.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|