Skip to content

Commit 5c5aace

Browse files
committed
workflow to publish to pip, ruff linting, license and readme
1 parent 469fdcf commit 5c5aace

File tree

8 files changed

+212
-20
lines changed

8 files changed

+212
-20
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Build and Publish Package
2+
on:
3+
release:
4+
types: [created]
5+
workflow_dispatch: # Allow manual triggering
6+
7+
jobs:
8+
build-and-publish:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
fetch-depth: 0 # Get all tags for proper versioning
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.12'
19+
20+
- name: Install uv
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install uv
24+
25+
- name: Build package
26+
run: |
27+
uv build
28+
29+
- name: Publish to PyPI
30+
if: github.event_name == 'release'
31+
run: |
32+
uv publish
33+
env:
34+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
35+
36+
- name: Publish to TestPyPI
37+
if: github.event_name == 'workflow_dispatch'
38+
run: |
39+
uv publish --index testpypi
40+
41+
env:
42+
UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: CI
22

33
on:
44
push:
5-
branches: [master, dev]
65
pull_request:
76
branches: [main]
87

README.md

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,142 @@
1-
A utility to convert anndata files into loupe files visualizable in 10xs loupe browser.
1+
from loupepy import get_count_matrixWhile this tool is independently maintained and not officially supported by 10x Genomics, it relies on code from [LoupeR](https://github.com/10xGenomics/loupeR), an R package developed by 10x Genomics. LoupeR is licensed for use only in connection with data generated from 10x Genomics products. For more information, please refer to the [10x End User Software License](https://www.10xgenomics.com/legal/end-user-software-license-agreement). By using the setup function of this tool, you will need to agree to this license with an interactive prompt.
22

3-
This package widely mirrors the behavior of the R package, with some minor differences:
43

5-
- Cell based annotations are automatically filtered out if they contain more than the max number of categories.
6-
(This was an issue in R when converting from anndata).
7-
- Easier to pass specific embeddings to the function, which is especially useful when using scVI.
8-
9-
Note this package requires an agreement to the 10x loupe converter license, which can be agreed to and downloaded by
10-
running the setup function.
11-
**I am not an employee of 10x, nor associated with them**. This package simply writes a file which the converter can read
12-
and calls it.
4+
## Main Functions
135

14-
# This is still not done and a work in progress #
6+
### `create_loupe_from_anndata(anndata, output_cloupe="cloupe.cloupe", layer=None, ...)`
7+
8+
Creates a Loupe Browser compatible `.cloupe` file from an `AnnData` object.
9+
10+
**Parameters:**
11+
- `anndata`: AnnData object containing single-cell data
12+
- `output_cloupe`: Path to save the output file (default: "cloupe.cloupe")
13+
- `layer`: Layer in AnnData to use for counts (default: None, uses `.X`)
14+
- `dims`: List of dimension reduction keys from `adata.obsm` (default: None, uses all valid projections)
15+
- `obs_keys`: List of categorical annotations from `adata.obs` to include (default: None, uses all valid categories)
16+
- `strict_checking`: Whether to strictly validate inputs (default: False)
17+
- `tmp_file`: Temporary file path for intermediate data (default: 'tmp.h5')
18+
- `dims`: List of dimension reduction keys from `adata.obsm` to include (default: None, uses all valid projections)
19+
- `obs_keys`: List of categorical annotations from `adata.obs` to include (default: None, uses all valid categories)
20+
- `loupe_converter_path`: Path to the Loupe converter binary (default: None, uses default installation path)
21+
- `clean_tmp_file`: Whether to delete the temporary file after creation (default: True)
22+
- `force`: Whether to overwrite existing cloupe file (default: False)
23+
- `test_mode`: Whether to run in test mode. Will not create a cloupe file.
24+
25+
**Example:**
26+
```python
27+
import scanpy as sc
28+
import loupepy
29+
from scipy.sparse import diags
30+
31+
# Load data
32+
adata = sc.datasets.pbmc3k_processed().raw.to_adata()
33+
# Revert to raw counts, as loupee converter only takes raw counts
34+
n_counts = adata.obs["n_counts"].values
35+
counts_matrix = adata.X.expm1()
36+
counts = diags(n_counts) * counts_matrix
37+
adata.X = counts
38+
39+
# Create Loupe file with default settings
40+
loupepy.create_loupe_from_anndata(adata, "my_results.cloupe")
41+
42+
# Create with specific dimensions and annotations
43+
loupepy.create_loupe_from_anndata(
44+
adata,
45+
"my_custom_results.cloupe",
46+
dims=["X_umap"],
47+
obs_keys=["leiden"]
48+
)
49+
```
50+
## Utility Functions
51+
52+
### `get_obs(anndata, obs_keys=None, strict=False)`
53+
54+
Extracts categorical observation data from an AnnData object.
55+
56+
**Parameters:**
57+
- `anndata`: AnnData object containing the data
58+
- `obs_keys`: List of keys to extract from `adata.obs` (default: None, uses all valid categories)
59+
- `strict`: Whether to strictly validate the keys. If true will throw errors on invalid keys instead of dropping
60+
(default: False)
61+
62+
### `get_obsm(anndata, obsm_keys=None, strict=False)`
63+
64+
Extracts dimensional reduction data from an AnnData object.
65+
**Parameters:**
66+
- `anndata`: AnnData object containing the data
67+
- `obsm_keys`: List of keys to extract from `adata.obsm` (default: None, uses all valid dimension reductions)
68+
- `strict`: Whether to strictly validate the keys. If true will throw errors on invalid keys instead of dropping
69+
(default: False)
70+
### `get_count_matrix(anndata, layer=None)`
71+
72+
Gets the count matrix from an AnnData object in the format required for Loupe.
73+
74+
**Parameters:**
75+
- `anndata`: AnnData object containing the data
76+
- `layer`: Layer in AnnData to use for counts (default: None, uses `.X`)
77+
78+
---
79+
80+
### `create_loupe(mat, obs, var, obsm, tmp_file, ...)`
81+
82+
Lower-level function to create a Loupe file from raw data components.
83+
84+
**Parameters:**
85+
- `mat`: CSC matrix of counts (shape: n_features × n_cells)
86+
- `obs`: DataFrame of categorical cell annotations
87+
- `var`: DataFrame of gene/feature information
88+
- `obsm`: Dictionary of dimension reduction embeddings
89+
- `tmp_file`: Path for temporary file
90+
- `output_path`: Path to save the output file
91+
- `strict_checking`: Whether to strictly validate inputs (default: False)
92+
- `loupe_converter_path`: Path to the Loupe converter binary (default: None, uses default installation path)'
93+
- `clean_tmp_file`: Whether to delete the temporary file after creation (default: True)
94+
- `force`: Whether to overwrite existing cloupe file (default: False)
95+
- `test_mode`: Whether to run in test mode. Will not create a cloupe file.
96+
97+
**Example:**
98+
```python
99+
import scanpy as sc
100+
import loupepy
101+
from scipy.sparse import diags
102+
103+
# Load data
104+
adata = sc.datasets.pbmc3k_processed().raw.to_adata()
105+
# Revert to raw counts, as loupee converter only takes raw counts
106+
n_counts = adata.obs["n_counts"].values
107+
counts_matrix = adata.X.expm1()
108+
counts = diags(n_counts) * counts_matrix
109+
adata.X = counts
110+
111+
mat = loupepy.get_count_matrix(adata)
112+
obs = loupepy.get_obs(adata, obs_keys=["leiden"])
113+
var = adata.var
114+
obsm = loupepy.get_obsm(adata, obsm_keys=["X_umap"])
115+
loupepy.create_loupe(mat,obs,var,obsm,"my_results.cloupe")
116+
```
117+
118+
## Setup Functions
119+
120+
### `setup(path=None)`
121+
122+
Downloads and installs the Loupe converter binary.
123+
124+
**Parameters:**
125+
- `path`: Installation directory path (default: None, uses OS-specific default location)
126+
127+
**Example:**
128+
```python
129+
import loupepy
130+
131+
# Install the Loupe converter to the default location
132+
loupepy.setup()
133+
```
134+
135+
### `eula(path=None)`
136+
137+
**Parameters:**
138+
- `path`: Installation path (default: None, uses OS-specific default install location. Usually same path as setup)
139+
140+
### `eula_reset(path=None)`
141+
142+
Resets the EULA agreement and removes the installed Loupe converter binary.

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,10 @@ dev = [
4141
"ruff>=0.11.5",
4242
"scanpy>=1.11.1",
4343
"scipy-stubs>=1.15.2.1",
44-
]
44+
]
45+
46+
[[tool.uv.index]]
47+
name = "testpypi"
48+
url = "https://test.pypi.org/simple/"
49+
publish-url = "https://test.pypi.org/legacy/"
50+
explicit = true

