jsrc 0.2.0__tar.gz → 0.2.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.
Files changed (97) hide show
  1. {jsrc-0.2.0/src/jsrc.egg-info → jsrc-0.2.2}/PKG-INFO +22 -10
  2. {jsrc-0.2.0 → jsrc-0.2.2}/README.md +21 -9
  3. {jsrc-0.2.0 → jsrc-0.2.2}/pyproject.toml +4 -1
  4. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/bootstrap_phylo.py +1 -0
  5. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/msa_consensus.py +6 -1
  6. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/cli.py +6 -10
  7. jsrc-0.2.2/src/jsrc/core.py +107 -0
  8. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/grn/__init__.py +2 -1
  9. jsrc-0.2.2/src/jsrc/grn/build.py +88 -0
  10. jsrc-0.2.2/src/jsrc/grn/net2json.py +45 -0
  11. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/grn/serve.py +7 -12
  12. jsrc-0.2.2/src/jsrc/grn/sources/index.html +37 -0
  13. jsrc-0.2.0/src/jsrc/grn/viewer.py → jsrc-0.2.2/src/jsrc/grn/sources/script.js +4 -359
  14. jsrc-0.2.2/src/jsrc/grn/sources/style.css +277 -0
  15. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/gs/build.py +1 -0
  16. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/gs/train.py +2 -1
  17. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/__init__.py +14 -1
  18. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/core.py +0 -15
  19. jsrc-0.2.2/src/jsrc/seq/digest.py +134 -0
  20. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/extract.py +1 -0
  21. jsrc-0.2.2/src/jsrc/seq/fetch.py +92 -0
  22. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/kmer.py +7 -1
  23. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/qc.py +4 -0
  24. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/translate.py +11 -0
  25. {jsrc-0.2.0 → jsrc-0.2.2/src/jsrc.egg-info}/PKG-INFO +22 -10
  26. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc.egg-info/SOURCES.txt +10 -1
  27. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_analyze_extra.py +3 -5
  28. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_grn_conversion.py +4 -2
  29. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_gs_train.py +18 -7
  30. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_job_core.py +3 -4
  31. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_plot_commands.py +4 -3
  32. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_plot_core.py +4 -5
  33. jsrc-0.2.2/tests/test_progress.py +100 -0
  34. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_codon_kmer.py +2 -4
  35. jsrc-0.2.2/tests/test_seq_digest.py +177 -0
  36. jsrc-0.2.2/tests/test_seq_fetch.py +164 -0
  37. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_promoter.py +3 -12
  38. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_translate.py +1 -1
  39. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_vision_efd.py +11 -20
  40. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_vision_extract.py +0 -2
  41. jsrc-0.2.0/src/jsrc/grn/net2json.py +0 -141
  42. {jsrc-0.2.0 → jsrc-0.2.2}/LICENSE +0 -0
  43. {jsrc-0.2.0 → jsrc-0.2.2}/setup.cfg +0 -0
  44. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/__init__.py +0 -0
  45. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/__init__.py +0 -0
  46. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/core.py +0 -0
  47. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/motif.py +0 -0
  48. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/phylo.py +0 -0
  49. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/qc.py +0 -0
  50. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/analyze/snpindel.py +0 -0
  51. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/grn/anno2json.py +0 -0
  52. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/grn/centrality.py +0 -0
  53. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/grn/core.py +0 -0
  54. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/gs/__init__.py +0 -0
  55. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/gs/split.py +0 -0
  56. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/__init__.py +0 -0
  57. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/core.py +0 -0
  58. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/gc.py +0 -0
  59. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/history.py +0 -0
  60. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/kill.py +0 -0
  61. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/logs.py +0 -0
  62. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/ls.py +0 -0
  63. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/show.py +0 -0
  64. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/job/submit.py +0 -0
  65. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/__init__.py +0 -0
  66. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/chromosome.py +0 -0
  67. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/circoslite.py +0 -0
  68. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/cis.py +0 -0
  69. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/core.py +0 -0
  70. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/domain.py +0 -0
  71. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/dotplot.py +0 -0
  72. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/exon.py +0 -0
  73. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/gene.py +0 -0
  74. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/heart.py +0 -0
  75. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/plot/rose.py +0 -0
  76. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/codon.py +0 -0
  77. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/promoter.py +0 -0
  78. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/rename.py +0 -0
  79. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/seq/window.py +0 -0
  80. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/vision/__init__.py +0 -0
  81. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/vision/core.py +0 -0
  82. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/vision/efd.py +1 -1
  83. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/vision/extract.py +0 -0
  84. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc/vision/traits.py +0 -0
  85. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc.egg-info/dependency_links.txt +0 -0
  86. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc.egg-info/entry_points.txt +0 -0
  87. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc.egg-info/requires.txt +0 -0
  88. {jsrc-0.2.0 → jsrc-0.2.2}/src/jsrc.egg-info/top_level.txt +0 -0
  89. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_analyze_phylo.py +0 -0
  90. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_cli_error_behavior.py +0 -0
  91. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_cli_module_flows.py +0 -0
  92. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_job_portability.py +0 -0
  93. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_core.py +0 -0
  94. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_extract.py +0 -0
  95. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_qc.py +0 -0
  96. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_rename.py +0 -0
  97. {jsrc-0.2.0 → jsrc-0.2.2}/tests/test_seq_window.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsrc
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python library for bioinformatics and scientific computing
5
5
  Author-email: Jiaoyuan <imjiaoyuan@gmail.com>
