jsrc 0.2.2__tar.gz → 0.2.4__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.
- {jsrc-0.2.2/src/jsrc.egg-info → jsrc-0.2.4}/PKG-INFO +1 -1
- {jsrc-0.2.2 → jsrc-0.2.4}/pyproject.toml +1 -1
- jsrc-0.2.4/src/jsrc/analyze/__init__.py +39 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/cli.py +78 -32
- jsrc-0.2.4/src/jsrc/grn/__init__.py +38 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/anno2json.py +4 -4
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/build.py +1 -1
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/net2json.py +2 -2
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/serve.py +1 -1
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/script.js +3 -3
- jsrc-0.2.4/src/jsrc/gs/__init__.py +38 -0
- jsrc-0.2.4/src/jsrc/job/__init__.py +39 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/core.py +63 -15
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/gc.py +2 -2
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/history.py +5 -11
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/kill.py +2 -2
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/ls.py +9 -8
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/submit.py +2 -2
- jsrc-0.2.4/src/jsrc/plot/__init__.py +42 -0
- jsrc-0.2.4/src/jsrc/plot/heart.py +82 -0
- jsrc-0.2.4/src/jsrc/seq/__init__.py +43 -0
- jsrc-0.2.4/src/jsrc/vision/__init__.py +38 -0
- {jsrc-0.2.2 → jsrc-0.2.4/src/jsrc.egg-info}/PKG-INFO +1 -1
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/SOURCES.txt +0 -1
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_cli_module_flows.py +56 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_grn_conversion.py +10 -1
- jsrc-0.2.2/src/jsrc/analyze/__init__.py +0 -16
- jsrc-0.2.2/src/jsrc/grn/__init__.py +0 -15
- jsrc-0.2.2/src/jsrc/gs/__init__.py +0 -15
- jsrc-0.2.2/src/jsrc/job/__init__.py +0 -17
- jsrc-0.2.2/src/jsrc/job/show.py +0 -51
- jsrc-0.2.2/src/jsrc/plot/__init__.py +0 -29
- jsrc-0.2.2/src/jsrc/plot/heart.py +0 -37
- jsrc-0.2.2/src/jsrc/seq/__init__.py +0 -31
- jsrc-0.2.2/src/jsrc/vision/__init__.py +0 -15
- {jsrc-0.2.2 → jsrc-0.2.4}/LICENSE +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/README.md +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/setup.cfg +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/__init__.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/bootstrap_phylo.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/motif.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/msa_consensus.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/phylo.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/qc.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/snpindel.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/centrality.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/index.html +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/style.css +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/build.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/split.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/train.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/logs.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/chromosome.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/circoslite.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/cis.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/domain.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/dotplot.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/exon.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/gene.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/rose.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/codon.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/digest.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/extract.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/fetch.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/kmer.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/promoter.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/qc.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/rename.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/translate.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/window.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/efd.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/extract.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/traits.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/dependency_links.txt +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/entry_points.txt +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/requires.txt +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/top_level.txt +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_analyze_extra.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_analyze_phylo.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_cli_error_behavior.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_gs_train.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_job_core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_job_portability.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_plot_commands.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_plot_core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_progress.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_codon_kmer.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_core.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_digest.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_extract.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_fetch.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_promoter.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_qc.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_rename.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_translate.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_window.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_vision_efd.py +0 -0
- {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_vision_extract.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
_SUBCOMMANDS: dict[str, tuple[str, str]] = {
|
|
5
|
+
"phylo": ("jsrc.analyze.phylo", "Build phylogenetic trees"),
|
|
6
|
+
"motif": ("jsrc.analyze.motif", "Motif discovery and reporting"),
|
|
7
|
+
"qc": ("jsrc.analyze.qc", "Alignment/sequence QC"),
|
|
8
|
+
"msa_consensus": ("jsrc.analyze.msa_consensus", "MSA consensus statistics"),
|
|
9
|
+
"snpindel": ("jsrc.analyze.snpindel", "SNP/INDEL analysis from alignments"),
|
|
10
|
+
"bootstrap_phylo": ("jsrc.analyze.bootstrap_phylo", "Bootstrap phylogeny support"),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _register_stub_subcommands(subparsers: Any) -> None:
|
|
15
|
+
for name, (_, help_text) in _SUBCOMMANDS.items():
|
|
16
|
+
subparsers.add_parser(name, help=help_text)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _register_selected_subcommand(subparsers: Any, selected: str) -> bool:
|
|
20
|
+
module_path, _ = _SUBCOMMANDS.get(selected, ("", ""))
|
|
21
|
+
if not module_path:
|
|
22
|
+
return False
|
|
23
|
+
mod = importlib.import_module(module_path)
|
|
24
|
+
reg = getattr(mod, "register", None)
|
|
25
|
+
if reg is None:
|
|
26
|
+
raise AttributeError(f"{module_path}: missing register")
|
|
27
|
+
reg(subparsers)
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_subparser(subparsers: Any, selected_subcommand: str | None = None) -> None:
|
|
32
|
+
analyze_parser = subparsers.add_parser("analyze", help="Analysis tools")
|
|
33
|
+
analyze_sub = analyze_parser.add_subparsers(dest="analyze_cmd")
|
|
34
|
+
analyze_parser.set_defaults(_group_parser=analyze_parser)
|
|
35
|
+
if selected_subcommand and _register_selected_subcommand(
|
|
36
|
+
analyze_sub, selected_subcommand
|
|
37
|
+
):
|
|
38
|
+
return
|
|
39
|
+
_register_stub_subcommands(analyze_sub)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import importlib
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
@@ -24,6 +25,16 @@ MODULES = {
|
|
|
24
25
|
"job": "jsrc.job",
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
MODULE_HELP = {
|
|
29
|
+
"seq": "Sequence tools",
|
|
30
|
+
"plot": "Visualization",
|
|
31
|
+
"analyze": "Analysis workflows",
|
|
32
|
+
"gs": "Genomic selection",
|
|
33
|
+
"grn": "GRN tools",
|
|
34
|
+
"vision": "Image analysis",
|
|
35
|
+
"job": "Background jobs",
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
|
|
28
39
|
def _iter_enabled_modules() -> list[str]:
|
|
29
40
|
only = [x.strip() for x in os.getenv("JSRC_MODULES", "").split(",") if x.strip()]
|
|
@@ -34,31 +45,7 @@ def _iter_enabled_modules() -> list[str]:
|
|
|
34
45
|
return [n for n in names if n in MODULES and n not in disabled]
|
|
35
46
|
|
|
36
47
|
|
|
37
|
-
def
|
|
38
|
-
subparsers: argparse.Action, *, debug: bool = False
|
|
39
|
-
) -> tuple[list[str], list[str]]:
|
|
40
|
-
loaded: list[str] = []
|
|
41
|
-
errors: list[str] = []
|
|
42
|
-
for name in _iter_enabled_modules():
|
|
43
|
-
try:
|
|
44
|
-
mod = importlib.import_module(MODULES[name])
|
|
45
|
-
reg = getattr(mod, "register_subparser", None)
|
|
46
|
-
if reg is None:
|
|
47
|
-
errors.append(f"{name}: missing register_subparser")
|
|
48
|
-
continue
|
|
49
|
-
reg(subparsers)
|
|
50
|
-
loaded.append(name)
|
|
51
|
-
except (ImportError, AttributeError) as exc:
|
|
52
|
-
if debug:
|
|
53
|
-
raise
|
|
54
|
-
errors.append(f"{name}: {exc}")
|
|
55
|
-
return loaded, errors
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def main() -> None:
|
|
59
|
-
debug_mode = "--debug" in sys.argv[1:]
|
|
60
|
-
verbose = "--verbose" in sys.argv[1:] or debug_mode
|
|
61
|
-
setup_logging(verbose=verbose)
|
|
48
|
+
def _build_base_parser() -> argparse.ArgumentParser:
|
|
62
49
|
parser = argparse.ArgumentParser(
|
|
63
50
|
prog="jsrc", description="General-purpose bioinformatics and data toolkit"
|
|
64
51
|
)
|
|
@@ -69,17 +56,76 @@ def main() -> None:
|
|
|
69
56
|
action="store_true",
|
|
70
57
|
help="Show traceback for module loading and runtime errors",
|
|
71
58
|
)
|
|
72
|
-
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _register_stub_modules(
|
|
63
|
+
subparsers: argparse.Action, enabled_modules: list[str]
|
|
64
|
+
) -> None:
|
|
65
|
+
for name in enabled_modules:
|
|
66
|
+
subparsers.add_parser(name, help=MODULE_HELP.get(name, f"{name} module"))
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
|
|
69
|
+
def _register_one_module(
|
|
70
|
+
subparsers: argparse.Action,
|
|
71
|
+
module_name: str,
|
|
72
|
+
*,
|
|
73
|
+
selected_subcommand: str | None = None,
|
|
74
|
+
debug: bool = False,
|
|
75
|
+
) -> bool:
|
|
76
|
+
try:
|
|
77
|
+
mod = importlib.import_module(MODULES[module_name])
|
|
78
|
+
reg = getattr(mod, "register_subparser", None)
|
|
79
|
+
if reg is None:
|
|
80
|
+
raise AttributeError("missing register_subparser")
|
|
81
|
+
params = inspect.signature(reg).parameters
|
|
82
|
+
if "selected_subcommand" in params:
|
|
83
|
+
reg(subparsers, selected_subcommand=selected_subcommand)
|
|
84
|
+
else:
|
|
85
|
+
reg(subparsers)
|
|
86
|
+
return True
|
|
87
|
+
except (ImportError, AttributeError) as exc:
|
|
88
|
+
if debug:
|
|
89
|
+
raise
|
|
90
|
+
logging.error("Error: failed to load module '%s': %s", module_name, exc)
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _probe_route(argv: list[str]) -> tuple[str | None, str | None]:
|
|
95
|
+
probe = argparse.ArgumentParser(add_help=False)
|
|
96
|
+
probe.add_argument("--verbose", action="store_true")
|
|
97
|
+
probe.add_argument("--debug", action="store_true")
|
|
98
|
+
probe.add_argument("-v", "--version", action="store_true")
|
|
99
|
+
probe.add_argument("command", nargs="?")
|
|
100
|
+
probe.add_argument("subcommand", nargs="?")
|
|
101
|
+
args, _ = probe.parse_known_args(argv)
|
|
102
|
+
return args.command, args.subcommand
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> None:
|
|
106
|
+
argv = sys.argv[1:]
|
|
107
|
+
debug_mode = "--debug" in argv
|
|
108
|
+
verbose = "--verbose" in argv or debug_mode
|
|
109
|
+
setup_logging(verbose=verbose)
|
|
110
|
+
enabled_modules = _iter_enabled_modules()
|
|
111
|
+
if not enabled_modules:
|
|
80
112
|
logging.error("no module loaded")
|
|
81
113
|
sys.exit(2)
|
|
82
114
|
|
|
115
|
+
requested_module, requested_subcommand = _probe_route(argv)
|
|
116
|
+
parser = _build_base_parser()
|
|
117
|
+
subparsers = parser.add_subparsers(dest="command", help="Available modules")
|
|
118
|
+
if requested_module and requested_module in enabled_modules:
|
|
119
|
+
if not _register_one_module(
|
|
120
|
+
subparsers,
|
|
121
|
+
requested_module,
|
|
122
|
+
selected_subcommand=requested_subcommand,
|
|
123
|
+
debug=debug_mode,
|
|
124
|
+
):
|
|
125
|
+
sys.exit(2)
|
|
126
|
+
else:
|
|
127
|
+
_register_stub_modules(subparsers, enabled_modules)
|
|
128
|
+
|
|
83
129
|
args = parser.parse_args()
|
|
84
130
|
if not args.command:
|
|
85
131
|
parser.print_help()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
_SUBCOMMANDS: dict[str, tuple[str, str]] = {
|
|
5
|
+
"build": ("jsrc.grn.build", "Build GRN viewer package"),
|
|
6
|
+
"net2json": ("jsrc.grn.net2json", "Convert GRN edge table to grn.json"),
|
|
7
|
+
"anno2json": ("jsrc.grn.anno2json", "Convert annotation table to annotation.json"),
|
|
8
|
+
"serve": ("jsrc.grn.serve", "Start GRN viewer service"),
|
|
9
|
+
"centrality": ("jsrc.grn.centrality", "Compute GRN centrality metrics"),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _register_stub_subcommands(subparsers: Any) -> None:
|
|
14
|
+
for name, (_, help_text) in _SUBCOMMANDS.items():
|
|
15
|
+
subparsers.add_parser(name, help=help_text)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _register_selected_subcommand(subparsers: Any, selected: str) -> bool:
|
|
19
|
+
module_path, _ = _SUBCOMMANDS.get(selected, ("", ""))
|
|
20
|
+
if not module_path:
|
|
21
|
+
return False
|
|
22
|
+
mod = importlib.import_module(module_path)
|
|
23
|
+
reg = getattr(mod, "register", None)
|
|
24
|
+
if reg is None:
|
|
25
|
+
raise AttributeError(f"{module_path}: missing register")
|
|
26
|
+
reg(subparsers)
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_subparser(subparsers: Any, selected_subcommand: str | None = None) -> None:
|
|
31
|
+
grn_parser = subparsers.add_parser("grn", help="GRN conversion and local viewer")
|
|
32
|
+
grn_sub = grn_parser.add_subparsers(dest="grn_cmd")
|
|
33
|
+
grn_parser.set_defaults(_group_parser=grn_parser)
|
|
34
|
+
if selected_subcommand and _register_selected_subcommand(
|
|
35
|
+
grn_sub, selected_subcommand
|
|
36
|
+
):
|
|
37
|
+
return
|
|
38
|
+
_register_stub_subcommands(grn_sub)
|
|
@@ -15,10 +15,10 @@ def annotation_to_json(input_path: str, output_path: str) -> dict[str, dict[str,
|
|
|
15
15
|
for row in reader:
|
|
16
16
|
if not row:
|
|
17
17
|
continue
|
|
18
|
-
gid = str(row[0])
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
anno[gid] = {"p":
|
|
18
|
+
gid = str(row[0])
|
|
19
|
+
desc = str(row[1]) if len(row) > 1 else ""
|
|
20
|
+
map_id = str(row[2]) if len(row) > 2 else ""
|
|
21
|
+
anno[gid] = {"p": map_id, "d": desc}
|
|
22
22
|
write_json(output_path, anno)
|
|
23
23
|
logger.info("Annotation JSON written: %s", output_path)
|
|
24
24
|
return anno
|
|
@@ -57,7 +57,7 @@ def _zip_viewer(viewer_dir: Path, zip_output: str) -> None:
|
|
|
57
57
|
|
|
58
58
|
def cmd(args: Namespace) -> None:
|
|
59
59
|
root = Path(args.dir).expanduser().resolve()
|
|
60
|
-
view_mode = "expand" if args.expand else "auto"
|
|
60
|
+
view_mode = "full" if args.all else "expand" if args.expand else "auto"
|
|
61
61
|
_sync_assets(str(root), view_mode, args.threshold, args.max_nodes)
|
|
62
62
|
if args.grn_json:
|
|
63
63
|
shutil.copy(args.grn_json, root / "json" / "grn.json")
|
|
@@ -17,8 +17,8 @@ def network_to_json(
|
|
|
17
17
|
for row in reader:
|
|
18
18
|
if len(row) < 3:
|
|
19
19
|
continue
|
|
20
|
-
source_id = str(row[0])
|
|
21
|
-
target_id = str(row[1])
|
|
20
|
+
source_id = str(row[0])
|
|
21
|
+
target_id = str(row[1])
|
|
22
22
|
try:
|
|
23
23
|
weight = float(row[2])
|
|
24
24
|
except ValueError:
|
|
@@ -10,7 +10,7 @@ from jsrc.grn.build import _sync_assets
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def cmd(args: Namespace) -> None:
|
|
13
|
-
view_mode = "expand" if args.expand else "auto"
|
|
13
|
+
view_mode = "full" if args.all else "expand" if args.expand else "auto"
|
|
14
14
|
_sync_assets(args.dir, view_mode, args.threshold, 0)
|
|
15
15
|
ensure_dir(f"{args.dir}/json")
|
|
16
16
|
src_grn = os.path.abspath(args.grn_json)
|
|
@@ -229,7 +229,7 @@ function highlightNode(id) {
|
|
|
229
229
|
function downloadListInfo() {
|
|
230
230
|
if (!currentCenterId) return;
|
|
231
231
|
|
|
232
|
-
let content = "GeneID\
|
|
232
|
+
let content = "GeneID\tMapID\tAnnotation\tRelation\tWeight\n";
|
|
233
233
|
|
|
234
234
|
const centerInfo = annotations[currentCenterId] || { p: "", d: "" };
|
|
235
235
|
content += `${currentCenterId}\t${centerInfo.p}\t${centerInfo.d.replace(/[\n\r]/g, " ")}\tCenter\t-\n`;
|
|
@@ -278,7 +278,7 @@ function updateInfoPanel() {
|
|
|
278
278
|
header.innerHTML = `
|
|
279
279
|
<div class="panel-title">${currentCenterId}</div>
|
|
280
280
|
<div class="panel-desc">
|
|
281
|
-
<b>
|
|
281
|
+
<b>MapID:</b> ${info.p || '-'}<br>
|
|
282
282
|
${info.d || '-'}
|
|
283
283
|
</div>
|
|
284
284
|
<div class="panel-stats">
|
|
@@ -308,7 +308,7 @@ function updateInfoPanel() {
|
|
|
308
308
|
<span class="item-val">w:${n.val}</span>
|
|
309
309
|
</div>
|
|
310
310
|
<div class="item-details" onclick="event.stopPropagation()">
|
|
311
|
-
<div class="detail-line"><span class="detail-label">
|
|
311
|
+
<div class="detail-line"><span class="detail-label">MapID:</span> ${nInfo.p || '-'}</div>
|
|
312
312
|
<div class="detail-line"><span class="detail-label">Desc:</span> ${nInfo.d || '-'}</div>
|
|
313
313
|
</div>
|
|
314
314
|
`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
_SUBCOMMANDS: dict[str, tuple[str, str]] = {
|
|
5
|
+
"build": ("jsrc.gs.build", "Build genomic selection datasets"),
|
|
6
|
+
"split": ("jsrc.gs.split", "Split GS datasets into folds"),
|
|
7
|
+
"train": ("jsrc.gs.train", "Train and evaluate GS models"),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _register_stub_subcommands(subparsers: Any) -> None:
|
|
12
|
+
for name, (_, help_text) in _SUBCOMMANDS.items():
|
|
13
|
+
subparsers.add_parser(name, help=help_text)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _register_selected_subcommand(subparsers: Any, selected: str) -> bool:
|
|
17
|
+
module_path, _ = _SUBCOMMANDS.get(selected, ("", ""))
|
|
18
|
+
if not module_path:
|
|
19
|
+
return False
|
|
20
|
+
mod = importlib.import_module(module_path)
|
|
21
|
+
reg = getattr(mod, "register", None)
|
|
22
|
+
if reg is None:
|
|
23
|
+
raise AttributeError(f"{module_path}: missing register")
|
|
24
|
+
reg(subparsers)
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register_subparser(subparsers: Any, selected_subcommand: str | None = None) -> None:
|
|
29
|
+
gs_parser = subparsers.add_parser(
|
|
30
|
+
"gs", help="Genomic selection dataset and model workflows"
|
|
31
|
+
)
|
|
32
|
+
gs_sub = gs_parser.add_subparsers(dest="gs_cmd")
|
|
33
|
+
gs_parser.set_defaults(_group_parser=gs_parser)
|
|
34
|
+
if selected_subcommand and _register_selected_subcommand(
|
|
35
|
+
gs_sub, selected_subcommand
|
|
36
|
+
):
|
|
37
|
+
return
|
|
38
|
+
_register_stub_subcommands(gs_sub)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
_SUBCOMMANDS: dict[str, tuple[str, str]] = {
|
|
5
|
+
"submit": ("jsrc.job.submit", "Submit a background job"),
|
|
6
|
+
"ls": ("jsrc.job.ls", "List jobs"),
|
|
7
|
+
"logs": ("jsrc.job.logs", "Show job logs"),
|
|
8
|
+
"kill": ("jsrc.job.kill", "Terminate a running job"),
|
|
9
|
+
"history": ("jsrc.job.history", "Show job history"),
|
|
10
|
+
"gc": ("jsrc.job.gc", "Garbage-collect old job artifacts"),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _register_stub_subcommands(subparsers: Any) -> None:
|
|
15
|
+
for name, (_, help_text) in _SUBCOMMANDS.items():
|
|
16
|
+
subparsers.add_parser(name, help=help_text)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _register_selected_subcommand(subparsers: Any, selected: str) -> bool:
|
|
20
|
+
module_path, _ = _SUBCOMMANDS.get(selected, ("", ""))
|
|
21
|
+
if not module_path:
|
|
22
|
+
return False
|
|
23
|
+
mod = importlib.import_module(module_path)
|
|
24
|
+
reg = getattr(mod, "register", None)
|
|
25
|
+
if reg is None:
|
|
26
|
+
raise AttributeError(f"{module_path}: missing register")
|
|
27
|
+
reg(subparsers)
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_subparser(subparsers: Any, selected_subcommand: str | None = None) -> None:
|
|
32
|
+
job_parser = subparsers.add_parser("job", help="Track and manage background jobs")
|
|
33
|
+
job_sub = job_parser.add_subparsers(dest="job_cmd")
|
|
34
|
+
job_parser.set_defaults(_group_parser=job_parser)
|
|
35
|
+
if selected_subcommand and _register_selected_subcommand(
|
|
36
|
+
job_sub, selected_subcommand
|
|
37
|
+
):
|
|
38
|
+
return
|
|
39
|
+
_register_stub_subcommands(job_sub)
|
|
@@ -17,7 +17,6 @@ _PLATFORM_NOTE_EMITTED = False
|
|
|
17
17
|
|
|
18
18
|
FIELDS = [
|
|
19
19
|
"job_id",
|
|
20
|
-
"name",
|
|
21
20
|
"submit_time",
|
|
22
21
|
"start_time",
|
|
23
22
|
"end_time",
|
|
@@ -35,6 +34,8 @@ FIELDS = [
|
|
|
35
34
|
"command",
|
|
36
35
|
]
|
|
37
36
|
|
|
37
|
+
DEFAULT_KEEP = 100
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
def now_iso() -> str:
|
|
40
41
|
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
|
@@ -54,6 +55,13 @@ def to_float(value: str, default: float = 0.0) -> float:
|
|
|
54
55
|
return default
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def config_home() -> Path:
|
|
59
|
+
xdg = os.getenv("XDG_CONFIG_HOME")
|
|
60
|
+
if xdg:
|
|
61
|
+
return Path(xdg).expanduser() / "jsrc"
|
|
62
|
+
return Path.home() / ".config" / "jsrc"
|
|
63
|
+
|
|
64
|
+
|
|
57
65
|
def data_home() -> Path:
|
|
58
66
|
xdg = os.getenv("XDG_DATA_HOME")
|
|
59
67
|
if xdg:
|
|
@@ -65,7 +73,7 @@ def history_path() -> Path:
|
|
|
65
73
|
override = os.getenv("JSRC_JOBS_FILE", "")
|
|
66
74
|
if override:
|
|
67
75
|
return Path(override).expanduser()
|
|
68
|
-
return
|
|
76
|
+
return config_home() / "job" / "history"
|
|
69
77
|
|
|
70
78
|
|
|
71
79
|
def default_log_dir() -> Path:
|
|
@@ -82,20 +90,35 @@ def ensure_dirs() -> None:
|
|
|
82
90
|
state_dir().mkdir(parents=True, exist_ok=True)
|
|
83
91
|
|
|
84
92
|
|
|
93
|
+
def _migrate_old_history() -> None:
|
|
94
|
+
old = data_home() / "jobs"
|
|
95
|
+
new = history_path()
|
|
96
|
+
if old.exists() and not new.exists():
|
|
97
|
+
new.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
old.rename(new)
|
|
99
|
+
|
|
100
|
+
|
|
85
101
|
def load_jobs() -> list[dict[str, str]]:
|
|
102
|
+
_migrate_old_history()
|
|
86
103
|
path = history_path()
|
|
87
104
|
if not path.exists():
|
|
88
105
|
return []
|
|
106
|
+
rows = []
|
|
89
107
|
with path.open("r", encoding="utf-8", newline="") as fh:
|
|
90
108
|
reader = csv.DictReader(fh, delimiter="\t")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
for i, row in enumerate(reader):
|
|
110
|
+
try:
|
|
111
|
+
rows.append({k: row.get(k, "") for k in FIELDS})
|
|
112
|
+
except Exception:
|
|
113
|
+
logger.warning("job history line %d: skipping malformed entry", i + 2)
|
|
114
|
+
continue
|
|
115
|
+
return rows
|
|
95
116
|
|
|
96
117
|
|
|
97
118
|
def write_jobs(rows: list[dict[str, str]], keep: int | None = None) -> None:
|
|
98
|
-
if keep is
|
|
119
|
+
if keep is None:
|
|
120
|
+
keep = DEFAULT_KEEP
|
|
121
|
+
if keep > 0 and len(rows) > keep:
|
|
99
122
|
rows = rows[-keep:]
|
|
100
123
|
path = history_path()
|
|
101
124
|
with path.open("w", encoding="utf-8", newline="") as fh:
|
|
@@ -275,7 +298,11 @@ def to_row_view(row: dict[str, str], live: dict[str, str]) -> dict[str, str]:
|
|
|
275
298
|
avg_kb = int(sum_kb / samples) if samples > 0 else rss_kb
|
|
276
299
|
runtime_sec = runtime_seconds(row, live)
|
|
277
300
|
out = dict(row)
|
|
278
|
-
|
|
301
|
+
rss_mb_val = rss_kb / 1024
|
|
302
|
+
rss_display = f"{rss_mb_val:.1f}" if rss_mb_val < 1024 else f"{rss_mb_val / 1024:.1f}g"
|
|
303
|
+
out["rss_mb"] = rss_display
|
|
304
|
+
out["rss"] = rss_display
|
|
305
|
+
out["mem"] = rss_display
|
|
279
306
|
out["rss_min_mb"] = f"{min_kb / 1024:.1f}"
|
|
280
307
|
out["rss_avg_mb"] = f"{avg_kb / 1024:.1f}"
|
|
281
308
|
out["rss_peak_mb"] = f"{peak_kb / 1024:.1f}"
|
|
@@ -285,6 +312,19 @@ def to_row_view(row: dict[str, str], live: dict[str, str]) -> dict[str, str]:
|
|
|
285
312
|
out["runtime"] = format_duration(runtime_sec)
|
|
286
313
|
out["cpu_pct"] = f"{to_float(live.get('pcpu', '0'), 0.0):.1f}"
|
|
287
314
|
out["state"] = live.get("stat", "")
|
|
315
|
+
st = row.get("status", "")
|
|
316
|
+
out["s"] = {"running": "R", "exited": "E", "failed": "F", "killed": "K", "lost": "L"}.get(
|
|
317
|
+
st, st
|
|
318
|
+
)
|
|
319
|
+
submit = row.get("submit_time", "")
|
|
320
|
+
if submit:
|
|
321
|
+
try:
|
|
322
|
+
dt = datetime.fromisoformat(submit)
|
|
323
|
+
out["time"] = f"{dt.strftime('%Y-%m-%d %H:%M')} / {out.get('runtime', '')}"
|
|
324
|
+
except (TypeError, ValueError):
|
|
325
|
+
out["time"] = f"{submit} / {out.get('runtime', '')}"
|
|
326
|
+
else:
|
|
327
|
+
out["time"] = f" / {out.get('runtime', '')}"
|
|
288
328
|
return out
|
|
289
329
|
|
|
290
330
|
|
|
@@ -360,18 +400,26 @@ def filter_rows(rows: list[dict[str, str]], query: str) -> list[dict[str, str]]:
|
|
|
360
400
|
def sort_rows(
|
|
361
401
|
rows: list[dict[str, str]], key: str, reverse: bool
|
|
362
402
|
) -> list[dict[str, str]]:
|
|
363
|
-
if key
|
|
403
|
+
if key in {"submit_time", "time"}:
|
|
364
404
|
return sorted(rows, key=lambda r: r.get("submit_time", ""), reverse=reverse)
|
|
365
405
|
if key == "pid":
|
|
366
406
|
return sorted(rows, key=lambda r: to_int(r.get("pid", "0")), reverse=reverse)
|
|
367
407
|
if key == "job_id":
|
|
368
408
|
return sorted(rows, key=lambda r: to_int(r.get("job_id", "0")), reverse=reverse)
|
|
369
|
-
if key
|
|
409
|
+
if key in {"status", "s"}:
|
|
370
410
|
return sorted(rows, key=lambda r: r.get("status", ""), reverse=reverse)
|
|
371
411
|
if key == "rss_mb":
|
|
372
412
|
return sorted(
|
|
373
413
|
rows, key=lambda r: to_float(r.get("rss_mb", "0"), 0.0), reverse=reverse
|
|
374
414
|
)
|
|
415
|
+
if key == "rss":
|
|
416
|
+
return sorted(
|
|
417
|
+
rows, key=lambda r: to_int(r.get("rss_kb_last", "0"), 0), reverse=reverse
|
|
418
|
+
)
|
|
419
|
+
if key == "mem":
|
|
420
|
+
return sorted(
|
|
421
|
+
rows, key=lambda r: to_int(r.get("rss_kb_last", "0"), 0), reverse=reverse
|
|
422
|
+
)
|
|
375
423
|
if key == "rss_min_mb":
|
|
376
424
|
return sorted(
|
|
377
425
|
rows, key=lambda r: to_float(r.get("rss_min_mb", "0"), 0.0), reverse=reverse
|
|
@@ -397,15 +445,15 @@ def print_table(rows: list[dict[str, str]], columns: list[str]) -> None:
|
|
|
397
445
|
if not rows:
|
|
398
446
|
print("(no records)")
|
|
399
447
|
return
|
|
400
|
-
widths = {c: len(c) for c in columns}
|
|
448
|
+
widths = {c: len(c.upper()) for c in columns}
|
|
401
449
|
for row in rows:
|
|
402
450
|
for c in columns:
|
|
403
451
|
widths[c] = max(widths[c], len(str(row.get(c, ""))))
|
|
404
|
-
header = "
|
|
452
|
+
header = " ".join(c.upper().ljust(widths[c]) for c in columns)
|
|
405
453
|
print(header)
|
|
406
|
-
print("
|
|
454
|
+
print(" ".join("-" * widths[c] for c in columns))
|
|
407
455
|
for row in rows:
|
|
408
|
-
print("
|
|
456
|
+
print(" ".join(str(row.get(c, "")).ljust(widths[c]) for c in columns))
|
|
409
457
|
|
|
410
458
|
|
|
411
459
|
def print_rows(rows: list[dict[str, str]], columns: list[str], fmt: str) -> None:
|
|
@@ -466,7 +514,7 @@ def collect_render_rows(args: Any, refresh: bool) -> list[dict[str, str]]:
|
|
|
466
514
|
if refresh:
|
|
467
515
|
rows, changed = refresh_jobs(rows)
|
|
468
516
|
if changed:
|
|
469
|
-
write_jobs(rows
|
|
517
|
+
write_jobs(rows)
|
|
470
518
|
rows = filter_rows(rows, args.query)
|
|
471
519
|
rendered = []
|
|
472
520
|
for row in rows:
|
|
@@ -3,7 +3,7 @@ from argparse import Namespace
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from jsrc.job.core import load_jobs, now_iso, runtime_seconds, state_dir, write_jobs
|
|
6
|
+
from jsrc.job.core import DEFAULT_KEEP, load_jobs, now_iso, runtime_seconds, state_dir, write_jobs
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
@@ -40,7 +40,7 @@ def register(subparsers: Any) -> None:
|
|
|
40
40
|
"-k",
|
|
41
41
|
"--keep-history",
|
|
42
42
|
type=int,
|
|
43
|
-
default=
|
|
43
|
+
default=DEFAULT_KEEP,
|
|
44
44
|
help="Keep last N history records",
|
|
45
45
|
)
|
|
46
46
|
p.add_argument(
|
|
@@ -5,6 +5,7 @@ from jsrc.job.core import (
|
|
|
5
5
|
filter_rows,
|
|
6
6
|
load_jobs,
|
|
7
7
|
print_rows,
|
|
8
|
+
refresh_jobs,
|
|
8
9
|
to_row_view,
|
|
9
10
|
warn_portability_limits,
|
|
10
11
|
)
|
|
@@ -13,6 +14,7 @@ from jsrc.job.core import (
|
|
|
13
14
|
def cmd(args: Namespace) -> None:
|
|
14
15
|
warn_portability_limits()
|
|
15
16
|
rows = load_jobs()
|
|
17
|
+
rows, _ = refresh_jobs(rows)
|
|
16
18
|
rows = filter_rows(rows, args.query)
|
|
17
19
|
if args.limit > 0:
|
|
18
20
|
rows = rows[-args.limit :]
|
|
@@ -21,18 +23,10 @@ def cmd(args: Namespace) -> None:
|
|
|
21
23
|
view = to_row_view(row, {})
|
|
22
24
|
rendered.append(view)
|
|
23
25
|
cols = [
|
|
24
|
-
"job_id",
|
|
25
|
-
"status",
|
|
26
26
|
"pid",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"runtime_sec",
|
|
31
|
-
"rss_mb",
|
|
32
|
-
"rss_min_mb",
|
|
33
|
-
"rss_avg_mb",
|
|
34
|
-
"rss_peak_mb",
|
|
35
|
-
"log_path",
|
|
27
|
+
"s",
|
|
28
|
+
"mem",
|
|
29
|
+
"time",
|
|
36
30
|
"command",
|
|
37
31
|
]
|
|
38
32
|
print_rows(rendered, cols, args.format)
|
|
@@ -30,12 +30,12 @@ def cmd(args: Namespace) -> None:
|
|
|
30
30
|
os.killpg(pgid, sig)
|
|
31
31
|
else:
|
|
32
32
|
os.kill(pid, sig)
|
|
33
|
-
except ProcessLookupError:
|
|
33
|
+
except (ProcessLookupError, FileNotFoundError):
|
|
34
34
|
pass
|
|
35
35
|
row["status"] = "killed"
|
|
36
36
|
row["end_time"] = now_iso()
|
|
37
37
|
row["runtime_sec"] = str(runtime_seconds(row, {}))
|
|
38
|
-
write_jobs(rows
|
|
38
|
+
write_jobs(rows)
|
|
39
39
|
print(f"killed\t{pid}")
|
|
40
40
|
print(f"signal\t{args.signal}")
|
|
41
41
|
|