scmcp-shared 0.1.0__tar.gz
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-0.1.0/.github/workflows/publish.yml +49 -0
- scmcp_shared-0.1.0/LICENSE +28 -0
- scmcp_shared-0.1.0/PKG-INFO +44 -0
- scmcp_shared-0.1.0/README.md +3 -0
- scmcp_shared-0.1.0/pyproject.toml +33 -0
- scmcp_shared-0.1.0/src/scmcp_shared/__init__.py +3 -0
- scmcp_shared-0.1.0/src/scmcp_shared/logging_config.py +31 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/__init__.py +1 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/io.py +120 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/pl.py +948 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/pp.py +707 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/tl.py +902 -0
- scmcp_shared-0.1.0/src/scmcp_shared/schema/util.py +131 -0
- scmcp_shared-0.1.0/src/scmcp_shared/server/__init__.py +1 -0
- scmcp_shared-0.1.0/src/scmcp_shared/server/io.py +80 -0
- scmcp_shared-0.1.0/src/scmcp_shared/util.py +186 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
name: Publishing
|
2
|
+
on:
|
3
|
+
release:
|
4
|
+
types: [published]
|
5
|
+
|
6
|
+
jobs:
|
7
|
+
release-build:
|
8
|
+
name: Build distribution
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
steps:
|
11
|
+
- uses: actions/checkout@v4
|
12
|
+
|
13
|
+
- name: Install uv
|
14
|
+
uses: astral-sh/setup-uv@v5
|
15
|
+
with:
|
16
|
+
enable-cache: true
|
17
|
+
|
18
|
+
- name: "Set up Python"
|
19
|
+
uses: actions/setup-python@v5
|
20
|
+
with:
|
21
|
+
python-version-file: "pyproject.toml"
|
22
|
+
|
23
|
+
- name: Build
|
24
|
+
run: uv build
|
25
|
+
|
26
|
+
- name: Upload artifacts
|
27
|
+
uses: actions/upload-artifact@v4
|
28
|
+
with:
|
29
|
+
name: release-dists
|
30
|
+
path: dist/
|
31
|
+
|
32
|
+
pypi-publish:
|
33
|
+
name: Upload release to PyPI
|
34
|
+
runs-on: ubuntu-latest
|
35
|
+
environment: pypi
|
36
|
+
needs:
|
37
|
+
- release-build
|
38
|
+
permissions:
|
39
|
+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
40
|
+
|
41
|
+
steps:
|
42
|
+
- name: Retrieve release distributions
|
43
|
+
uses: actions/download-artifact@v4
|
44
|
+
with:
|
45
|
+
name: release-dists
|
46
|
+
path: dist/
|
47
|
+
|
48
|
+
- name: Publish package distributions to PyPI
|
49
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
@@ -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.
|
@@ -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,33 @@
|
|
1
|
+
[project]
|
2
|
+
name = "scmcp_shared"
|
3
|
+
dynamic = ["version"]
|
4
|
+
description = "A shared function libray for scmcphub"
|
5
|
+
readme = "README.md"
|
6
|
+
license = { file = "LICENSE" }
|
7
|
+
authors = [
|
8
|
+
{ name = "shuang", email = "hsh-me@outlook.com" }
|
9
|
+
]
|
10
|
+
requires-python = ">=3.10"
|
11
|
+
dependencies = [
|
12
|
+
"scanpy",
|
13
|
+
"mcp>=1.8.0",
|
14
|
+
"fastmcp>=2.3.0",
|
15
|
+
"pydantic",
|
16
|
+
]
|
17
|
+
|
18
|
+
[build-system]
|
19
|
+
requires = ["hatchling"]
|
20
|
+
build-backend = "hatchling.build"
|
21
|
+
|
22
|
+
|
23
|
+
[dependency-groups]
|
24
|
+
dev = [
|
25
|
+
"pytest>=8.3.4",
|
26
|
+
]
|
27
|
+
|
28
|
+
[tool.hatch.version]
|
29
|
+
path = "src/scmcp_shared/__init__.py"
|
30
|
+
|
31
|
+
[tool.pytest.ini_options]
|
32
|
+
asyncio_mode = "strict"
|
33
|
+
asyncio_default_fixture_loop_scope = "function"
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import logging
|
2
|
+
import sys
|
3
|
+
import os
|
4
|
+
from .util import get_env
|
5
|
+
|
6
|
+
def setup_logger(name="sc-mcp-server", log_file=None):
|
7
|
+
|
8
|
+
logger = logging.getLogger(name)
|
9
|
+
logger.setLevel(logging.INFO)
|
10
|
+
if logger.handlers:
|
11
|
+
return logger
|
12
|
+
|
13
|
+
formatter = logging.Formatter(
|
14
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
15
|
+
'%Y-%m-%d %H:%M:%S'
|
16
|
+
)
|
17
|
+
if log_file is None:
|
18
|
+
log_file = get_env("LOG_FILE")
|
19
|
+
if log_file:
|
20
|
+
log_handler = logging.FileHandler(log_file)
|
21
|
+
log_handler.setFormatter(formatter)
|
22
|
+
logger.addHandler(log_handler)
|
23
|
+
|
24
|
+
logger.info(f"logging output: {log_file}")
|
25
|
+
else:
|
26
|
+
log_handler = logging.StreamHandler(sys.stdout)
|
27
|
+
log_handler.setFormatter(formatter)
|
28
|
+
logger.addHandler(log_handler)
|
29
|
+
logger.info(f"loggin file output: stdout")
|
30
|
+
return logger
|
31
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
from .io import *
|
@@ -0,0 +1,120 @@
|
|
1
|
+
from pydantic import (
|
2
|
+
Field,
|
3
|
+
ValidationInfo,
|
4
|
+
computed_field,
|
5
|
+
field_validator,
|
6
|
+
model_validator,
|
7
|
+
BaseModel
|
8
|
+
)
|
9
|
+
from typing import Optional, Union, Literal, Any, Sequence, Dict
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
class ReadModel(BaseModel):
|
14
|
+
"""Input schema for the read tool."""
|
15
|
+
filename: str = Field(
|
16
|
+
...,
|
17
|
+
description="Path to the file to read."
|
18
|
+
)
|
19
|
+
|
20
|
+
backed: Literal['r', 'r+'] = Field(
|
21
|
+
default=None,
|
22
|
+
description="If 'r', load AnnData in 'backed' mode instead of fully loading it into memory ('memory' mode). If you want to modify backed attributes of the AnnData object, you need to choose 'r+'."
|
23
|
+
)
|
24
|
+
sheet: str = Field(
|
25
|
+
default=None,
|
26
|
+
description="Name of sheet/table in hdf5 or Excel file."
|
27
|
+
)
|
28
|
+
ext: str = Field(
|
29
|
+
default=None,
|
30
|
+
description="Extension that indicates the file type. If None, uses extension of filename."
|
31
|
+
)
|
32
|
+
delimiter: str = Field(
|
33
|
+
default=None,
|
34
|
+
description="Delimiter that separates data within text file. If None, will split at arbitrary number of white spaces, which is different from enforcing splitting at any single white space."
|
35
|
+
)
|
36
|
+
first_column_names: bool = Field(
|
37
|
+
default=False,
|
38
|
+
description="Assume the first column stores row names. This is only necessary if these are not strings: strings in the first column are automatically assumed to be row names."
|
39
|
+
)
|
40
|
+
first_column_obs: bool = Field(
|
41
|
+
default=True,
|
42
|
+
description="If True, assume the first column stores observations (cell or barcode) names when provide text file. If False, the data will be transposed."
|
43
|
+
)
|
44
|
+
backup_url: str = Field(
|
45
|
+
default=None,
|
46
|
+
description="Retrieve the file from an URL if not present on disk."
|
47
|
+
)
|
48
|
+
cache: bool = Field(
|
49
|
+
default=False,
|
50
|
+
description="If False, read from source, if True, read from fast 'h5ad' cache."
|
51
|
+
)
|
52
|
+
cache_compression: Literal['gzip', 'lzf'] = Field(
|
53
|
+
default=None,
|
54
|
+
description="See the h5py dataset_compression. (Default: settings.cache_compression)"
|
55
|
+
)
|
56
|
+
var_names: str = Field(
|
57
|
+
default="gene_symbols",
|
58
|
+
description="The variables index for 10x mtx format. Either 'gene_symbols' or 'gene_ids'."
|
59
|
+
)
|
60
|
+
make_unique: bool = Field(
|
61
|
+
default=True,
|
62
|
+
description="Whether to make the variables index unique by appending '-1', '-2' etc. or not. Used for 10x mtx format."
|
63
|
+
)
|
64
|
+
gex_only: bool = Field(
|
65
|
+
default=True,
|
66
|
+
description="Only keep 'Gene Expression' data and ignore other feature types, e.g. 'Antibody Capture', 'CRISPR Guide Capture', or 'Custom'. Used for 10x formats."
|
67
|
+
)
|
68
|
+
prefix: str = Field(
|
69
|
+
default=None,
|
70
|
+
description="Any prefix before matrix.mtx, genes.tsv and barcodes.tsv. For instance, if the files are named patientA_matrix.mtx, patientA_genes.tsv and patientA_barcodes.tsv the prefix is patientA_. Used for 10x mtx format."
|
71
|
+
)
|
72
|
+
|
73
|
+
@field_validator('backed')
|
74
|
+
def validate_backed(cls, v: Optional[str]) -> Optional[str]:
|
75
|
+
if v is not None and v not in ['r', 'r+']:
|
76
|
+
raise ValueError("If backed is provided, it must be either 'r' or 'r+'")
|
77
|
+
return v
|
78
|
+
|
79
|
+
@field_validator('cache_compression')
|
80
|
+
def validate_cache_compression(cls, v: Optional[str]) -> Optional[str]:
|
81
|
+
if v is not None and v not in ['gzip', 'lzf']:
|
82
|
+
raise ValueError("cache_compression must be either 'gzip', 'lzf', or None")
|
83
|
+
return v
|
84
|
+
|
85
|
+
@field_validator('var_names')
|
86
|
+
def validate_var_names(cls, v: Optional[str]) -> Optional[str]:
|
87
|
+
if v is not None and v not in ['gene_symbols', 'gene_ids']:
|
88
|
+
raise ValueError("var_names must be either 'gene_symbols' or 'gene_ids'")
|
89
|
+
return v
|
90
|
+
|
91
|
+
|
92
|
+
class WriteModel(BaseModel):
|
93
|
+
"""Input schema for the write tool."""
|
94
|
+
filename: str = Field(
|
95
|
+
description="Path to save the file. If no extension is provided, the default format will be used."
|
96
|
+
)
|
97
|
+
ext: Literal['h5', 'csv', 'txt', 'npz'] = Field(
|
98
|
+
default=None,
|
99
|
+
description="File extension to infer file format. If None, defaults to scanpy's settings.file_format_data."
|
100
|
+
)
|
101
|
+
compression: Literal['gzip', 'lzf'] = Field(
|
102
|
+
default='gzip',
|
103
|
+
description="Compression format for h5 files."
|
104
|
+
)
|
105
|
+
compression_opts: int = Field(
|
106
|
+
default=None,
|
107
|
+
description="Compression options for h5 files."
|
108
|
+
)
|
109
|
+
|
110
|
+
@field_validator('filename')
|
111
|
+
def validate_filename(cls, v: str) -> str:
|
112
|
+
# Allow any filename since the extension is optional and can be inferred
|
113
|
+
return v
|
114
|
+
|
115
|
+
@model_validator(mode='after')
|
116
|
+
def validate_extension_compression(self) -> 'WriteInput':
|
117
|
+
# If ext is provided and not h5, compression should be None
|
118
|
+
if self.ext is not None and self.ext != 'h5' and self.compression is not None:
|
119
|
+
raise ValueError("Compression can only be used with h5 files")
|
120
|
+
return self
|