6
6
  License-Expression: MIT
@@ -38,27 +38,36 @@ Python library for bioinformatics and scientific computing.
38
38
 
39
39
  ## Installation
40
40
 
41
- PyPI:
41
+ Recommended (global CLI via `uv tool`):
42
42
  ```bash
43
- pip install jsrc
43
+ uv tool install jsrc
44
44
  ```
45
45
 
46
- uv:
46
+ Using `uv` virtual environment:
47
47
  ```bash
48
+ uv venv
49
+ source .venv/bin/activate
48
50
  uv pip install jsrc
49
51
  ```
50
52
 
51
- From source:
53
+ Using Conda virtual environment + pip:
54
+ ```bash
55
+ conda create -n jsrc python=3.11 -y
56
+ conda activate jsrc
57
+ pip install jsrc
58
+ ```
59
+
60
+ From source (development):
52
61
  ```bash
53
62
  git clone https://github.com/imjiaoyuan/jsrc.git
54
63
  cd jsrc
55
64
  uv venv
56
- uv sync --extra dev
65
+ uv sync --extra dev --extra all
57
66
  ```
58
67
 
59
68
  Run `jsrc --help` to get started.
60
69
 
61
- For detailed usage, see the [Documentation](docs/en/index.md).中文文档请参阅 [文档](docs/zh/index.md)。
70
+ For detailed usage, see the [Documentation](docs/en/index.md). 中文文档请参阅 [文档](docs/zh/index.md)。
62
71
 
63
72
  ## Quick Start
64
73
 
@@ -84,7 +93,7 @@ jsrc vision extract --help
84
93
  | `plot` | Gene/exon/chromosome/domain and other plots | `jsrc plot gene ...` |
85
94
  | `analyze` | Phylogeny, motif, consensus, SNP/INDEL, QC | `jsrc analyze phylo ...` |
86
95
  | `gs` | Genomic selection dataset build/split/train | `jsrc gs train ...` |
87
- | `grn` | GRN conversion, centrality, local viewer | `jsrc grn net2json ...` |
96
+ | `grn` | GRN conversion, centrality, build packaging, local serve | `jsrc grn build ...` |
88
97
  | `vision` | Object extraction, morphology traits, EFD | `jsrc vision extract ...` |
89
98
  | `job` | Background job submit/list/log/kill/history | `jsrc job submit "cmd"` |
90
99
 
@@ -136,10 +145,13 @@ jsrc vision traits -i test/leaf1.jpg --channel a --invert
136
145
 
137
146
  **grn module**
138
147
 
139
- Generate 1000-gene random network viewer with full-view mode (top 200 nodes):
148
+ Generate and launch a 1000-gene random network viewer:
140
149
 
