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.
Files changed (104) hide show
  1. {jsrc-0.2.2/src/jsrc.egg-info → jsrc-0.2.4}/PKG-INFO +1 -1
  2. {jsrc-0.2.2 → jsrc-0.2.4}/pyproject.toml +1 -1
  3. jsrc-0.2.4/src/jsrc/analyze/__init__.py +39 -0
  4. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/cli.py +78 -32
  5. jsrc-0.2.4/src/jsrc/grn/__init__.py +38 -0
  6. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/anno2json.py +4 -4
  7. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/build.py +1 -1
  8. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/net2json.py +2 -2
  9. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/serve.py +1 -1
  10. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/script.js +3 -3
  11. jsrc-0.2.4/src/jsrc/gs/__init__.py +38 -0
  12. jsrc-0.2.4/src/jsrc/job/__init__.py +39 -0
  13. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/core.py +63 -15
  14. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/gc.py +2 -2
  15. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/history.py +5 -11
  16. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/kill.py +2 -2
  17. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/ls.py +9 -8
  18. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/submit.py +2 -2
  19. jsrc-0.2.4/src/jsrc/plot/__init__.py +42 -0
  20. jsrc-0.2.4/src/jsrc/plot/heart.py +82 -0
  21. jsrc-0.2.4/src/jsrc/seq/__init__.py +43 -0
  22. jsrc-0.2.4/src/jsrc/vision/__init__.py +38 -0
  23. {jsrc-0.2.2 → jsrc-0.2.4/src/jsrc.egg-info}/PKG-INFO +1 -1
  24. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/SOURCES.txt +0 -1
  25. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_cli_module_flows.py +56 -0
  26. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_grn_conversion.py +10 -1
  27. jsrc-0.2.2/src/jsrc/analyze/__init__.py +0 -16
  28. jsrc-0.2.2/src/jsrc/grn/__init__.py +0 -15
  29. jsrc-0.2.2/src/jsrc/gs/__init__.py +0 -15
  30. jsrc-0.2.2/src/jsrc/job/__init__.py +0 -17
  31. jsrc-0.2.2/src/jsrc/job/show.py +0 -51
  32. jsrc-0.2.2/src/jsrc/plot/__init__.py +0 -29
  33. jsrc-0.2.2/src/jsrc/plot/heart.py +0 -37
  34. jsrc-0.2.2/src/jsrc/seq/__init__.py +0 -31
  35. jsrc-0.2.2/src/jsrc/vision/__init__.py +0 -15
  36. {jsrc-0.2.2 → jsrc-0.2.4}/LICENSE +0 -0
  37. {jsrc-0.2.2 → jsrc-0.2.4}/README.md +0 -0
  38. {jsrc-0.2.2 → jsrc-0.2.4}/setup.cfg +0 -0
  39. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/__init__.py +0 -0
  40. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/bootstrap_phylo.py +0 -0
  41. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/core.py +0 -0
  42. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/motif.py +0 -0
  43. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/msa_consensus.py +0 -0
  44. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/phylo.py +0 -0
  45. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/qc.py +0 -0
  46. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/analyze/snpindel.py +0 -0
  47. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/core.py +0 -0
  48. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/centrality.py +0 -0
  49. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/core.py +0 -0
  50. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/index.html +0 -0
  51. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/grn/sources/style.css +0 -0
  52. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/build.py +0 -0
  53. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/split.py +0 -0
  54. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/gs/train.py +0 -0
  55. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/job/logs.py +0 -0
  56. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/chromosome.py +0 -0
  57. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/circoslite.py +0 -0
  58. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/cis.py +0 -0
  59. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/core.py +0 -0
  60. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/domain.py +0 -0
  61. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/dotplot.py +0 -0
  62. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/exon.py +0 -0
  63. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/gene.py +0 -0
  64. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/plot/rose.py +0 -0
  65. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/codon.py +0 -0
  66. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/core.py +0 -0
  67. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/digest.py +0 -0
  68. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/extract.py +0 -0
  69. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/fetch.py +0 -0
  70. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/kmer.py +0 -0
  71. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/promoter.py +0 -0
  72. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/qc.py +0 -0
  73. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/rename.py +0 -0
  74. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/translate.py +0 -0
  75. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/seq/window.py +0 -0
  76. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/core.py +0 -0
  77. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/efd.py +0 -0
  78. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/extract.py +0 -0
  79. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc/vision/traits.py +0 -0
  80. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/dependency_links.txt +0 -0
  81. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/entry_points.txt +0 -0
  82. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/requires.txt +0 -0
  83. {jsrc-0.2.2 → jsrc-0.2.4}/src/jsrc.egg-info/top_level.txt +0 -0
  84. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_analyze_extra.py +0 -0
  85. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_analyze_phylo.py +0 -0
  86. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_cli_error_behavior.py +0 -0
  87. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_gs_train.py +0 -0
  88. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_job_core.py +0 -0
  89. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_job_portability.py +0 -0
  90. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_plot_commands.py +0 -0
  91. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_plot_core.py +0 -0
  92. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_progress.py +0 -0
  93. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_codon_kmer.py +0 -0
  94. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_core.py +0 -0
  95. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_digest.py +0 -0
  96. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_extract.py +0 -0
  97. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_fetch.py +0 -0
  98. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_promoter.py +0 -0
  99. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_qc.py +0 -0
  100. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_rename.py +0 -0
  101. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_translate.py +0 -0
  102. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_seq_window.py +0 -0
  103. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_vision_efd.py +0 -0
  104. {jsrc-0.2.2 → jsrc-0.2.4}/tests/test_vision_extract.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsrc
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Python library for bioinformatics and scientific computing
5
5
  Author-email: Jiaoyuan <imjiaoyuan@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jsrc"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Python library for bioinformatics and scientific computing"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 _register_modules(
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
- subparsers = parser.add_subparsers(dest="command", help="Available modules")
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
- loaded, errors = _register_modules(subparsers, debug=debug_mode)
75
- if errors:
76
- logging.warning("some modules failed to load:")
77
- for item in errors:
78
- logging.warning(" - %s", item)
79
- if not loaded:
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]).replace("_", "-")
19
- ptr = str(row[1]) if len(row) > 1 else ""
20
- desc = str(row[2]) if len(row) > 2 else ""
21
- anno[gid] = {"p": ptr, "d": desc}
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]).replace("_", "-")
21
- target_id = str(row[1]).replace("_", "-")
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\tPotriID\tAnnotation\tRelation\tWeight\n";
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>Ptr:</b> ${info.p || '-'}<br>
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">Potri:</span> ${nInfo.p || '-'}</div>
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 data_home() / "jobs"
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
- rows = []
92
- for row in reader:
93
- rows.append({k: row.get(k, "") for k in FIELDS})
94
- return rows
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 not None and keep > 0 and len(rows) > keep:
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
- out["rss_mb"] = f"{rss_kb / 1024:.1f}"
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 == "submit_time":
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 == "status":
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 = " ".join(c.ljust(widths[c]) for c in columns)
452
+ header = " ".join(c.upper().ljust(widths[c]) for c in columns)
405
453
  print(header)
406
- print(" ".join("-" * widths[c] for c in columns))
454
+ print(" ".join("-" * widths[c] for c in columns))
407
455
  for row in rows:
408
- print(" ".join(str(row.get(c, "")).ljust(widths[c]) for c in columns))
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, keep=1000)
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=1000,
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
- "submit_time",
28
- "end_time",
29
- "runtime",
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, keep=1000)
38
+ write_jobs(rows)
39
39
  print(f"killed\t{pid}")
40
40
  print(f"signal\t{args.signal}")
41
41