makoview 0.1.2__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.
@@ -0,0 +1,75 @@
1
+ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution 📦
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v6
12
+ with:
13
+ persist-credentials: false
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v7
17
+
18
+ - name: Install Python 3.13
19
+ run: uv python install 3.13
20
+
21
+ - name: Build
22
+ run: uv build
23
+
24
+ - name: Store the distribution packages
25
+ uses: actions/upload-artifact@v5
26
+ with:
27
+ name: python-package-distributions
28
+ path: dist/
29
+
30
+ publish-to-pypi:
31
+ name: >-
32
+ Publish Python 🐍 distribution 📦 to PyPI
33
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
34
+ needs:
35
+ - build
36
+ runs-on: ubuntu-latest
37
+ environment:
38
+ name: pypi
39
+ url: https://pypi.org/p/makoview # Replace <package-name> with your PyPI project name
40
+ permissions:
41
+ id-token: write # IMPORTANT: mandatory for trusted publishing
42
+
43
+ steps:
44
+ - name: Download all the dists
45
+ uses: actions/download-artifact@v6
46
+ with:
47
+ name: python-package-distributions
48
+ path: dist/
49
+ - name: Publish distribution 📦 to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
51
+
52
+ publish-to-testpypi:
53
+ name: Publish Python 🐍 distribution 📦 to TestPyPI
54
+ needs:
55
+ - build
56
+ runs-on: ubuntu-latest
57
+
58
+ environment:
59
+ name: testpypi
60
+ url: https://test.pypi.org/p/makoview
61
+
62
+ permissions:
63
+ id-token: write # IMPORTANT: mandatory for trusted publishing
64
+
65
+ steps:
66
+ - name: Download all the dists
67
+ uses: actions/download-artifact@v6
68
+ with:
69
+ name: python-package-distributions
70
+ path: dist/
71
+ - name: Publish distribution 📦 to TestPyPI
72
+ uses: pypa/gh-action-pypi-publish@release/v1
73
+ with:
74
+ repository-url: https://test.pypi.org/legacy/
75
+ verbose: true
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
217
+
218
+ # DuckDB database files used in testing
219
+ **/*.duckdb
220
+ **/.DS_Store
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: makoview
3
+ Version: 0.1.2
4
+ Summary: Visualise the results from Mako
5
+ Requires-Python: <3.13,>=3.9
6
+ Requires-Dist: duckdb>=1.0.0
7
+ Requires-Dist: matplotlib>=3.9.4
8
+ Requires-Dist: pandas>=2.3.3
9
+ Requires-Dist: seaborn>=0.13.2
10
+ Requires-Dist: shiny>=0.10.0
11
+ Requires-Dist: shinyswatch>=0.9.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # makoview
@@ -0,0 +1 @@
1
+ # makoview
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "makoview"
3
+ version = "0.1.2"
4
+ description = "Visualise the results from Mako"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9,<3.13"
7
+ dependencies = [
8
+ "shiny>=0.10.0",
9
+ "duckdb>=1.0.0",
10
+ "pandas>=2.3.3",
11
+ "matplotlib>=3.9.4",
12
+ "seaborn>=0.13.2",
13
+ "shinyswatch>=0.9.0",
14
+ ]
15
+
16
+ [build-system]
17
+ build-backend = 'hatchling.build'
18
+ requires = ['hatchling']
19
+
20
+
21
+ [tool.uv_build]
22
+ src-layout = true
23
+
24
+
25
+ [tool.uv]
26
+ dev-dependencies = []
27
+ package = true
28
+
29
+
30
+ [project.scripts]
31
+ makoview = "makoview.wrapper:main"
File without changes
@@ -0,0 +1,179 @@
1
+ from shiny.types import SilentException
2
+ from shiny.express import input, render, ui
3
+ from shiny import reactive
4
+ import duckdb
5
+ import os
6
+ import pandas as pd
7
+ from pathlib import Path
8
+
9
+ import shinyswatch
10
+
11
+ import plots
12
+
13
+ _db_path = os.environ["MAKO_DIFFERENTIAL_DB"]
14
+ _reads_path = os.environ["MAKO_MODIFICATION_DB"]
15
+
16
+ # Validate that database files exist
17
+ if not os.path.exists(_db_path):
18
+ raise FileNotFoundError(f"Differential sites database not found: {_db_path}")
19
+ if not os.path.exists(_reads_path):
20
+ raise FileNotFoundError(f"Modification database not found: {_reads_path}")
21
+
22
+
23
+ # load from db into a Pandas dataframe in-memory for faster access
24
+ def load_data_into_memory(db_path) -> pd.DataFrame:
25
+ conn = duckdb.connect(database=db_path, read_only=True)
26
+ df = conn.execute("SELECT * FROM sites").fetchdf()
27
+ conn.close()
28
+ return df
29
+
30
+
31
+ df = load_data_into_memory(_db_path)
32
+
33
+
34
+ # UI Definition
35
+ ui.page_opts(title="makoview", theme=shinyswatch.theme.lumen)
36
+ ui.include_css(Path(__file__).parent / "styles.css")
37
+ # ui.page_opts(title="Mako modification lookup", fillable=True)
38
+
39
+ read_cache: reactive.Value[pd.DataFrame] = reactive.value()
40
+
41
+
42
+ with ui.sidebar(width="400px", open="always"):
43
+ with ui.card(fill=False):
44
+ ui.card_header("Search for a transcript...")
45
+ # with ui.layout_columns(col_widths=(8, 4), gap="0.75rem", row_heights="auto"):
46
+ ui.input_text(
47
+ "transcript_id",
48
+ "Transcript ID",
49
+ placeholder="e.g. ENST00000000233.10",
50
+ width="100%",
51
+ )
52
+
53
+ ui.input_selectize(
54
+ "transcript_position",
55
+ "Transcript Position",
56
+ choices=[],
57
+ multiple=False,
58
+ options={
59
+ "placeholder": "transcript not found...",
60
+ "dropdownParent": "body",
61
+ },
62
+ width="100%",
63
+ )
64
+ ui.input_action_button(
65
+ "search_btn", "Search", class_="btn-primary", style="margin-top: 0.5rem;"
66
+ )
67
+
68
+ @render.data_frame
69
+ @reactive.event(input.search_btn, ignore_none=False)
70
+ def on_search():
71
+ """Generate boxplot of probability_modified by group and sample."""
72
+ transcript_id: str = input.transcript_id()
73
+ transcript_position: int = input.transcript_position()
74
+
75
+ if not transcript_id or not transcript_id.strip():
76
+ return None
77
+
78
+ # Get matching reads as a dataframe
79
+ reads_df = get_matching_reads(transcript_id.strip(), int(transcript_position))
80
+
81
+ site_df = df.loc[
82
+ (df["transcript_id"] == transcript_id.strip())
83
+ & (df["transcript_position"] == int(transcript_position))
84
+ ]
85
+ site_df_tidy = site_df.melt(var_name="column", value_name="value")
86
+
87
+ read_cache.set(reads_df)
88
+
89
+ return render.DataGrid(data=site_df_tidy, width="100%", height="auto")
90
+
91
+
92
+ def get_matching_reads(transcript_id: str, transcript_position: int) -> pd.DataFrame:
93
+ """
94
+ Find rnames for the given transcript_id and transcript_position,
95
+ then query reads database for matching non-ignored reads.
96
+ """
97
+ # Find matching rnames from the sites dataframe
98
+ matching_rnames = (
99
+ df.loc[
100
+ (df["transcript_id"] == transcript_id)
101
+ & (df["transcript_position"] == transcript_position),
102
+ "rname",
103
+ ]
104
+ .unique()
105
+ .tolist()
106
+ )
107
+
108
+ print(matching_rnames)
109
+
110
+ if not matching_rnames:
111
+ return pd.DataFrame()
112
+
113
+ # Query reads database for matching, non-ignored reads
114
+ conn = duckdb.connect(database=_reads_path, read_only=True)
115
+
116
+ # Use parameterized query to avoid SQL injection
117
+ placeholders = ", ".join(["?" for _ in matching_rnames])
118
+ query = f"""
119
+ SELECT * FROM reads
120
+ WHERE rname IN ({placeholders})
121
+ AND transcript_position = {transcript_position}
122
+ AND ignored = FALSE
123
+ """
124
+
125
+ reads_df = conn.execute(query, matching_rnames).fetchdf()
126
+ conn.close()
127
+
128
+ return reads_df
129
+
130
+
131
+ # Server logic
132
+ @reactive.effect
133
+ @reactive.event(input.transcript_id)
134
+ def update_transcript_pos():
135
+ """Update the transcript position options based on the entered transcript ID."""
136
+ transcript_id: str = input.transcript_id()
137
+
138
+ if transcript_id and transcript_id.strip():
139
+ positions = df.loc[
140
+ df["transcript_id"] == transcript_id.strip(), "transcript_position"
141
+ ].unique()
142
+ positions = sorted(positions.tolist())
143
+ else:
144
+ positions = []
145
+
146
+ ui.update_selectize(
147
+ "transcript_position",
148
+ choices=positions,
149
+ )
150
+
151
+
152
+ @render.data_frame
153
+ def plot_counts():
154
+ try:
155
+ subset = read_cache.get()
156
+ except SilentException:
157
+ return None
158
+
159
+ binarized_df = (
160
+ subset.groupby(["sample_name", "group_name"])
161
+ .agg(
162
+ successes=("probability_modified", lambda x: (x >= 0.5).sum()),
163
+ failures=("probability_modified", lambda x: (x < 0.5).sum()),
164
+ )
165
+ .sort_values(["group_name", "sample_name"])
166
+ .reset_index()
167
+ )
168
+
169
+ return binarized_df
170
+
171
+
172
+ @render.plot(height=750)
173
+ def plot_modification():
174
+ try:
175
+ subset = read_cache.get()
176
+ except SilentException:
177
+ return None
178
+
179
+ return plots.plot_binarised_violin_by_site(subset)
@@ -0,0 +1,105 @@
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ import seaborn as sns
4
+ import numpy as np
5
+
6
+
7
+ def plot_binarised_violin_by_site(subset):
8
+ # Binarise probability (threshold 0.5)
9
+ subset["binarised_probability_modified"] = (
10
+ subset["probability_modified"] >= 0.5
11
+ ).astype(int)
12
+
13
+ # Add combined label and sort
14
+ subset["label"] = subset["sample_name"] + " (" + subset["group_name"] + ")"
15
+ subset.sort_values(["group_name", "sample_name"], inplace=True)
16
+ label_order = subset["label"].unique()
17
+
18
+ # Compute counts per label
19
+ counts = subset.groupby("label")["binarised_probability_modified"]
20
+ n = counts.size().reindex(label_order)
21
+ T = counts.sum().reindex(label_order)
22
+ F = n - T
23
+ T_over_n = (T / n).round(3)
24
+
25
+ label_order_with_stats = [
26
+ f"{lbl}\n(n={n[lbl]}, T={T[lbl]}, F={F[lbl]}, T/n={T_over_n[lbl]})"
27
+ for lbl in label_order
28
+ ]
29
+
30
+ # Plot
31
+ fig, ax = plt.subplots(1, 1, figsize=(max(10, len(label_order) * 1.5), 8))
32
+
33
+ sns.violinplot(
34
+ data=subset,
35
+ x="label",
36
+ y="binarised_probability_modified",
37
+ order=label_order,
38
+ inner=None,
39
+ density_norm="width",
40
+ cut=0,
41
+ color="skyblue",
42
+ ax=ax,
43
+ )
44
+
45
+ sns.boxplot(
46
+ data=subset,
47
+ x="label",
48
+ y="binarised_probability_modified",
49
+ order=label_order,
50
+ showcaps=True,
51
+ width=0.15,
52
+ boxprops={"facecolor": "white", "edgecolor": "black", "linewidth": 1},
53
+ whiskerprops={"color": "black", "linewidth": 1},
54
+ capprops={"color": "black", "linewidth": 1},
55
+ medianprops={"color": "black", "linewidth": 1},
56
+ showfliers=False,
57
+ ax=ax,
58
+ )
59
+
60
+ sns.stripplot(
61
+ data=subset,
62
+ x="label",
63
+ y="binarised_probability_modified",
64
+ order=label_order,
65
+ color="black",
66
+ size=3,
67
+ jitter=True,
68
+ alpha=0.5,
69
+ ax=ax,
70
+ )
71
+
72
+ # Mean ± SD
73
+ stats = (
74
+ subset.groupby("label")["binarised_probability_modified"]
75
+ .agg(["mean", "std"])
76
+ .reindex(label_order)
77
+ )
78
+
79
+ for j, label in enumerate(label_order):
80
+ mean_val = stats.loc[label, "mean"]
81
+ std_val = stats.loc[label, "std"]
82
+
83
+ ax.plot(j, mean_val, "o", color="red", markersize=6)
84
+
85
+ ymin = max(0, mean_val - std_val)
86
+ ymax = min(1, mean_val + std_val)
87
+ ax.errorbar(
88
+ j,
89
+ mean_val,
90
+ yerr=[[mean_val - ymin], [ymax - mean_val]],
91
+ fmt="none",
92
+ ecolor="red",
93
+ elinewidth=1,
94
+ capsize=5,
95
+ )
96
+
97
+ ax.set_xticks(range(len(label_order)))
98
+ ax.set_xticklabels(label_order_with_stats, rotation=45, ha="right")
99
+ ax.set_xlabel("Sample (Group)")
100
+ ax.set_ylabel("Binarised Probability Modified")
101
+ ax.set_title("Violin Plot of Binarised Probability Modified for each Sample")
102
+ ax.set_ylim(-0.05, 1.05)
103
+ ax.grid(True, linestyle="--", alpha=0.6)
104
+
105
+ return fig
@@ -0,0 +1,3 @@
1
+ .navbar {
2
+ border: none;
3
+ }
@@ -0,0 +1,63 @@
1
+ from shiny import run_app
2
+ import os
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+
7
+ def main():
8
+ """Launch the MakoView Shiny visualization application."""
9
+
10
+ parser = argparse.ArgumentParser(
11
+ description="Launch makoview",
12
+ )
13
+
14
+ parser.add_argument(
15
+ "--differential-results",
16
+ type=Path,
17
+ required=True,
18
+ dest="differential_results",
19
+ help="Path to differential sites database file",
20
+ )
21
+
22
+ parser.add_argument(
23
+ "--modification-db",
24
+ type=Path,
25
+ required=True,
26
+ dest="modification_db",
27
+ help="Path to modification database file",
28
+ )
29
+
30
+ parser.add_argument(
31
+ "--port",
32
+ type=int,
33
+ default=8000,
34
+ help="Port for the Shiny application (default: 8000)",
35
+ )
36
+
37
+ args = parser.parse_args()
38
+
39
+ # Use explicitly provided paths
40
+ diff_db_path = args.differential_results
41
+ mod_db_path = args.modification_db
42
+
43
+ # Validate files exist
44
+ if not diff_db_path.exists():
45
+ parser.error(f"Differential results file not found: {diff_db_path}")
46
+ if not mod_db_path.exists():
47
+ parser.error(f"Modification database file not found: {mod_db_path}")
48
+
49
+ # Set environment variables for app.py to consume
50
+ os.environ["MAKO_DIFFERENTIAL_DB"] = str(diff_db_path.absolute())
51
+ os.environ["MAKO_MODIFICATION_DB"] = str(mod_db_path.absolute())
52
+
53
+ print(f"Starting Mako Shiny app on port {args.port}...")
54
+ print(f" Differential DB: {diff_db_path.absolute()}")
55
+ print(f" Modification DB: {mod_db_path.absolute()}")
56
+
57
+ path = Path(__file__).parent.resolve()
58
+
59
+ run_app(str(path / "app.py"), port=args.port) # type: ignore[call-non-callable]
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()