141
150
  ```bash
142
- jsrc grn net2json -i test/grn/network.tsv -o test/grn/json/grn.json -n test/grn/annotation.tsv -z test/grn/grn-viewer.zip -s
151
+ jsrc grn net2json -i test/grn/network.tsv -o test/grn/grn.json
152
+ jsrc grn anno2json -i test/grn/annotation.tsv -o test/grn/annotation.json
153
+ jsrc grn build -d test/grn/public -g test/grn/grn.json -n test/grn/annotation.json -z test/grn/grn-viewer.zip -a -t 200
154
+ jsrc grn serve -d test/grn/public -g test/grn/public/json/grn.json -n test/grn/public/json/annotation.json -p 8000 -a -t 200
143
155
  ```
144
156
 
145
157
  ![](assets/grn.jpg)
@@ -4,27 +4,36 @@ Python library for bioinformatics and scientific computing.
4
4
 
5
5
  ## Installation
6
6
 
7
- PyPI:
7
+ Recommended (global CLI via `uv tool`):
8
8
  ```bash
9
- pip install jsrc
9
+ uv tool install jsrc
10
10
  ```
11
11
 
12
- uv:
12
+ Using `uv` virtual environment:
13
13
  ```bash
14
+ uv venv
15
+ source .venv/bin/activate
14
16
  uv pip install jsrc
15
17
  ```
16
18
 
17
- From source:
19
+ Using Conda virtual environment + pip:
20
+ ```bash
21
+ conda create -n jsrc python=3.11 -y
22
+ conda activate jsrc
23
+ pip install jsrc
24
+ ```
25
+
26
+ From source (development):
18
27
  ```bash
19
28
  git clone https://github.com/imjiaoyuan/jsrc.git
20
29
  cd jsrc
21
30
  uv venv
22
- uv sync --extra dev
31
+ uv sync --extra dev --extra all
23
32
  ```
24
33
 
25
34
  Run `jsrc --help` to get started.
26
35
 
27
- For detailed usage, see the [Documentation](docs/en/index.md).中文文档请参阅 [文档](docs/zh/index.md)。
36
+ For detailed usage, see the [Documentation](docs/en/index.md). 中文文档请参阅 [文档](docs/zh/index.md)。
28
37
 
29
38
  ## Quick Start
30
39
 
@@ -50,7 +59,7 @@ jsrc vision extract --help
50
59
  | `plot` | Gene/exon/chromosome/domain and other plots | `jsrc plot gene ...` |
51
60
  | `analyze` | Phylogeny, motif, consensus, SNP/INDEL, QC | `jsrc analyze phylo ...` |
52
61
  | `gs` | Genomic selection dataset build/split/train | `jsrc gs train ...` |
53
- | `grn` | GRN conversion, centrality, local viewer | `jsrc grn net2json ...` |
62
+ | `grn` | GRN conversion, centrality, build packaging, local serve | `jsrc grn build ...` |
54
63
  | `vision` | Object extraction, morphology traits, EFD | `jsrc vision extract ...` |
55
64
  | `job` | Background job submit/list/log/kill/history | `jsrc job submit "cmd"` |
56
65
 
@@ -102,10 +111,13 @@ jsrc vision traits -i test/leaf1.jpg --channel a --invert
102
111
 
103
112
  **grn module**
104
113
 
105
- Generate 1000-gene random network viewer with full-view mode (top 200 nodes):
114
+ Generate and launch a 1000-gene random network viewer:
106
115
 
107
116
  ```bash
108
- jsrc grn net2json -i test/grn/network.tsv -o test/grn/json/grn.json -n test/grn/annotation.tsv -z test/grn/grn-viewer.zip -s
117
+ jsrc grn net2json -i test/grn/network.tsv -o test/grn/grn.json
118
+ jsrc grn anno2json -i test/grn/annotation.tsv -o test/grn/annotation.json
119
+ jsrc grn build -d test/grn/public -g test/grn/grn.json -n test/grn/annotation.json -z test/grn/grn-viewer.zip -a -t 200
120
+ jsrc grn serve -d test/grn/public -g test/grn/public/json/grn.json -n test/grn/public/json/annotation.json -p 8000 -a -t 200
109
121
  ```
110
122
 
