roxylib 0.2.0__py3-none-any.whl
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.
- roxy/__init__.py +26 -0
- roxy/cli/__init__.py +7 -0
- roxy/cli/_utils.py +24 -0
- roxy/cli/compute.py +167 -0
- roxy/cli/describe_descriptor.py +101 -0
- roxy/cli/init_config.py +61 -0
- roxy/cli/list_descriptors.py +78 -0
- roxy/cli/main.py +65 -0
- roxy/core/__init__.py +17 -0
- roxy/core/constants.py +450 -0
- roxy/core/exceptions.py +17 -0
- roxy/core/io.py +140 -0
- roxy/data/__init__.py +1 -0
- roxy/data/aaindex.csv +567 -0
- roxy/descriptors/__init__.py +22 -0
- roxy/descriptors/_utils.py +284 -0
- roxy/descriptors/aaindex/__init__.py +5 -0
- roxy/descriptors/aaindex/aaindex.py +138 -0
- roxy/descriptors/autocorrelation/__init__.py +5 -0
- roxy/descriptors/autocorrelation/autocorrelation.py +195 -0
- roxy/descriptors/base.py +82 -0
- roxy/descriptors/complexity/__init__.py +11 -0
- roxy/descriptors/complexity/entropy_complexity.py +166 -0
- roxy/descriptors/complexity/local_repetition.py +217 -0
- roxy/descriptors/complexity/run_blockiness.py +128 -0
- roxy/descriptors/composition/__init__.py +15 -0
- roxy/descriptors/composition/aac.py +63 -0
- roxy/descriptors/composition/compositional_bias.py +147 -0
- roxy/descriptors/composition/dpc.py +70 -0
- roxy/descriptors/composition/grouped.py +54 -0
- roxy/descriptors/composition/kmer_full_alphabet.py +86 -0
- roxy/descriptors/ctd/__init__.py +9 -0
- roxy/descriptors/ctd/ctd_classic.py +155 -0
- roxy/descriptors/ctd/distribution.py +130 -0
- roxy/descriptors/hybrid/__init__.py +5 -0
- roxy/descriptors/hybrid/family_summary.py +205 -0
- roxy/descriptors/motif/__init__.py +13 -0
- roxy/descriptors/motif/functional_residue_content.py +123 -0
- roxy/descriptors/motif/pattern.py +112 -0
- roxy/descriptors/motif/spacing.py +185 -0
- roxy/descriptors/motif/user_regex.py +128 -0
- roxy/descriptors/physicochemical/__init__.py +15 -0
- roxy/descriptors/physicochemical/charge.py +172 -0
- roxy/descriptors/physicochemical/global_basic.py +202 -0
- roxy/descriptors/physicochemical/hydrophobicity.py +191 -0
- roxy/descriptors/physicochemical/order_disorder.py +165 -0
- roxy/descriptors/physicochemical/structural_propensity.py +140 -0
- roxy/descriptors/positional/__init__.py +11 -0
- roxy/descriptors/positional/normalized.py +122 -0
- roxy/descriptors/positional/sliding_window.py +124 -0
- roxy/descriptors/positional/terminal.py +116 -0
- roxy/descriptors/pseudo/__init__.py +11 -0
- roxy/descriptors/pseudo/pseaac.py +122 -0
- roxy/descriptors/pseudo/qso.py +114 -0
- roxy/descriptors/pseudo/sequence_order.py +159 -0
- roxy/descriptors/registry.py +36 -0
- roxylib-0.2.0.dist-info/METADATA +154 -0
- roxylib-0.2.0.dist-info/RECORD +61 -0
- roxylib-0.2.0.dist-info/WHEEL +4 -0
- roxylib-0.2.0.dist-info/entry_points.txt +2 -0
- roxylib-0.2.0.dist-info/licenses/LICENSE +21 -0
roxy/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Roxy — protein sequence descriptors for machine learning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
|
|
7
|
+
from .core import (
|
|
8
|
+
RoxyError,
|
|
9
|
+
RoxyIOError,
|
|
10
|
+
read_fasta,
|
|
11
|
+
read_sequences,
|
|
12
|
+
write_table,
|
|
13
|
+
)
|
|
14
|
+
from .descriptors import DESCRIPTOR_REGISTRY, BaseDescriptor, register
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DESCRIPTOR_REGISTRY",
|
|
18
|
+
"BaseDescriptor",
|
|
19
|
+
"RoxyError",
|
|
20
|
+
"RoxyIOError",
|
|
21
|
+
"__version__",
|
|
22
|
+
"read_fasta",
|
|
23
|
+
"read_sequences",
|
|
24
|
+
"register",
|
|
25
|
+
"write_table",
|
|
26
|
+
]
|
roxy/cli/__init__.py
ADDED
roxy/cli/_utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared CLI utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from roxy.descriptors import DESCRIPTOR_REGISTRY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _resolve_names(
|
|
9
|
+
all_: bool,
|
|
10
|
+
descriptors: list[str],
|
|
11
|
+
family: list[str],
|
|
12
|
+
) -> list[str]:
|
|
13
|
+
if all_:
|
|
14
|
+
return sorted(DESCRIPTOR_REGISTRY)
|
|
15
|
+
names: list[str] = list(descriptors)
|
|
16
|
+
for fam in family:
|
|
17
|
+
names += [n for n, c in DESCRIPTOR_REGISTRY.items() if c.family == fam]
|
|
18
|
+
seen: set[str] = set()
|
|
19
|
+
result: list[str] = []
|
|
20
|
+
for n in names:
|
|
21
|
+
if n not in seen:
|
|
22
|
+
seen.add(n)
|
|
23
|
+
result.append(n)
|
|
24
|
+
return sorted(result)
|
roxy/cli/compute.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""``roxy compute`` — run descriptors on a sequence file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
import typer
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from roxy.cli._utils import _resolve_names
|
|
15
|
+
from roxy.core.exceptions import RoxyIOError
|
|
16
|
+
from roxy.core.io import read_sequences, write_table
|
|
17
|
+
from roxy.descriptors import DESCRIPTOR_REGISTRY
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from roxy.descriptors.base import BaseDescriptor
|
|
21
|
+
|
|
22
|
+
_BASE_COLS = {"length", "valid_residue_count"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_config(path: Path) -> dict[str, dict[str, object]]:
|
|
26
|
+
try:
|
|
27
|
+
raw = yaml.safe_load(path.read_text())
|
|
28
|
+
except yaml.YAMLError as exc:
|
|
29
|
+
msg = f"Invalid YAML in {path}: {exc}"
|
|
30
|
+
raise typer.BadParameter(msg) from exc
|
|
31
|
+
|
|
32
|
+
if not isinstance(raw, dict):
|
|
33
|
+
msg = "Config must be a YAML mapping of descriptor names to params."
|
|
34
|
+
raise typer.BadParameter(msg)
|
|
35
|
+
|
|
36
|
+
validated: dict[str, dict[str, object]] = {}
|
|
37
|
+
for name, raw_params in raw.items():
|
|
38
|
+
if name not in DESCRIPTOR_REGISTRY:
|
|
39
|
+
available = ", ".join(sorted(DESCRIPTOR_REGISTRY))
|
|
40
|
+
msg = f"Unknown descriptor {name!r}.\nAvailable: {available}"
|
|
41
|
+
raise typer.BadParameter(msg)
|
|
42
|
+
norm_params: dict[str, object] = raw_params or {}
|
|
43
|
+
if not isinstance(norm_params, dict):
|
|
44
|
+
msg = f"Params for {name!r} must be a mapping, got {type(norm_params).__name__}."
|
|
45
|
+
raise typer.BadParameter(msg)
|
|
46
|
+
sig = inspect.signature(DESCRIPTOR_REGISTRY[name].__init__)
|
|
47
|
+
valid = {k for k in sig.parameters if k != "self"}
|
|
48
|
+
unknown = set(norm_params) - valid
|
|
49
|
+
if unknown:
|
|
50
|
+
msg = (
|
|
51
|
+
f"Unknown param(s) for {name!r}: {', '.join(sorted(unknown))}.\n"
|
|
52
|
+
f"Valid: {', '.join(sorted(valid))}"
|
|
53
|
+
)
|
|
54
|
+
raise typer.BadParameter(msg)
|
|
55
|
+
validated[name] = dict(norm_params)
|
|
56
|
+
return validated
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_instances(
|
|
60
|
+
config: dict[str, dict[str, object]] | None,
|
|
61
|
+
names: list[str],
|
|
62
|
+
) -> list[tuple[str, BaseDescriptor]]:
|
|
63
|
+
if config is not None:
|
|
64
|
+
return [(n, DESCRIPTOR_REGISTRY[n](**p)) for n, p in config.items()]
|
|
65
|
+
return [(n, DESCRIPTOR_REGISTRY[n]()) for n in names]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_descriptors(
|
|
69
|
+
instances: list[tuple[str, BaseDescriptor]],
|
|
70
|
+
seqs: list[str],
|
|
71
|
+
ids: list[str],
|
|
72
|
+
*,
|
|
73
|
+
no_progress: bool,
|
|
74
|
+
) -> tuple[list[pl.DataFrame], list[str]]:
|
|
75
|
+
frames: list[pl.DataFrame] = []
|
|
76
|
+
desc_names: list[str] = []
|
|
77
|
+
|
|
78
|
+
if no_progress:
|
|
79
|
+
for name, inst in instances:
|
|
80
|
+
frames.append(inst.compute(seqs, ids=ids))
|
|
81
|
+
desc_names.append(name)
|
|
82
|
+
return frames, desc_names
|
|
83
|
+
|
|
84
|
+
from rich.progress import Progress # noqa: PLC0415
|
|
85
|
+
|
|
86
|
+
with Progress() as progress:
|
|
87
|
+
task = progress.add_task("Computing descriptors...", total=len(instances))
|
|
88
|
+
for name, inst in instances:
|
|
89
|
+
progress.update(task, description=f"[cyan]{name}[/cyan]")
|
|
90
|
+
frames.append(inst.compute(seqs, ids=ids))
|
|
91
|
+
desc_names.append(name)
|
|
92
|
+
progress.advance(task)
|
|
93
|
+
return frames, desc_names
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _concat_frames(frames: list[pl.DataFrame], desc_names: list[str]) -> pl.DataFrame:
|
|
97
|
+
"""Horizontally concat descriptor frames, keeping id and base cols only from first."""
|
|
98
|
+
_drop_from_rest = _BASE_COLS | {"id"}
|
|
99
|
+
parts: list[pl.DataFrame] = []
|
|
100
|
+
for i, (df, name) in enumerate(zip(frames, desc_names, strict=True)):
|
|
101
|
+
if i == 0:
|
|
102
|
+
rename = {f"{name}_{col}": col for col in _BASE_COLS if f"{name}_{col}" in df.columns}
|
|
103
|
+
parts.append(df.rename(rename))
|
|
104
|
+
else:
|
|
105
|
+
drop = [c for col in _drop_from_rest for c in (f"{name}_{col}", col) if c in df.columns]
|
|
106
|
+
parts.append(df.drop(drop))
|
|
107
|
+
return functools.reduce(pl.DataFrame.hstack, parts)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def compute( # noqa: PLR0917
|
|
111
|
+
input_path: Path = typer.Argument(..., metavar="INPUT", help="FASTA, CSV, or Parquet file."),
|
|
112
|
+
config: Path | None = typer.Option(
|
|
113
|
+
None, "--config", "-c", help="YAML config file with descriptor params."
|
|
114
|
+
),
|
|
115
|
+
all_: bool = typer.Option(False, "--all", help="Run all registered descriptors."),
|
|
116
|
+
descriptors: list[str] = typer.Option([], "--descriptor", "-d", help="Descriptor name (repeatable)."),
|
|
117
|
+
family: list[str] = typer.Option(
|
|
118
|
+
[], "--family", "-f", help="Family name — runs all descriptors in it (repeatable)."
|
|
119
|
+
),
|
|
120
|
+
output: Path = typer.Option(..., "--output", "-o", help="Output file (.csv or .parquet)."),
|
|
121
|
+
seq_col: str = typer.Option("sequence", "--seq-col", help="Sequence column name (CSV/Parquet)."),
|
|
122
|
+
id_col: str | None = typer.Option(None, "--id-col", help="ID column to preserve (CSV/Parquet)."),
|
|
123
|
+
no_progress: bool = typer.Option(False, "--no-progress", help="Disable progress bar."),
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Compute descriptors for sequences in INPUT and write to OUTPUT."""
|
|
126
|
+
if config is not None and (all_ or descriptors or family):
|
|
127
|
+
typer.echo("--config is mutually exclusive with --all / -d / -f.", err=True)
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
|
|
130
|
+
if config is not None:
|
|
131
|
+
cfg = _load_config(config)
|
|
132
|
+
instances = _build_instances(cfg, [])
|
|
133
|
+
else:
|
|
134
|
+
names = _resolve_names(all_, descriptors, family)
|
|
135
|
+
if not names:
|
|
136
|
+
typer.echo("Specify --config, --all, -d <name>, or -f <family>.", err=True)
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
unknown = [n for n in names if n not in DESCRIPTOR_REGISTRY]
|
|
139
|
+
if unknown:
|
|
140
|
+
typer.echo(f"Unknown descriptors: {', '.join(unknown)}", err=True)
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
instances = _build_instances(None, names)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
records = read_sequences(input_path, seq_col=seq_col, id_col=id_col)
|
|
146
|
+
except RoxyIOError as exc:
|
|
147
|
+
typer.echo(str(exc), err=True)
|
|
148
|
+
raise typer.Exit(1) from exc
|
|
149
|
+
|
|
150
|
+
ids = [r[0] for r in records]
|
|
151
|
+
seqs = [r[1] for r in records]
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
frames, desc_names = _run_descriptors(instances, seqs, ids, no_progress=no_progress)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
typer.echo(f"Error computing descriptors: {exc}", err=True)
|
|
157
|
+
raise typer.Exit(1) from exc
|
|
158
|
+
|
|
159
|
+
result = _concat_frames(frames, desc_names)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
write_table(result, output)
|
|
163
|
+
except RoxyIOError as exc:
|
|
164
|
+
typer.echo(str(exc), err=True)
|
|
165
|
+
raise typer.Exit(1) from exc
|
|
166
|
+
|
|
167
|
+
typer.echo(f"Wrote {result.shape[0]} sequences x {result.shape[1]} features → {output}")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""``roxy describe`` — show full details for a registered descriptor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from roxy.core.constants import AA20_ORDERED
|
|
14
|
+
from roxy.descriptors import DESCRIPTOR_REGISTRY
|
|
15
|
+
|
|
16
|
+
_SEQ = "".join(AA20_ORDERED)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_google_args(doc: str) -> dict[str, str]:
|
|
20
|
+
"""Extract param descriptions from a Google-style Args section."""
|
|
21
|
+
args: dict[str, str] = {}
|
|
22
|
+
in_args = False
|
|
23
|
+
current: str | None = None
|
|
24
|
+
parts: list[str] = []
|
|
25
|
+
|
|
26
|
+
for line in doc.splitlines():
|
|
27
|
+
stripped = line.strip()
|
|
28
|
+
if stripped == "Args:":
|
|
29
|
+
in_args = True
|
|
30
|
+
continue
|
|
31
|
+
if not in_args:
|
|
32
|
+
continue
|
|
33
|
+
# New top-level section ends Args
|
|
34
|
+
if stripped and not line.startswith(" "):
|
|
35
|
+
break
|
|
36
|
+
# Param line: exactly 4-space indent with ": "
|
|
37
|
+
if line.startswith(" ") and not line.startswith(" ") and ": " in stripped:
|
|
38
|
+
if current is not None:
|
|
39
|
+
args[current] = " ".join(parts)
|
|
40
|
+
param, _, desc = stripped.partition(": ")
|
|
41
|
+
current, parts = param.strip(), [desc.strip()]
|
|
42
|
+
elif line.startswith(" ") and current is not None:
|
|
43
|
+
parts.append(stripped)
|
|
44
|
+
|
|
45
|
+
if current is not None:
|
|
46
|
+
args[current] = " ".join(parts)
|
|
47
|
+
return args
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _summary(doc: str) -> str:
|
|
51
|
+
return doc.splitlines()[0] if doc else ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def describe_descriptor(name: str = typer.Argument(..., help="Descriptor name.")) -> None:
|
|
55
|
+
"""Show family, parameters, column count, and column names for a descriptor."""
|
|
56
|
+
cls = DESCRIPTOR_REGISTRY.get(name)
|
|
57
|
+
if cls is None:
|
|
58
|
+
available = ", ".join(sorted(DESCRIPTOR_REGISTRY))
|
|
59
|
+
typer.echo(f"Unknown descriptor: {name!r}\nAvailable: {available}", err=True)
|
|
60
|
+
raise typer.Exit(1)
|
|
61
|
+
|
|
62
|
+
console = Console()
|
|
63
|
+
|
|
64
|
+
doc = inspect.getdoc(cls) or ""
|
|
65
|
+
sig = inspect.signature(cls.__init__)
|
|
66
|
+
params = {k: v for k, v in sig.parameters.items() if k != "self"}
|
|
67
|
+
arg_descs = _parse_google_args(doc)
|
|
68
|
+
|
|
69
|
+
inst = cls()
|
|
70
|
+
columns = list(inst.compute_one(_SEQ))
|
|
71
|
+
|
|
72
|
+
header = Text()
|
|
73
|
+
header.append(name, style="bold cyan")
|
|
74
|
+
header.append(f" [{cls.family}]", style="green")
|
|
75
|
+
console.print(header)
|
|
76
|
+
console.print()
|
|
77
|
+
if doc:
|
|
78
|
+
console.print(_summary(doc))
|
|
79
|
+
console.print()
|
|
80
|
+
|
|
81
|
+
if params:
|
|
82
|
+
ptable = Table(show_header=True, header_style="bold", box=None, padding=(0, 2, 0, 0))
|
|
83
|
+
ptable.add_column("Parameter")
|
|
84
|
+
ptable.add_column("Default")
|
|
85
|
+
ptable.add_column("Annotation")
|
|
86
|
+
ptable.add_column("Description")
|
|
87
|
+
for pname, p in params.items():
|
|
88
|
+
default = "" if p.default is inspect.Parameter.empty else repr(p.default)
|
|
89
|
+
ann = "" if p.annotation is inspect.Parameter.empty else str(p.annotation)
|
|
90
|
+
desc = arg_descs.get(pname, "")
|
|
91
|
+
ptable.add_row(pname, default, ann, f"[dim]{desc}[/dim]")
|
|
92
|
+
console.print(Panel(ptable, title="Parameters", title_align="left"))
|
|
93
|
+
|
|
94
|
+
col_text = " ".join(f"[dim]{c}[/dim]" for c in columns)
|
|
95
|
+
console.print(
|
|
96
|
+
Panel(
|
|
97
|
+
f"[bold]{len(columns)} columns[/bold] (default parameters)\n\n{col_text}",
|
|
98
|
+
title="Output columns",
|
|
99
|
+
title_align="left",
|
|
100
|
+
)
|
|
101
|
+
)
|
roxy/cli/init_config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""``roxy init-config`` — generate a YAML config with descriptor defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from roxy.cli._utils import _resolve_names
|
|
13
|
+
from roxy.descriptors import DESCRIPTOR_REGISTRY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _default_params(cls: type) -> dict[str, object]:
|
|
17
|
+
sig = inspect.signature(cls.__init__)
|
|
18
|
+
return {
|
|
19
|
+
name: param.default
|
|
20
|
+
for name, param in sig.parameters.items()
|
|
21
|
+
if name != "self" and param.default is not inspect.Parameter.empty
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init_config(
|
|
26
|
+
all_: bool = typer.Option(False, "--all", help="Include all registered descriptors."),
|
|
27
|
+
descriptors: list[str] = typer.Option([], "--descriptor", "-d", help="Descriptor name (repeatable)."),
|
|
28
|
+
family: list[str] = typer.Option([], "--family", "-f", help="Family name (repeatable)."),
|
|
29
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output file (default: stdout)."),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Generate a YAML config file populated with descriptor defaults."""
|
|
32
|
+
names = _resolve_names(all_, descriptors, family)
|
|
33
|
+
|
|
34
|
+
if not names:
|
|
35
|
+
typer.echo("Specify --all, -d <name>, or -f <family>.", err=True)
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
unknown = [n for n in names if n not in DESCRIPTOR_REGISTRY]
|
|
39
|
+
if unknown:
|
|
40
|
+
typer.echo(f"Unknown descriptors: {', '.join(unknown)}", err=True)
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
header = (
|
|
44
|
+
"# Roxy descriptor configuration\n"
|
|
45
|
+
"# Edit parameters, then run:\n"
|
|
46
|
+
"# roxy compute sequences.fasta --config <this-file> -o results.parquet\n"
|
|
47
|
+
"# Use 'roxy describe <name>' for parameter documentation.\n"
|
|
48
|
+
)
|
|
49
|
+
config_map = {name: _default_params(DESCRIPTOR_REGISTRY[name]) for name in names}
|
|
50
|
+
body = yaml.safe_dump(
|
|
51
|
+
config_map,
|
|
52
|
+
sort_keys=False,
|
|
53
|
+
default_flow_style=False,
|
|
54
|
+
)
|
|
55
|
+
content = header + "\n" + body
|
|
56
|
+
|
|
57
|
+
if output is None:
|
|
58
|
+
sys.stdout.write(content)
|
|
59
|
+
else:
|
|
60
|
+
output.write_text(content)
|
|
61
|
+
typer.echo(f"Config written to {output}")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""``roxy list`` — display all registered descriptor families."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import textwrap
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from roxy.descriptors import DESCRIPTOR_REGISTRY
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _descriptor_lines(
|
|
16
|
+
name: str,
|
|
17
|
+
summary: str,
|
|
18
|
+
*,
|
|
19
|
+
name_width: int,
|
|
20
|
+
desc_width: int,
|
|
21
|
+
) -> list[Text]:
|
|
22
|
+
desc_lines = textwrap.wrap(summary, width=desc_width) or [""]
|
|
23
|
+
lines: list[Text] = []
|
|
24
|
+
for idx, desc_line in enumerate(desc_lines):
|
|
25
|
+
line = Text()
|
|
26
|
+
if idx == 0:
|
|
27
|
+
line.append(f"{name:<{name_width}}", style="cyan")
|
|
28
|
+
else:
|
|
29
|
+
line.append(" " * name_width)
|
|
30
|
+
line.append(" ")
|
|
31
|
+
line.append(desc_line, style="dim")
|
|
32
|
+
lines.append(line)
|
|
33
|
+
return lines
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_descriptors(
|
|
37
|
+
family: str | None = typer.Option(None, "--family", "-f", help="Filter by family name."),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""List all registered descriptors grouped by family."""
|
|
40
|
+
entries = sorted(DESCRIPTOR_REGISTRY.items())
|
|
41
|
+
if family:
|
|
42
|
+
entries = [(n, c) for n, c in entries if c.family == family]
|
|
43
|
+
|
|
44
|
+
if not entries:
|
|
45
|
+
Console().print("[yellow]No descriptors registered.[/yellow]")
|
|
46
|
+
raise typer.Exit
|
|
47
|
+
|
|
48
|
+
families: dict[str, list[tuple[str, type]]] = {}
|
|
49
|
+
for name, cls in entries:
|
|
50
|
+
families.setdefault(cls.family, []).append((name, cls))
|
|
51
|
+
|
|
52
|
+
total = sum(len(v) for v in families.values())
|
|
53
|
+
max_name = max(len(n) for n, _ in entries)
|
|
54
|
+
console = Console()
|
|
55
|
+
console.print(f"[bold]Roxy descriptors[/bold] ({total})")
|
|
56
|
+
|
|
57
|
+
for fam in sorted(families):
|
|
58
|
+
descriptors = families[fam]
|
|
59
|
+
title = Text()
|
|
60
|
+
title.append(fam, style="bold green")
|
|
61
|
+
title.append(f" ({len(descriptors)})", style="dim")
|
|
62
|
+
console.print()
|
|
63
|
+
console.print(title)
|
|
64
|
+
|
|
65
|
+
desc_width = max(24, console.width - max_name - 2)
|
|
66
|
+
for name, cls in descriptors:
|
|
67
|
+
doc = inspect.getdoc(cls) or ""
|
|
68
|
+
first_line = doc.splitlines()[0] if doc else ""
|
|
69
|
+
for line in _descriptor_lines(
|
|
70
|
+
name,
|
|
71
|
+
first_line,
|
|
72
|
+
name_width=max_name,
|
|
73
|
+
desc_width=desc_width,
|
|
74
|
+
):
|
|
75
|
+
console.print(line)
|
|
76
|
+
|
|
77
|
+
console.print()
|
|
78
|
+
console.print("[dim]Use [bold]roxy describe <name>[/bold] for full details.[/dim]")
|
roxy/cli/main.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Roxy CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from roxy import __version__
|
|
8
|
+
from roxy.cli.compute import compute
|
|
9
|
+
from roxy.cli.describe_descriptor import describe_descriptor
|
|
10
|
+
from roxy.cli.init_config import init_config
|
|
11
|
+
from roxy.cli.list_descriptors import list_descriptors
|
|
12
|
+
|
|
13
|
+
_HELP_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="roxy",
|
|
17
|
+
add_completion=False,
|
|
18
|
+
context_settings=_HELP_CONTEXT_SETTINGS,
|
|
19
|
+
help="Roxy — protein sequence descriptors for ML.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _version_callback(value: bool | None) -> None:
|
|
24
|
+
if value:
|
|
25
|
+
typer.echo(f"roxy {__version__}")
|
|
26
|
+
raise typer.Exit
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback()
|
|
30
|
+
def main(
|
|
31
|
+
_version: bool | None = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--version",
|
|
34
|
+
"-v",
|
|
35
|
+
callback=_version_callback,
|
|
36
|
+
is_eager=True,
|
|
37
|
+
help="Show version and exit.",
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Roxy CLI main callback."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
app.command(
|
|
44
|
+
name="list",
|
|
45
|
+
help="List all registered descriptors grouped by family.",
|
|
46
|
+
)(list_descriptors)
|
|
47
|
+
|
|
48
|
+
app.command(
|
|
49
|
+
name="describe",
|
|
50
|
+
help="Show full details for a descriptor.",
|
|
51
|
+
)(describe_descriptor)
|
|
52
|
+
|
|
53
|
+
app.command(
|
|
54
|
+
name="compute",
|
|
55
|
+
help="Compute descriptors for sequences in a file.",
|
|
56
|
+
)(compute)
|
|
57
|
+
|
|
58
|
+
app.command(
|
|
59
|
+
name="init-config",
|
|
60
|
+
help="Generate a YAML config file populated with descriptor defaults.",
|
|
61
|
+
)(init_config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
app()
|
roxy/core/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Roxy core package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
RoxyError,
|
|
7
|
+
RoxyIOError,
|
|
8
|
+
)
|
|
9
|
+
from .io import read_fasta, read_sequences, write_table
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RoxyError",
|
|
13
|
+
"RoxyIOError",
|
|
14
|
+
"read_fasta",
|
|
15
|
+
"read_sequences",
|
|
16
|
+
"write_table",
|
|
17
|
+
]
|