src/loupepy/convert.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import os
22
from os import PathLike
33

4-
import numpy as np
54
from anndata import AnnData # type: ignore
65
import pandas as pd
76
from scipy.sparse import csc_matrix
87
import h5py # type: ignore
98
from typing import List
10-
from array import array
119
import logging
1210
from numpy import ndarray
1311
from .utils import _validate_anndata, _get_loupe_path, get_obs, get_obsm, get_count_matrix

src/loupepy/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pandas import Series, DataFrame
2-
from typing import Union
2+
from typing import Union, List
33
from numpy.typing import ArrayLike
44
from anndata import AnnData # type: ignore
55
import numpy as np
@@ -141,7 +141,7 @@ def get_count_matrix(anndata: AnnData, layer: str | None = None) -> csc_matrix:
141141
else:
142142
return csc_matrix(anndata.X)
143143

144-
def get_obs(anndata: AnnData, obs_keys: str|None = None, strict: bool = False) -> DataFrame:
144+
def get_obs(anndata: AnnData, obs_keys: List[str]|None = None, strict: bool = False) -> DataFrame:
145145
"""
146146
Get the obs dataframe from an AnnData object in the format for loupe converter.
147147
Args:
@@ -157,7 +157,7 @@ def get_obs(anndata: AnnData, obs_keys: str|None = None, strict: bool = False) -
157157
_validate_obs(obs, strict)
158158
return obs
159159

160-
def get_obsm(anndata: AnnData, obsm_keys: str | None = None, strict: bool = False) -> dict[str, ndarray]:
160+
def get_obsm(anndata: AnnData, obsm_keys: List[str] | None = None, strict: bool = False) -> dict[str, ndarray]:
161161
"""
162162
Get the obsm dictionary from an AnnData object in the format for loupe converter.
163163
Args:

tests/LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2018 The Python Packaging Authority
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.
20+

tests/test_setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import pytest
2-
from loupepy.setup import eula_reset, eula_reset, setup # type: ignore
1+
from loupepy.setup import eula_reset, setup # type: ignore
32
import os
43

54
def test_eula_and_reset(monkeypatch, tmp_path_factory):

0 commit comments

Comments
 (0)