111
123
  ![](assets/grn.jpg)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jsrc"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Python library for bioinformatics and scientific computing"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -39,5 +39,8 @@ build-backend = "setuptools.build_meta"
39
39
  where = ["src"]
40
40
  include = ["jsrc*"]
41
41
 
42
+ [tool.setuptools.package-data]
43
+ "jsrc.grn" = ["sources/*"]
44
+
42
45
  [tool.black]
43
46
  target-version = ["py310"]
@@ -14,6 +14,7 @@ from jsrc.analyze.core import pad_alignment
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
+
17
18
  def _tree_from_alignment(aln: MultipleSeqAlignment):
18
19
  calculator = DistanceCalculator("identity")
19
20
  dm = calculator.get_distance(aln)
@@ -7,6 +7,7 @@ from typing import Any
7
7
  from Bio import SeqIO
8
8
 
9
9
  from jsrc.analyze.core import pad_alignment
10
+ from jsrc.core import progressbar
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
@@ -25,9 +26,11 @@ def cmd(args: Namespace) -> None:
25
26
  max_len,
26
27
  )
27
28
  seqs = [str(r.seq) for r in pad_alignment(records)]
29
+ seq_len = len(seqs[0])
28
30
  consensus_chars = []
29
31
  conservation = []
30
- for i in range(len(seqs[0])):
32
+ bar = progressbar(total=seq_len, desc="Consensus")
33
+ for i in range(seq_len):
31
34
  col = [s[i] for s in seqs if s[i] != "-"]
32
35
  if not col:
33
36
  consensus_chars.append("N")
@@ -37,7 +40,9 @@ def cmd(args: Namespace) -> None:
37
40
  base, c = cnt.most_common(1)[0]
38
41
  consensus_chars.append(base)
39
42
  conservation.append(c / len(col))
43
+ bar.update()
40
44
  consensus = "".join(consensus_chars)
45
+ bar.finish()
41
46
  avg_cons = sum(conservation) / len(conservation) if conservation else 0.0
