silkroute 0.1.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.
- silkroute/__init__.py +116 -0
- silkroute/cli/__init__.py +8 -0
- silkroute/cli/_shared.py +396 -0
- silkroute/cli/collect_data.py +67 -0
- silkroute/cli/helper_commands.py +99 -0
- silkroute/cli/interfaces/__init__.py +1 -0
- silkroute/cli/interfaces/alphafold.py +35 -0
- silkroute/cli/interfaces/biodbnet.py +71 -0
- silkroute/cli/interfaces/biogrid.py +39 -0
- silkroute/cli/interfaces/brenda.py +345 -0
- silkroute/cli/interfaces/chebi.py +106 -0
- silkroute/cli/interfaces/chembl.py +51 -0
- silkroute/cli/interfaces/chemical_search_query.py +254 -0
- silkroute/cli/interfaces/genontology.py +27 -0
- silkroute/cli/interfaces/interpro.py +48 -0
- silkroute/cli/interfaces/kegg.py +41 -0
- silkroute/cli/interfaces/panther.py +74 -0
- silkroute/cli/interfaces/pathwaycommons.py +116 -0
- silkroute/cli/interfaces/pride.py +70 -0
- silkroute/cli/interfaces/proteindatabank.py +30 -0
- silkroute/cli/interfaces/pubchem.py +201 -0
- silkroute/cli/interfaces/reactome.py +36 -0
- silkroute/cli/interfaces/refseq.py +56 -0
- silkroute/cli/interfaces/rhea.py +27 -0
- silkroute/cli/interfaces/stringdb.py +50 -0
- silkroute/cli/interfaces/uniprot.py +18 -0
- silkroute/cli/interfaces/uniprot_crossref.py +130 -0
- silkroute/cli/interfaces/uniprot_search_ids.py +70 -0
- silkroute/cli/interfaces/uniprot_search_query.py +74 -0
- silkroute/cli/interfaces/uniprot_search_sequences.py +155 -0
- silkroute/cli/main.py +42 -0
- silkroute/cli/workflows.py +2138 -0
- silkroute/config/.env.example +7 -0
- silkroute/config/alphafold/fields.yml +7 -0
- silkroute/config/alphafold/init.yml +1 -0
- silkroute/config/biodbnet/fields.yml +157 -0
- silkroute/config/biogrid/fields.yml +6 -0
- silkroute/config/brenda/fields.yml +58 -0
- silkroute/config/chebi/fields.yml +58 -0
- silkroute/config/chembl/fields.yml +206 -0
- silkroute/config/go/fields.yml +24 -0
- silkroute/config/interpro/fields.yml +22 -0
- silkroute/config/kegg/fields.yml +36 -0
- silkroute/config/panther/fields.yml +5 -0
- silkroute/config/pathwaycommons/fields.yml +91 -0
- silkroute/config/pdb/fields.yml +7 -0
- silkroute/config/pdb/init.yml +1 -0
- silkroute/config/pride/fields.yml +90 -0
- silkroute/config/pubchem/fields.yml +147 -0
- silkroute/config/reactome/fields.yml +7 -0
- silkroute/config/refseq/fields.yml +6 -0
- silkroute/config/rhea/fields.yml +0 -0
- silkroute/config/sabiork/fields.yml +0 -0
- silkroute/config/string/fields.yml +18 -0
- silkroute/config/uniprot_crossref/config_endpoints.yml +148 -0
- silkroute/constants/__init__.py +1 -0
- silkroute/constants/biodbnet.py +40 -0
- silkroute/constants/brenda.py +166 -0
- silkroute/constants/databases.py +69 -0
- silkroute/constants/interpro.py +19 -0
- silkroute/constants/kegg.py +26 -0
- silkroute/constants/pathwaycommons.py +47 -0
- silkroute/constants/pubchem.py +53 -0
- silkroute/constants/reactome.py +14 -0
- silkroute/constants/refseq.py +3 -0
- silkroute/constants/stringdb.py +59 -0
- silkroute/constants/uniprot.py +189 -0
- silkroute/core/__init__.py +1 -0
- silkroute/core/cache.py +241 -0
- silkroute/core/credentials.py +127 -0
- silkroute/core/crossref_enricher.py +962 -0
- silkroute/core/dbconfig.py +21 -0
- silkroute/core/exceptions.py +42 -0
- silkroute/core/export.py +210 -0
- silkroute/core/interfaces/__init__.py +1 -0
- silkroute/core/interfaces/alphafold.py +323 -0
- silkroute/core/interfaces/base.py +1496 -0
- silkroute/core/interfaces/biodbnet.py +62 -0
- silkroute/core/interfaces/biogrid.py +122 -0
- silkroute/core/interfaces/brenda.py +148 -0
- silkroute/core/interfaces/chebi.py +162 -0
- silkroute/core/interfaces/chembl.py +669 -0
- silkroute/core/interfaces/genontology.py +133 -0
- silkroute/core/interfaces/interpro.py +185 -0
- silkroute/core/interfaces/kegg.py +322 -0
- silkroute/core/interfaces/panther.py +58 -0
- silkroute/core/interfaces/pathwaycommons.py +92 -0
- silkroute/core/interfaces/pride.py +74 -0
- silkroute/core/interfaces/proteindatabank.py +314 -0
- silkroute/core/interfaces/pubchem.py +679 -0
- silkroute/core/interfaces/reactome.py +131 -0
- silkroute/core/interfaces/refseq.py +128 -0
- silkroute/core/interfaces/rhea.py +35 -0
- silkroute/core/interfaces/sabiork.py +447 -0
- silkroute/core/interfaces/stringdb.py +99 -0
- silkroute/core/interfaces/uniprot.py +911 -0
- silkroute/core/interfacesconfig.py +40 -0
- silkroute/core/metadata.py +299 -0
- silkroute/core/utils/__init__.py +1 -0
- silkroute/core/utils/base_auxiliary_methods.py +111 -0
- silkroute/core/utils/blast_search.py +215 -0
- silkroute/core/utils/crossref_enrichment.py +410 -0
- silkroute/core/utils/frames.py +59 -0
- silkroute/core/utils/query_builders.py +415 -0
- silkroute/core/utils/structure_files.py +64 -0
- silkroute/core/utils/uniprot_auxiliary_methods.py +341 -0
- silkroute/core/utils/xmlhandler.py +143 -0
- silkroute/core/workflow/__init__.py +1 -0
- silkroute/core/workflow/chebi_execution.py +165 -0
- silkroute/core/workflow/chebi_query_catalog.py +119 -0
- silkroute/core/workflow/chebi_query_parser.py +78 -0
- silkroute/core/workflow/chembl_query_catalog.py +416 -0
- silkroute/core/workflow/chembl_query_parser.py +97 -0
- silkroute/core/workflow/main_workflow.py +1696 -0
- silkroute/core/workflow/pubchem_execution.py +204 -0
- silkroute/core/workflow/pubchem_query_catalog.py +176 -0
- silkroute/core/workflow/pubchem_query_parser.py +145 -0
- silkroute/core/workflow/query_field_catalog.py +244 -0
- silkroute/core/workflow/query_interpreter.py +716 -0
- silkroute/core/workflow/query_prefixes.py +53 -0
- silkroute/core/workflow/schema.py +1078 -0
- silkroute/gui/__init__.py +24 -0
- silkroute/gui/form_helpers.py +828 -0
- silkroute/gui/nicegui_app.py +2053 -0
- silkroute/gui/query_builders/__init__.py +5 -0
- silkroute/gui/query_builders/chebi.py +71 -0
- silkroute/gui/query_builders/chembl.py +356 -0
- silkroute/gui/query_builders/common.py +17 -0
- silkroute/gui/query_builders/metadata.py +550 -0
- silkroute/gui/query_builders/pubchem.py +107 -0
- silkroute/gui/query_builders/registry.py +230 -0
- silkroute/gui/query_builders/uniprot.py +140 -0
- silkroute/gui/uniprot_return_fields.py +99 -0
- silkroute/gui/yaml_builder.py +1795 -0
- silkroute/logging/__init__.py +16 -0
- silkroute/logging/logger.py +266 -0
- silkroute-0.1.0.dist-info/METADATA +510 -0
- silkroute-0.1.0.dist-info/RECORD +141 -0
- silkroute-0.1.0.dist-info/WHEEL +4 -0
- silkroute-0.1.0.dist-info/entry_points.txt +3 -0
- silkroute-0.1.0.dist-info/licenses/LICENSE +21 -0
silkroute/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""SilkRoute — download and integrate biological data from multiple APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# Map public name -> module path (relative to this package). Loaded lazily via
|
|
11
|
+
# __getattr__ (PEP 562) so `import silkroute` does not pull every interface and
|
|
12
|
+
# its heavy dependencies (e.g. zeep/SOAP for BRENDA) at import time.
|
|
13
|
+
_LAZY_EXPORTS: dict[str, str] = {
|
|
14
|
+
"BaseAPIInterface": ".core.interfaces.base",
|
|
15
|
+
"AlphafoldInterface": ".core.interfaces.alphafold",
|
|
16
|
+
"BioDBNetInterface": ".core.interfaces.biodbnet",
|
|
17
|
+
"BioGRIDInterface": ".core.interfaces.biogrid",
|
|
18
|
+
"BrendaInterface": ".core.interfaces.brenda",
|
|
19
|
+
"ChEBIInterface": ".core.interfaces.chebi",
|
|
20
|
+
"ChEMBLInterface": ".core.interfaces.chembl",
|
|
21
|
+
"GenOntologyInterface": ".core.interfaces.genontology",
|
|
22
|
+
"InterproInterface": ".core.interfaces.interpro",
|
|
23
|
+
"KEGGInterface": ".core.interfaces.kegg",
|
|
24
|
+
"PantherInterface": ".core.interfaces.panther",
|
|
25
|
+
"PathwayCommonsInterface": ".core.interfaces.pathwaycommons",
|
|
26
|
+
"PDBInterface": ".core.interfaces.proteindatabank",
|
|
27
|
+
"PrideInterface": ".core.interfaces.pride",
|
|
28
|
+
"PubChemInterface": ".core.interfaces.pubchem",
|
|
29
|
+
"ReactomeInterface": ".core.interfaces.reactome",
|
|
30
|
+
"RefSeqInterface": ".core.interfaces.refseq",
|
|
31
|
+
"RheaInterface": ".core.interfaces.rhea",
|
|
32
|
+
"SabiorkInterface": ".core.interfaces.sabiork",
|
|
33
|
+
"StringInterface": ".core.interfaces.stringdb",
|
|
34
|
+
"UniprotInterface": ".core.interfaces.uniprot",
|
|
35
|
+
"Workflow": ".core.workflow.main_workflow",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Public names whose attribute in the source module differs from the export name.
|
|
39
|
+
_EXPORT_ALIASES: dict[str, str] = {"Workflow": "MainWorkflow"}
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from .core.interfaces.alphafold import AlphafoldInterface
|
|
43
|
+
from .core.interfaces.base import BaseAPIInterface
|
|
44
|
+
from .core.interfaces.biodbnet import BioDBNetInterface
|
|
45
|
+
from .core.interfaces.biogrid import BioGRIDInterface
|
|
46
|
+
from .core.interfaces.brenda import BrendaInterface
|
|
47
|
+
from .core.interfaces.chebi import ChEBIInterface
|
|
48
|
+
from .core.interfaces.chembl import ChEMBLInterface
|
|
49
|
+
from .core.interfaces.genontology import GenOntologyInterface
|
|
50
|
+
from .core.interfaces.interpro import InterproInterface
|
|
51
|
+
from .core.interfaces.kegg import KEGGInterface
|
|
52
|
+
from .core.interfaces.panther import PantherInterface
|
|
53
|
+
from .core.interfaces.pathwaycommons import PathwayCommonsInterface
|
|
54
|
+
from .core.interfaces.pride import PrideInterface
|
|
55
|
+
from .core.interfaces.proteindatabank import PDBInterface
|
|
56
|
+
from .core.interfaces.pubchem import PubChemInterface
|
|
57
|
+
from .core.interfaces.reactome import ReactomeInterface
|
|
58
|
+
from .core.interfaces.refseq import RefSeqInterface
|
|
59
|
+
from .core.interfaces.rhea import RheaInterface
|
|
60
|
+
from .core.interfaces.sabiork import SabiorkInterface
|
|
61
|
+
from .core.interfaces.stringdb import StringInterface
|
|
62
|
+
from .core.interfaces.uniprot import UniprotInterface
|
|
63
|
+
from .core.workflow.main_workflow import MainWorkflow as Workflow
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name: str) -> object:
|
|
67
|
+
"""Lazily import public exports on first access (PEP 562).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name (str): Attribute name being accessed on the package.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
object: The resolved export (interface class, workflow, etc.).
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
AttributeError: If ``name`` is not a known public export.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
module_path = _LAZY_EXPORTS.get(name)
|
|
80
|
+
if module_path is None:
|
|
81
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
82
|
+
raise AttributeError(msg)
|
|
83
|
+
module = import_module(module_path, __name__)
|
|
84
|
+
return getattr(module, _EXPORT_ALIASES.get(name, name))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def __dir__() -> list[str]:
|
|
88
|
+
"""Return the sorted public attribute names for tab-completion."""
|
|
89
|
+
return sorted(__all__)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"AlphafoldInterface",
|
|
94
|
+
"BaseAPIInterface",
|
|
95
|
+
"BioDBNetInterface",
|
|
96
|
+
"BioGRIDInterface",
|
|
97
|
+
"BrendaInterface",
|
|
98
|
+
"ChEBIInterface",
|
|
99
|
+
"ChEMBLInterface",
|
|
100
|
+
"GenOntologyInterface",
|
|
101
|
+
"InterproInterface",
|
|
102
|
+
"KEGGInterface",
|
|
103
|
+
"PDBInterface",
|
|
104
|
+
"PantherInterface",
|
|
105
|
+
"PathwayCommonsInterface",
|
|
106
|
+
"PrideInterface",
|
|
107
|
+
"PubChemInterface",
|
|
108
|
+
"ReactomeInterface",
|
|
109
|
+
"RefSeqInterface",
|
|
110
|
+
"RheaInterface",
|
|
111
|
+
"SabiorkInterface",
|
|
112
|
+
"StringInterface",
|
|
113
|
+
"UniprotInterface",
|
|
114
|
+
"Workflow",
|
|
115
|
+
"__version__",
|
|
116
|
+
]
|
silkroute/cli/_shared.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Shared helpers for CLI commands.
|
|
2
|
+
|
|
3
|
+
``BaseAPIInterface.fetch_single`` / ``fetch_batch`` return a ``(data, metadata)``
|
|
4
|
+
tuple. CLI commands historically did ``result = interface.fetch_single(...)``
|
|
5
|
+
followed by ``result.to_csv(...)``, which raised ``AttributeError`` because
|
|
6
|
+
``result`` was the tuple, not the data. ``save_or_print`` unpacks the tuple and
|
|
7
|
+
either writes the data to ``output`` or prints a short preview.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
15
|
+
|
|
16
|
+
import polars as pl
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from silkroute.core.export import (
|
|
20
|
+
USER_EXPORT_FORMATS,
|
|
21
|
+
export_dataframe,
|
|
22
|
+
normalize_export_format,
|
|
23
|
+
normalize_parse_format,
|
|
24
|
+
normalize_user_export_format,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import logging
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_OUTPUT_HELP = "Output file to save the results. Prints a preview if omitted."
|
|
32
|
+
_OUTPUT_DIR_HELP = "Output directory for results."
|
|
33
|
+
_FORMAT_HELP = "Output format: csv, json, xml, parquet. Inferred from extension if omitted."
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fetch_auto(interface: Any, queries: list[Any], method: str, **kwargs: Any) -> Any:
|
|
37
|
+
"""Dispatch to ``fetch_batch`` for several queries, else ``fetch_single``.
|
|
38
|
+
|
|
39
|
+
``queries`` is the already-split list of per-item queries; callers own the
|
|
40
|
+
splitting/shaping (comma input, dict wrapping, etc.). This owns only the
|
|
41
|
+
one-vs-many decision that was previously copy-pasted across commands.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
interface (Any): API interface exposing ``fetch_single`` / ``fetch_batch``.
|
|
45
|
+
queries (list[Any]): Already-split list of per-item queries.
|
|
46
|
+
method (str): Fetch method name forwarded to the interface.
|
|
47
|
+
**kwargs (Any): Extra keyword arguments forwarded to the fetch call.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Any: The interface's fetch result (a ``(data, metadata)`` tuple).
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
if len(queries) > 1:
|
|
54
|
+
return interface.fetch_batch(queries=queries, method=method, **kwargs)
|
|
55
|
+
return interface.fetch_single(query=queries[0], method=method, **kwargs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def output_option(help: str = _OUTPUT_HELP) -> Any: # noqa: A002 # `help` matches typer's kwarg
|
|
59
|
+
"""Build the standard ``--output/-o`` file option (None = print preview)."""
|
|
60
|
+
return typer.Option(None, "--output", "-o", help=help)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def output_dir_option(help: str = _OUTPUT_DIR_HELP) -> Any: # noqa: A002
|
|
64
|
+
"""Build the standard ``--output-dir/-o`` directory option (required)."""
|
|
65
|
+
return typer.Option(..., "--output-dir", "-o", help=help)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_option(help: str = _FORMAT_HELP) -> Any: # noqa: A002
|
|
69
|
+
"""Build the standard ``--format/-f`` export-format option."""
|
|
70
|
+
return typer.Option(None, "--format", "-f", help=help)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def split_result(result: Any) -> tuple[Any, dict | None]:
|
|
74
|
+
"""Split a ``(data, metadata)`` fetch result into its two parts.
|
|
75
|
+
|
|
76
|
+
``fetch_single`` / ``fetch_batch`` return a 2-tuple whose second element is a
|
|
77
|
+
metadata dict. Anything that is not such a tuple is treated as already-unwrapped
|
|
78
|
+
data with no metadata (``None``).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
result (Any): A ``(data, metadata)`` tuple or already-unwrapped data.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
tuple[Any, dict | None]: The data and its metadata dict (``None`` if absent).
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], dict): # noqa: PLR2004 # (data, metadata) pair
|
|
88
|
+
return result[0], result[1]
|
|
89
|
+
return result, None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def unwrap(result: Any) -> Any:
|
|
93
|
+
"""Return just the data from a ``(data, metadata)`` fetch result."""
|
|
94
|
+
return split_result(result)[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _metadata_enabled() -> bool:
|
|
98
|
+
"""Whether to write metadata sidecars, per the ``fetch --no-metadata`` flag.
|
|
99
|
+
|
|
100
|
+
Reads the shared click context meta set by the ``fetch`` callback; defaults
|
|
101
|
+
to ``True`` outside a CLI invocation (e.g. when called directly in tests).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
bool: ``True`` if metadata sidecars should be written.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# typer vendors click; this is its current location for the active-context lookup.
|
|
109
|
+
from typer._click.globals import get_current_context # noqa: PLC0415
|
|
110
|
+
except ImportError: # pragma: no cover - guards against typer internals moving
|
|
111
|
+
return True
|
|
112
|
+
ctx = get_current_context(silent=True)
|
|
113
|
+
if ctx is None:
|
|
114
|
+
return True
|
|
115
|
+
return bool(ctx.meta.get("write_metadata", True))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write_metadata_sidecar(output_path: str | Path, metadata: dict) -> None:
|
|
119
|
+
"""Write fetch metadata as a sidecar JSON next to ``output_path``.
|
|
120
|
+
|
|
121
|
+
``results.csv`` -> ``results.metadata.json``, keeping the provenance tied to
|
|
122
|
+
the data file it describes (no clobber when several commands share a dir).
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
output_path (str | Path): Path of the data file the sidecar describes.
|
|
126
|
+
metadata (dict): Provenance metadata to serialize.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
sidecar = Path(output_path).with_suffix(".metadata.json")
|
|
130
|
+
with sidecar.open("w") as fh:
|
|
131
|
+
json.dump(metadata, fh, indent=2, default=str)
|
|
132
|
+
typer.echo(f"Metadata saved to {sidecar}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def save_or_print(
|
|
136
|
+
result: Any,
|
|
137
|
+
output: str | None = None,
|
|
138
|
+
*,
|
|
139
|
+
output_format: str | None = None,
|
|
140
|
+
preview_rows: int = 5,
|
|
141
|
+
write_metadata: bool | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Unpack a fetch result and either save it to ``output`` or print a preview.
|
|
144
|
+
|
|
145
|
+
When ``output`` is given and the result carries metadata, the provenance is
|
|
146
|
+
also written to a ``<output>.metadata.json`` sidecar.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
result (Any): Raw return value of ``fetch_single`` / ``fetch_batch`` (a
|
|
150
|
+
``(data, metadata)`` tuple) or already-unwrapped data.
|
|
151
|
+
output (str | None): Path to save to. If ``None``, a preview is printed instead.
|
|
152
|
+
output_format (str | None): Export format (csv/json/xml/parquet) for DataFrame
|
|
153
|
+
results. If ``None``, inferred from the ``output`` extension,
|
|
154
|
+
defaulting to csv. Ignored for non-DataFrame data.
|
|
155
|
+
preview_rows (int): Number of rows to show when previewing a DataFrame.
|
|
156
|
+
write_metadata (bool | None): Whether to write the metadata sidecar. ``None`` (the
|
|
157
|
+
default) defers to the ``fetch --no-metadata`` CLI flag.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
data, metadata = split_result(result)
|
|
161
|
+
if write_metadata is None:
|
|
162
|
+
write_metadata = _metadata_enabled()
|
|
163
|
+
|
|
164
|
+
if isinstance(data, pl.DataFrame):
|
|
165
|
+
if output:
|
|
166
|
+
fmt = output_format or (Path(output).suffix.lstrip(".") or "csv")
|
|
167
|
+
try:
|
|
168
|
+
saved = export_dataframe(data, output, output_format=fmt)
|
|
169
|
+
except ValueError as e:
|
|
170
|
+
typer.echo(f"Error: {e}", err=True)
|
|
171
|
+
raise typer.Exit(code=1) from None
|
|
172
|
+
typer.echo(f"Results saved to {saved}")
|
|
173
|
+
if metadata and write_metadata:
|
|
174
|
+
_write_metadata_sidecar(saved, metadata)
|
|
175
|
+
else:
|
|
176
|
+
typer.echo(data.head(preview_rows))
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if output:
|
|
180
|
+
with Path(output).open("w") as fh:
|
|
181
|
+
if isinstance(data, bytes):
|
|
182
|
+
fh.write(data.decode("utf-8"))
|
|
183
|
+
elif isinstance(data, (dict, list)):
|
|
184
|
+
json.dump(data, fh, indent=2, default=str)
|
|
185
|
+
else:
|
|
186
|
+
fh.write(str(data))
|
|
187
|
+
typer.echo(f"Results saved to {output}")
|
|
188
|
+
if metadata and write_metadata:
|
|
189
|
+
_write_metadata_sidecar(output, metadata)
|
|
190
|
+
else:
|
|
191
|
+
typer.echo(data)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _write_metadata(out_dir: Path, metadata: dict) -> None:
|
|
195
|
+
"""Write the run metadata JSON into ``out_dir``."""
|
|
196
|
+
with (out_dir / "metadata.json").open("w") as f:
|
|
197
|
+
json.dump(metadata, f, indent=2, default=str)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _save_tabular(
|
|
201
|
+
export_data: Any,
|
|
202
|
+
enriched_data: Any,
|
|
203
|
+
metadata: dict,
|
|
204
|
+
out_dir: Path,
|
|
205
|
+
export_format: str,
|
|
206
|
+
logger: logging.Logger,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Save DataFrame results (and any enrichment frames) as csv/parquet.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
export_data (Any): Main result; saved only if a non-empty DataFrame.
|
|
212
|
+
enriched_data (Any): Optional dict of named enrichment DataFrames.
|
|
213
|
+
metadata (dict): Run metadata written alongside the results.
|
|
214
|
+
out_dir (Path): Output directory for the result files.
|
|
215
|
+
export_format (str): Requested format (``csv`` or ``parquet``).
|
|
216
|
+
logger (logging.Logger): Logger for progress and warnings.
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
if not (isinstance(export_data, pl.DataFrame) and not export_data.is_empty()):
|
|
220
|
+
logger.warning("No results to save in %s format.", export_format.upper())
|
|
221
|
+
return
|
|
222
|
+
tabular_format = normalize_export_format(export_format)
|
|
223
|
+
export_path = out_dir / f"uniprot_results.{tabular_format}"
|
|
224
|
+
export_dataframe(export_data, export_path, output_format=tabular_format)
|
|
225
|
+
if isinstance(enriched_data, dict):
|
|
226
|
+
for key, value in enriched_data.items():
|
|
227
|
+
logger.info("Saving %s results into %s directory", key, out_dir)
|
|
228
|
+
export_dataframe(value, out_dir / f"{key}_results.{tabular_format}", output_format=tabular_format)
|
|
229
|
+
_write_metadata(out_dir, metadata)
|
|
230
|
+
logger.info("Results saved to %s", export_path)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _save_json(
|
|
234
|
+
export_data: Any, enriched_data: Any, metadata: dict, out_dir: Path, logger: logging.Logger
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Save dict/list results (and any enrichment) as JSON.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
export_data (Any): Main result; saved only if a dict or list.
|
|
240
|
+
enriched_data (Any): Optional dict of named enrichment results.
|
|
241
|
+
metadata (dict): Run metadata written alongside the results.
|
|
242
|
+
out_dir (Path): Output directory for the result files.
|
|
243
|
+
logger (logging.Logger): Logger for progress and warnings.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
if not isinstance(export_data, (dict, list)):
|
|
247
|
+
logger.warning("No results to save in JSON format.")
|
|
248
|
+
return
|
|
249
|
+
with (out_dir / "uniprot_results.json").open("w") as f:
|
|
250
|
+
json.dump(export_data, f, indent=2, default=str)
|
|
251
|
+
if isinstance(enriched_data, dict):
|
|
252
|
+
for key, value in enriched_data.items():
|
|
253
|
+
logger.info("Saving %s results into %s directory", key, out_dir)
|
|
254
|
+
with (out_dir / f"{key}_results.json").open("w") as f:
|
|
255
|
+
json.dump(value, f, indent=2, default=str)
|
|
256
|
+
_write_metadata(out_dir, metadata)
|
|
257
|
+
logger.info("Results saved to %s/uniprot_results.json", out_dir)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _save_xml(
|
|
261
|
+
export_data: Any, enriched_data: Any, metadata: dict, out_dir: Path, logger: logging.Logger
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Save ElementTree results (and any enrichment) as XML.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
export_data (Any): Main result; saved only if it exposes ``getroot``.
|
|
267
|
+
enriched_data (Any): Optional dict of named enrichment ElementTrees.
|
|
268
|
+
metadata (dict): Run metadata written alongside the results.
|
|
269
|
+
out_dir (Path): Output directory for the result files.
|
|
270
|
+
logger (logging.Logger): Logger for progress and warnings.
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
if not hasattr(export_data, "getroot"):
|
|
274
|
+
logger.warning("No results to save in XML format.")
|
|
275
|
+
return
|
|
276
|
+
export_data.write(f"{out_dir}/uniprot_results.xml", encoding="utf-8", xml_declaration=True)
|
|
277
|
+
if isinstance(enriched_data, dict):
|
|
278
|
+
for key, value in enriched_data.items():
|
|
279
|
+
logger.info("Saving %s results into %s directory", key, out_dir)
|
|
280
|
+
value.write(f"{out_dir}/{key}_results.xml", encoding="utf-8", xml_declaration=True)
|
|
281
|
+
_write_metadata(out_dir, metadata)
|
|
282
|
+
logger.info("Results saved to %s/uniprot_results.xml", out_dir)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def save_uniprot_results(
|
|
286
|
+
export_data: Any,
|
|
287
|
+
enriched_data: Any,
|
|
288
|
+
metadata: dict,
|
|
289
|
+
output: str,
|
|
290
|
+
export_format: str,
|
|
291
|
+
logger: logging.Logger,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Persist UniProt search results and enrichment in the requested format.
|
|
294
|
+
|
|
295
|
+
Shared by the uniprot-search CLI commands (ids / query / sequences), which
|
|
296
|
+
previously duplicated this csv/parquet/json/xml export branching.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
export_data (Any): Main result to save.
|
|
300
|
+
enriched_data (Any): Optional dict of named enrichment results.
|
|
301
|
+
metadata (dict): Run metadata written alongside the results.
|
|
302
|
+
output (str): Output directory path.
|
|
303
|
+
export_format (str): Requested format (csv, parquet, json, or xml).
|
|
304
|
+
logger (logging.Logger): Logger for progress and warnings.
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
out_dir = Path(output)
|
|
308
|
+
if export_format in {"csv", "parquet"}:
|
|
309
|
+
_save_tabular(export_data, enriched_data, metadata, out_dir, export_format, logger)
|
|
310
|
+
elif export_format == "json":
|
|
311
|
+
_save_json(export_data, enriched_data, metadata, out_dir, logger)
|
|
312
|
+
elif export_format == "xml":
|
|
313
|
+
_save_xml(export_data, enriched_data, metadata, out_dir, logger)
|
|
314
|
+
else:
|
|
315
|
+
logger.warning(export_format)
|
|
316
|
+
logger.warning("No UniProt data found for the given search.")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def validate_export_format(export_format: str) -> str:
|
|
320
|
+
"""Normalize a user export-format string, or exit(1) with an error message.
|
|
321
|
+
|
|
322
|
+
Shared by the uniprot-search CLI commands (ids / query / sequences), which
|
|
323
|
+
previously duplicated this normalize-or-Exit(1) block verbatim.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
export_format (str): User-supplied export-format string.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
str: The normalized export format.
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
typer.Exit: If the format is not one of the supported export formats.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
normalized = normalize_user_export_format(export_format)
|
|
336
|
+
if normalized is None:
|
|
337
|
+
typer.echo(
|
|
338
|
+
f"Error: Unsupported export format '{export_format}'. "
|
|
339
|
+
f"Supported formats are: {', '.join(USER_EXPORT_FORMATS)}.",
|
|
340
|
+
err=True,
|
|
341
|
+
)
|
|
342
|
+
raise typer.Exit(code=1)
|
|
343
|
+
return normalized
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def parse_and_save_uniprot(
|
|
347
|
+
instance: Any,
|
|
348
|
+
response: Any,
|
|
349
|
+
metadata: dict,
|
|
350
|
+
*,
|
|
351
|
+
crossref_fields: str,
|
|
352
|
+
output: str,
|
|
353
|
+
export_format: str,
|
|
354
|
+
logger: logging.Logger,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Parse a UniProt response, optionally enrich, and save in the requested format.
|
|
357
|
+
|
|
358
|
+
Shared tail of the uniprot-search CLI commands (ids / query / sequences):
|
|
359
|
+
create the output dir, dump the raw response, parse, run cross-reference
|
|
360
|
+
enrichment when ``crossref_fields`` is set, and persist via
|
|
361
|
+
``save_uniprot_results``.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
instance (Any): UniProt interface exposing ``parse``.
|
|
365
|
+
response (Any): Raw UniProt response to parse and save.
|
|
366
|
+
metadata (dict): Run metadata, extended in place with parsing/enrichment info.
|
|
367
|
+
crossref_fields (str): Comma-separated cross-reference fields to enrich, if any.
|
|
368
|
+
output (str): Output directory path.
|
|
369
|
+
export_format (str): Requested export format.
|
|
370
|
+
logger (logging.Logger): Logger for progress messages.
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
# Lazy import: pulls the heavy interface/query-builder graph only for the
|
|
374
|
+
# uniprot-search commands, not every CLI module that imports this helper file.
|
|
375
|
+
from silkroute.core.utils.crossref_enrichment import run_crossref_enrichment # noqa: PLC0415
|
|
376
|
+
|
|
377
|
+
out_dir = Path(output)
|
|
378
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
379
|
+
with (out_dir / "raw_response.json").open("w") as f:
|
|
380
|
+
json.dump(response, f, indent=2, default=str)
|
|
381
|
+
|
|
382
|
+
logger.info("Parsing results...")
|
|
383
|
+
parse_format = normalize_parse_format(export_format) or "dataframe"
|
|
384
|
+
fmt = cast("Literal['json', 'dataframe', 'xml']", parse_format)
|
|
385
|
+
export_data, parsed_metadata = instance.parse(results=response, extract_fields=None, format=fmt)
|
|
386
|
+
metadata["parsing"] = parsed_metadata
|
|
387
|
+
|
|
388
|
+
enriched_data = None
|
|
389
|
+
if crossref_fields:
|
|
390
|
+
logger.info("Running cross-reference enrichment...")
|
|
391
|
+
enriched_data, enriched_metadata = run_crossref_enrichment(
|
|
392
|
+
export_data, crossref_fields.split(","), format=fmt
|
|
393
|
+
)
|
|
394
|
+
metadata["enrichment"] = enriched_metadata
|
|
395
|
+
|
|
396
|
+
save_uniprot_results(export_data, enriched_data, metadata, output, export_format, logger)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""CLI commands for batch data collection."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .interfaces.alphafold import app as alphafold_app
|
|
6
|
+
from .interfaces.biodbnet import app as biodbnet_app
|
|
7
|
+
from .interfaces.biogrid import app as biogrid_app
|
|
8
|
+
from .interfaces.brenda import app as brenda_app
|
|
9
|
+
from .interfaces.chebi import app as chebi_app
|
|
10
|
+
from .interfaces.chembl import app as chembl_app
|
|
11
|
+
from .interfaces.chemical_search_query import run_compound as chemical_search_cmd
|
|
12
|
+
from .interfaces.genontology import app as geneontology_app
|
|
13
|
+
from .interfaces.interpro import app as interpro_app
|
|
14
|
+
from .interfaces.kegg import app as kegg_app
|
|
15
|
+
from .interfaces.panther import app as panther_app
|
|
16
|
+
from .interfaces.pathwaycommons import app as pathwaycommons_app
|
|
17
|
+
from .interfaces.pride import app as pride_app
|
|
18
|
+
from .interfaces.proteindatabank import app as proteindatabank_app
|
|
19
|
+
from .interfaces.pubchem import app as pubchem_app
|
|
20
|
+
from .interfaces.reactome import app as reactome_app
|
|
21
|
+
from .interfaces.refseq import app as refseq_app
|
|
22
|
+
from .interfaces.rhea import app as rhea_app
|
|
23
|
+
from .interfaces.stringdb import app as stringdb_app
|
|
24
|
+
from .interfaces.uniprot import app as uniprot_app
|
|
25
|
+
|
|
26
|
+
search_app = typer.Typer(help="Fetch data from various biological databases using general search interfaces.")
|
|
27
|
+
search_app.command(
|
|
28
|
+
"chemical",
|
|
29
|
+
help="General chemical search to query compounds by name, CID, SMILES, InChI, or gene ID.",
|
|
30
|
+
)(chemical_search_cmd)
|
|
31
|
+
search_app.add_typer(uniprot_app, name="uniprot")
|
|
32
|
+
|
|
33
|
+
fetch_app = typer.Typer(help="Fetch data from various biological databases using API nomenclature.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@fetch_app.callback()
|
|
37
|
+
def _fetch_options(
|
|
38
|
+
ctx: typer.Context,
|
|
39
|
+
metadata: bool = typer.Option(
|
|
40
|
+
default=True,
|
|
41
|
+
help="Write a `<output>.metadata.json` provenance sidecar next to saved results.",
|
|
42
|
+
),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Shared options for every ``fetch`` subcommand."""
|
|
45
|
+
# Stored in the shared click context meta so save_or_print can honor it
|
|
46
|
+
# without threading the flag through every interface command.
|
|
47
|
+
ctx.meta["write_metadata"] = metadata
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
fetch_app.add_typer(alphafold_app, name="alphafold")
|
|
51
|
+
fetch_app.add_typer(biodbnet_app, name="biodbnet")
|
|
52
|
+
fetch_app.add_typer(biogrid_app, name="biogrid")
|
|
53
|
+
fetch_app.add_typer(brenda_app, name="brenda")
|
|
54
|
+
fetch_app.add_typer(chebi_app, name="chebi")
|
|
55
|
+
fetch_app.add_typer(chembl_app, name="chembl")
|
|
56
|
+
fetch_app.add_typer(geneontology_app, name="geneontology")
|
|
57
|
+
fetch_app.add_typer(interpro_app, name="interpro")
|
|
58
|
+
fetch_app.add_typer(kegg_app, name="kegg")
|
|
59
|
+
fetch_app.add_typer(pathwaycommons_app, name="pathwaycommons")
|
|
60
|
+
fetch_app.add_typer(panther_app, name="panther")
|
|
61
|
+
fetch_app.add_typer(pride_app, name="pride")
|
|
62
|
+
fetch_app.add_typer(proteindatabank_app, name="pdb")
|
|
63
|
+
fetch_app.add_typer(pubchem_app, name="pubchem")
|
|
64
|
+
fetch_app.add_typer(reactome_app, name="reactome")
|
|
65
|
+
fetch_app.add_typer(rhea_app, name="rhea")
|
|
66
|
+
fetch_app.add_typer(refseq_app, name="refseq")
|
|
67
|
+
fetch_app.add_typer(stringdb_app, name="stringdb")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Cache management CLI commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from silkroute.core.cache import clear_cache, list_caches
|
|
6
|
+
from silkroute.logging import get_logger
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(name="cache", help="Cache utility commands for SilkRoute.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("list")
|
|
12
|
+
def list_registered() -> None:
|
|
13
|
+
"""List registered caches."""
|
|
14
|
+
log = get_logger("silkroute.cli.cache.list")
|
|
15
|
+
regs = list_caches()
|
|
16
|
+
if not regs:
|
|
17
|
+
log.info("No caches registered.")
|
|
18
|
+
raise typer.Exit
|
|
19
|
+
log.info("Registered caches:")
|
|
20
|
+
for k, v in regs.items():
|
|
21
|
+
log.info(" %s -> %s", k, v)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command("clear")
|
|
25
|
+
def clear(
|
|
26
|
+
cache_names: list[str] | None = typer.Option(
|
|
27
|
+
None, "--name", "-n", help="Cache name to clear (repeatable)."
|
|
28
|
+
),
|
|
29
|
+
clear_all: bool = typer.Option(False, "--all", help="Clear all registered caches."),
|
|
30
|
+
dry_run: bool = typer.Option(True, "--dry-run", help="Don't delete, only show what would be removed."),
|
|
31
|
+
older_than: int | None = typer.Option(
|
|
32
|
+
None, "--older-than", help="Only delete entries older than this many days."
|
|
33
|
+
),
|
|
34
|
+
pattern: str | None = typer.Option(None, "--pattern", help="Glob pattern relative to each cache path."),
|
|
35
|
+
force: bool = typer.Option(False, "--force", help="Skip any confirmation prompts (non-interactive)."),
|
|
36
|
+
list_registered: bool = typer.Option(False, "--list", help="List registered caches and exit."),
|
|
37
|
+
empty: bool = typer.Option(False, "--empty", help="Clear empty caches only."),
|
|
38
|
+
) -> dict:
|
|
39
|
+
"""Clear cached data.
|
|
40
|
+
|
|
41
|
+
Use --name to specify one or more registered cache names, or --all to clear everything.
|
|
42
|
+
By default the command runs in --dry-run mode.
|
|
43
|
+
"""
|
|
44
|
+
log = get_logger("silkroute.cli.cache.clear")
|
|
45
|
+
|
|
46
|
+
if list_registered:
|
|
47
|
+
regs = list_caches()
|
|
48
|
+
if not regs:
|
|
49
|
+
log.info("No caches registered.")
|
|
50
|
+
raise typer.Exit
|
|
51
|
+
log.info("Registered caches:")
|
|
52
|
+
for k, v in regs.items():
|
|
53
|
+
log.info(" %s -> %s", k, v)
|
|
54
|
+
raise typer.Exit
|
|
55
|
+
|
|
56
|
+
if cache_names and clear_all:
|
|
57
|
+
log.error("Cannot use --name and --all together. Pick one.")
|
|
58
|
+
raise typer.Exit(code=2)
|
|
59
|
+
|
|
60
|
+
selected_names = None if clear_all else cache_names
|
|
61
|
+
|
|
62
|
+
if not dry_run and not force:
|
|
63
|
+
preview = clear_cache(
|
|
64
|
+
selected_names=selected_names,
|
|
65
|
+
dry_run=True,
|
|
66
|
+
older_than_days=older_than,
|
|
67
|
+
pattern=pattern,
|
|
68
|
+
empty=empty,
|
|
69
|
+
)
|
|
70
|
+
total = sum(len(v) for v in preview.values())
|
|
71
|
+
if total and not typer.confirm(f"Delete {total} path(s)?"):
|
|
72
|
+
raise typer.Abort
|
|
73
|
+
|
|
74
|
+
log.info("Clearing caches (dry_run=%s) - selected=%s", dry_run, selected_names or "ALL")
|
|
75
|
+
|
|
76
|
+
results = clear_cache(
|
|
77
|
+
selected_names=selected_names,
|
|
78
|
+
dry_run=dry_run,
|
|
79
|
+
older_than_days=older_than,
|
|
80
|
+
pattern=pattern,
|
|
81
|
+
empty=empty,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Summarize results
|
|
85
|
+
total = sum(len(v) for v in results.values())
|
|
86
|
+
if dry_run:
|
|
87
|
+
log.info("Dry-run: %d path(s) would be affected.", total)
|
|
88
|
+
else:
|
|
89
|
+
log.info("Removed %d path(s).", total)
|
|
90
|
+
|
|
91
|
+
for name, p_list in results.items():
|
|
92
|
+
if not p_list:
|
|
93
|
+
log.debug("%s: nothing to do", name)
|
|
94
|
+
continue
|
|
95
|
+
log.info("%s:", name)
|
|
96
|
+
for p in p_list:
|
|
97
|
+
log.info(" %s", p)
|
|
98
|
+
|
|
99
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-database Typer CLI command groups."""
|