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.
Files changed (61) hide show
  1. roxy/__init__.py +26 -0
  2. roxy/cli/__init__.py +7 -0
  3. roxy/cli/_utils.py +24 -0
  4. roxy/cli/compute.py +167 -0
  5. roxy/cli/describe_descriptor.py +101 -0
  6. roxy/cli/init_config.py +61 -0
  7. roxy/cli/list_descriptors.py +78 -0
  8. roxy/cli/main.py +65 -0
  9. roxy/core/__init__.py +17 -0
  10. roxy/core/constants.py +450 -0
  11. roxy/core/exceptions.py +17 -0
  12. roxy/core/io.py +140 -0
  13. roxy/data/__init__.py +1 -0
  14. roxy/data/aaindex.csv +567 -0
  15. roxy/descriptors/__init__.py +22 -0
  16. roxy/descriptors/_utils.py +284 -0
  17. roxy/descriptors/aaindex/__init__.py +5 -0
  18. roxy/descriptors/aaindex/aaindex.py +138 -0
  19. roxy/descriptors/autocorrelation/__init__.py +5 -0
  20. roxy/descriptors/autocorrelation/autocorrelation.py +195 -0
  21. roxy/descriptors/base.py +82 -0
  22. roxy/descriptors/complexity/__init__.py +11 -0
  23. roxy/descriptors/complexity/entropy_complexity.py +166 -0
  24. roxy/descriptors/complexity/local_repetition.py +217 -0
  25. roxy/descriptors/complexity/run_blockiness.py +128 -0
  26. roxy/descriptors/composition/__init__.py +15 -0
  27. roxy/descriptors/composition/aac.py +63 -0
  28. roxy/descriptors/composition/compositional_bias.py +147 -0
  29. roxy/descriptors/composition/dpc.py +70 -0
  30. roxy/descriptors/composition/grouped.py +54 -0
  31. roxy/descriptors/composition/kmer_full_alphabet.py +86 -0
  32. roxy/descriptors/ctd/__init__.py +9 -0
  33. roxy/descriptors/ctd/ctd_classic.py +155 -0
  34. roxy/descriptors/ctd/distribution.py +130 -0
  35. roxy/descriptors/hybrid/__init__.py +5 -0
  36. roxy/descriptors/hybrid/family_summary.py +205 -0
  37. roxy/descriptors/motif/__init__.py +13 -0
  38. roxy/descriptors/motif/functional_residue_content.py +123 -0
  39. roxy/descriptors/motif/pattern.py +112 -0
  40. roxy/descriptors/motif/spacing.py +185 -0
  41. roxy/descriptors/motif/user_regex.py +128 -0
  42. roxy/descriptors/physicochemical/__init__.py +15 -0
  43. roxy/descriptors/physicochemical/charge.py +172 -0
  44. roxy/descriptors/physicochemical/global_basic.py +202 -0
  45. roxy/descriptors/physicochemical/hydrophobicity.py +191 -0
  46. roxy/descriptors/physicochemical/order_disorder.py +165 -0
  47. roxy/descriptors/physicochemical/structural_propensity.py +140 -0
  48. roxy/descriptors/positional/__init__.py +11 -0
  49. roxy/descriptors/positional/normalized.py +122 -0
  50. roxy/descriptors/positional/sliding_window.py +124 -0
  51. roxy/descriptors/positional/terminal.py +116 -0
  52. roxy/descriptors/pseudo/__init__.py +11 -0
  53. roxy/descriptors/pseudo/pseaac.py +122 -0
  54. roxy/descriptors/pseudo/qso.py +114 -0
  55. roxy/descriptors/pseudo/sequence_order.py +159 -0
  56. roxy/descriptors/registry.py +36 -0
  57. roxylib-0.2.0.dist-info/METADATA +154 -0
  58. roxylib-0.2.0.dist-info/RECORD +61 -0
  59. roxylib-0.2.0.dist-info/WHEEL +4 -0
  60. roxylib-0.2.0.dist-info/entry_points.txt +2 -0
  61. 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
@@ -0,0 +1,7 @@
1
+ """Roxy CLI package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .main import app
6
+
7
+ __all__ = ["app"]
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
+ )
@@ -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
+ ]