42
47
  payload = {
43
48
  "sequence_count": len(records),
@@ -9,15 +9,9 @@ from jsrc import __version__
9
9
 
10
10
  def setup_logging(verbose: bool = False) -> None:
11
11
  level = logging.DEBUG if verbose else logging.INFO
12
- logging.basicConfig(level=level, format="%(message)s", stream=sys.stderr, force=True)
13
-
14
-
15
- def _dispatch(module_name: str, func_name: str = "cmd") -> argparse.Action:
16
- def _runner(args: argparse.Namespace) -> None:
17
- module = importlib.import_module(module_name)
18
- getattr(module, func_name)(args)
19
-
20
- return _runner
12
+ logging.basicConfig(
13
+ level=level, format="%(message)s", stream=sys.stderr, force=True
14
+ )
21
15
 
22
16
 
23
17
  MODULES = {
@@ -40,7 +34,9 @@ def _iter_enabled_modules() -> list[str]:
40
34
  return [n for n in names if n in MODULES and n not in disabled]
41
35
 
42
36
 
43
- def _register_modules(subparsers: argparse.Action, *, debug: bool = False) -> tuple[list[str], list[str]]:
37
+ def _register_modules(
38
+ subparsers: argparse.Action, *, debug: bool = False
39
+ ) -> tuple[list[str], list[str]]:
44
40
  loaded: list[str] = []
45
41
  errors: list[str] = []
46
42
  for name in _iter_enabled_modules():
@@ -0,0 +1,107 @@
1
+ import sys
2
+ import time
3
+ from typing import Any, Generator, Iterable
4
+
5
+
6
+ def _fmt_duration(seconds: float) -> str:
7
+ t = int(seconds)
8
+ h, m, s = t // 3600, (t % 3600) // 60, t % 60
9
+ if h:
10
+ return f"{h}:{m:02d}:{s:02d}"
11
+ return f"{m:02d}:{s:02d}"
12
+
13
+
14
+ class progressbar:
15
+ def __init__(
16
+ self,
17
+ total: int = 0,
18
+ desc: str = "",
19
+ width: int = 40,
20
+ min_interval: float = 0.1,
21
+ tty_only: bool = True,
22
+ ):
23
+ self.total = total
24
+ self.desc = desc
25
+ self.width = width
26
+ self.min_interval = min_interval
27
+ self.n = 0
28
+ self.start_time = time.time()
29
+ self._last_update = 0.0
30
+ self._finished = False
31
+ self._enabled = not tty_only or sys.stderr.isatty()
32
+
33
+ def update(self, n: int = 1) -> None:
34
+ if not self._enabled or self._finished:
35
+ self.n += n
36
+ return
37
+ self.n += n
38
+ self._try_render()
39
+
40
+ def set(self, n: int) -> None:
41
+ if not self._enabled or self._finished:
42
+ self.n = n
43
+ return
44
+ self.n = n
45
+ self._try_render()
46
+
47
+ def finish(self) -> None:
48
+ if not self._enabled or self._finished:
49
+ return
50
+ self.n = self.total if self.total > 0 else self.n
51
+ self._render(time.time())
52
+ self._finished = True
53
+
54
+ def _try_render(self) -> None:
55
+ now = time.time()
56
+ if self.n < self.total and now - self._last_update < self.min_interval:
57
+ return
58
+ self._render(now)
59
+
60
+ def _render(self, now: float) -> None:
61
+ elapsed = now - self.start_time
62
+
63
+ if self.total > 0:
64
+ pct = self.n / self.total
65
+ filled = int(self.width * pct)
66
+ bar = "#" * filled + " " * (self.width - filled)
67
+
68
+ rate = self.n / elapsed if self.n > 0 and elapsed > 0 else 0
69
+ remaining = (self.total - self.n) / rate if rate > 0 else 0
70
+
71
+ sys.stderr.write(
72
+ f"\r{self.desc} [{bar}] {self.n}/{self.total}"
73
+ f" ({pct * 100:5.1f}%)"
74
+ f" [{_fmt_duration(elapsed)}<{_fmt_duration(remaining)}]"
75
+ )
76
+ else:
77
+ rate = self.n / elapsed if elapsed > 0 else 0
78
+ sys.stderr.write(
79
+ f"\r{self.desc} {self.n} [{_fmt_duration(elapsed)}"
80
+ f" ({rate:.0f} items/s)]"
81
+ )
82
+
83
+ if 0 < self.total <= self.n:
84
+ sys.stderr.write("\n")
85
+ else:
86
+ sys.stderr.flush()
87
+
88
+ def __enter__(self) -> "progressbar":
89
+ return self
90
+
91
+ def __exit__(self, *args: object) -> None:
92
+ self.finish()
93
+
94
+ def iter(
95
+ self, items: Iterable[Any], total: int | None = None
96
+ ) -> Generator[Any, None, None]:
97
+ if total is not None:
98
+ self.total = total
99
+ elif hasattr(items, "__len__"):
100
+ self.total = len(items)
101
+ else:
102
+ self.total = 0
103
+ self.n = 0
104
+ for item in items:
105
+ yield item
106
+ self.update()
107
+ self.finish()
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from jsrc.grn import anno2json, centrality, net2json, serve
3
+ from jsrc.grn import anno2json, build, centrality, net2json, serve
4
4
 
5
5
 
6
6
  def register_subparser(subparsers: Any) -> None:
@@ -8,6 +8,7 @@ def register_subparser(subparsers: Any) -> None:
8
8
  grn_sub = grn_parser.add_subparsers(dest="grn_cmd")
9
9
  grn_parser.set_defaults(_group_parser=grn_parser)
10
10
 
11
+ build.register(grn_sub)
11
12
  net2json.register(grn_sub)
12
13
  anno2json.register(grn_sub)
13
14
  serve.register(grn_sub)
@@ -0,0 +1,88 @@
1
+ import os
2
+ import pathlib
3
+ import shutil
4
+ import zipfile
5
+ from argparse import Namespace
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from jsrc.grn.core import ensure_dir, write_text
10
+
11
+ _SCRIPT_TEMPLATE = (pathlib.Path(__file__).parent / "sources" / "script.js").read_text(
12
+ encoding="utf-8"
13
+ )
14
+ _INDEX_HTML = (pathlib.Path(__file__).parent / "sources" / "index.html").read_text(
15
+ encoding="utf-8"
16
+ )
17
+ _STYLE_CSS = (pathlib.Path(__file__).parent / "sources" / "style.css").read_text(
18
+ encoding="utf-8"
19
+ )
20
+
21
+
22
+ def _sync_assets(
23
+ base: str,
24
+ view_mode: str,
25
+ threshold: int,
26
+ max_nodes: int,
27
+ ) -> None:
28
+ ensure_dir(base)
29
+ ensure_dir(os.path.join(base, "css"))
30
+ ensure_dir(os.path.join(base, "js"))
31
+ ensure_dir(os.path.join(base, "json"))
32
+ write_text(os.path.join(base, "index.html"), _INDEX_HTML)
33
+ write_text(os.path.join(base, "css/style.css"), _STYLE_CSS)
34
+ script = (
35
+ _SCRIPT_TEMPLATE.replace("__JSRC_VIEW_MODE__", view_mode)
36
+ .replace("__JSRC_FULL_THRESHOLD__", str(threshold))
37
+ .replace("__JSRC_MAX_DISPLAY_NODES__", str(max_nodes))
38
+ )
39
+ write_text(os.path.join(base, "js/script.js"), script)
40
+
41
+
42
+ def _zip_viewer(viewer_dir: Path, zip_output: str) -> None:
43
+ zip_path = Path(zip_output).expanduser().resolve()
44
+ zip_path.parent.mkdir(parents=True, exist_ok=True)
45
+ wanted = [
46
+ viewer_dir / "index.html",
47
+ viewer_dir / "css" / "style.css",
48
+ viewer_dir / "js" / "script.js",
49
+ viewer_dir / "json" / "grn.json",
50
+ viewer_dir / "json" / "annotation.json",
51
+ ]
52
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
53
+ for f in wanted:
54
+ if f.exists():
55
+ zf.write(f, arcname=str(f.relative_to(viewer_dir)))
56
+
57
+
58
+ def cmd(args: Namespace) -> None:
59
+ root = Path(args.dir).expanduser().resolve()
60
+ view_mode = "expand" if args.expand else "auto"
61
+ _sync_assets(str(root), view_mode, args.threshold, args.max_nodes)
62
+ if args.grn_json:
63
+ shutil.copy(args.grn_json, root / "json" / "grn.json")
64
+ if args.annotation_json:
65
+ shutil.copy(args.annotation_json, root / "json" / "annotation.json")
66
+ if args.zip_output:
67
+ _zip_viewer(root, args.zip_output)
68
+
69
+
70
+ def register(subparsers: Any) -> None:
71
+ p = subparsers.add_parser("build", help="Build GRN viewer package")
72
+ p.add_argument("-d", "--dir", default=".", help="Output directory (default: .)")
73
+ p.add_argument("-g", "--grn-json", help="grn.json to copy into package")
74
+ p.add_argument(
75
+ "-n", "--annotation-json", help="annotation.json to copy into package"
76
+ )
77
+ p.add_argument("-z", "--zip-output", help="ZIP output path")
78
+ mode = p.add_mutually_exclusive_group()
79
+ mode.add_argument("-a", "--all", action="store_true", help="Full view mode")
80
+ mode.add_argument(
81
+ "-e", "--expand", action="store_true", help="Click-to-expand mode"
82
+ )
83
+ p.set_defaults(all=True)
84
+ p.add_argument(
85
+ "-t", "--threshold", type=int, default=300, help="Auto full-view threshold"
86
+ )
87
+ p.add_argument("--max-nodes", type=int, default=0, help="Max nodes (0 = all)")
88
+ p.set_defaults(func=cmd)
@@ -0,0 +1,45 @@
1
+ import csv
2
+ import logging
3
+ from argparse import Namespace
4
+ from typing import Any
5
+
6
+ from jsrc.grn.core import write_json
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def network_to_json(
12
+ input_path: str, output_path: str
13
+ ) -> tuple[list[dict[str, Any]], int]:
14
+ links = []
15
+ with open(input_path, "r", encoding="utf-8") as f:
16
+ reader = csv.reader(f, delimiter="\t")
17
+ for row in reader:
18
+ if len(row) < 3:
19
+ continue
20
+ source_id = str(row[0]).replace("_", "-")
21
+ target_id = str(row[1]).replace("_", "-")
22
+ try:
23
+ weight = float(row[2])
24
+ except ValueError:
25
+ continue
26
+ links.append({"source": source_id, "target": target_id, "val": weight})
27
+ write_json(output_path, links)
28
+ nodes = set()
29
+ for item in links:
30
+ nodes.add(item["source"])
31
+ nodes.add(item["target"])
32
+ logger.info("Network JSON written: %s", output_path)
33
+ logger.info("Genes: %d | Edges: %d", len(nodes), len(links))
34
+ return links, len(nodes)
35
+
36
+
37
+ def cmd(args: Namespace) -> None:
38
+ network_to_json(args.input, args.output)
39
+
40
+
41
+ def register(subparsers: Any) -> None:
42
+ p = subparsers.add_parser("net2json", help="Convert GRN edge table to grn.json")
43
+ p.add_argument("-i", "--input", required=True, help="Input edge table (TSV)")
44
+ p.add_argument("-o", dest="output", required=True, help="Output JSON path")
45
+ p.set_defaults(func=cmd)
@@ -6,17 +6,12 @@ from argparse import Namespace
6
6
  from typing import Any
7
7
 
8
8
  from jsrc.grn.core import ensure_dir, write_json
9
- from jsrc.grn.viewer import sync_viewer_assets
9
+ from jsrc.grn.build import _sync_assets
10
10
 
11
11
 
12
12
  def cmd(args: Namespace) -> None:
13
- view_mode = "expand" if args.some else "auto"
14
- sync_viewer_assets(
15
- args.dir,
16
- init_empty_json=False,
17
- view_mode=view_mode,
18
- full_view_threshold=args.threshold,
19
- )
13
+ view_mode = "expand" if args.expand else "auto"
14
+ _sync_assets(args.dir, view_mode, args.threshold, 0)
20
15
  ensure_dir(f"{args.dir}/json")
21
16
  src_grn = os.path.abspath(args.grn_json)
22
17
  dst_grn = os.path.abspath(f"{args.dir}/json/grn.json")
@@ -58,12 +53,12 @@ def register(subparsers: Any) -> None:
58
53
  help="Mode all: auto full-view when gene count <= threshold",
59
54
  )
60
55
  mode.add_argument(
61
- "-s",
62
- "--some",
56
+ "-e",
57
+ "--expand",
63
58
  action="store_true",
64
- help="Mode some: manual click-to-expand only",
59
+ help="Click-to-expand mode",
65
60
  )
66
- p.set_defaults(all=True, some=False)
61
+ p.set_defaults(all=True, expand=False)
67
62
  p.add_argument(
68
63
  "-t",
69
64
  "--threshold",
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>GRN</title>
6
+ <link rel="stylesheet" href="css/style.css">
7
+ <script src="//unpkg.com/d3"></script>
8
+ <script src="//unpkg.com/force-graph"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
11
+ </head>
12
+ <body>
13
+ <div id="controls">
14
+ <div class="row">
15
+ <button onclick="goBack()" id="btnBack" disabled>&lt;</button>
16
+ <button onclick="goForward()" id="btnFwd" disabled>&gt;</button>
17
+ <button onclick="exportImage('pdf')" class="btn-export">PDF</button>
18
+ <button onclick="resetView()" class="btn-reset">Reset</button>
19
+ </div>
20
+
21
+ <div class="row search-row">
22
+ <input type="text" id="geneInput" placeholder="Enter Gene ID">
23
+ <button onclick="startNewSearch()" class="btn-search">Search</button>
24
+ </div>
25
+
26
+ <div id="legend">Line thickness indicates weight</div>
27
+ <div id="nodeCount">Nodes: 0</div>
28
+ <div id="neighborList"></div>
29
+ </div>
30
+
31
+ <div id="watermark">GRN</div>
32
+ <div id="emptyState">No Nodes</div>
33
+ <div id="graph"></div>
34
+
35
+ <script src="js/script.js"></script>
36
+ </body>
37
+ </html>