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.
Files changed (141) hide show
  1. silkroute/__init__.py +116 -0
  2. silkroute/cli/__init__.py +8 -0
  3. silkroute/cli/_shared.py +396 -0
  4. silkroute/cli/collect_data.py +67 -0
  5. silkroute/cli/helper_commands.py +99 -0
  6. silkroute/cli/interfaces/__init__.py +1 -0
  7. silkroute/cli/interfaces/alphafold.py +35 -0
  8. silkroute/cli/interfaces/biodbnet.py +71 -0
  9. silkroute/cli/interfaces/biogrid.py +39 -0
  10. silkroute/cli/interfaces/brenda.py +345 -0
  11. silkroute/cli/interfaces/chebi.py +106 -0
  12. silkroute/cli/interfaces/chembl.py +51 -0
  13. silkroute/cli/interfaces/chemical_search_query.py +254 -0
  14. silkroute/cli/interfaces/genontology.py +27 -0
  15. silkroute/cli/interfaces/interpro.py +48 -0
  16. silkroute/cli/interfaces/kegg.py +41 -0
  17. silkroute/cli/interfaces/panther.py +74 -0
  18. silkroute/cli/interfaces/pathwaycommons.py +116 -0
  19. silkroute/cli/interfaces/pride.py +70 -0
  20. silkroute/cli/interfaces/proteindatabank.py +30 -0
  21. silkroute/cli/interfaces/pubchem.py +201 -0
  22. silkroute/cli/interfaces/reactome.py +36 -0
  23. silkroute/cli/interfaces/refseq.py +56 -0
  24. silkroute/cli/interfaces/rhea.py +27 -0
  25. silkroute/cli/interfaces/stringdb.py +50 -0
  26. silkroute/cli/interfaces/uniprot.py +18 -0
  27. silkroute/cli/interfaces/uniprot_crossref.py +130 -0
  28. silkroute/cli/interfaces/uniprot_search_ids.py +70 -0
  29. silkroute/cli/interfaces/uniprot_search_query.py +74 -0
  30. silkroute/cli/interfaces/uniprot_search_sequences.py +155 -0
  31. silkroute/cli/main.py +42 -0
  32. silkroute/cli/workflows.py +2138 -0
  33. silkroute/config/.env.example +7 -0
  34. silkroute/config/alphafold/fields.yml +7 -0
  35. silkroute/config/alphafold/init.yml +1 -0
  36. silkroute/config/biodbnet/fields.yml +157 -0
  37. silkroute/config/biogrid/fields.yml +6 -0
  38. silkroute/config/brenda/fields.yml +58 -0
  39. silkroute/config/chebi/fields.yml +58 -0
  40. silkroute/config/chembl/fields.yml +206 -0
  41. silkroute/config/go/fields.yml +24 -0
  42. silkroute/config/interpro/fields.yml +22 -0
  43. silkroute/config/kegg/fields.yml +36 -0
  44. silkroute/config/panther/fields.yml +5 -0
  45. silkroute/config/pathwaycommons/fields.yml +91 -0
  46. silkroute/config/pdb/fields.yml +7 -0
  47. silkroute/config/pdb/init.yml +1 -0
  48. silkroute/config/pride/fields.yml +90 -0
  49. silkroute/config/pubchem/fields.yml +147 -0
  50. silkroute/config/reactome/fields.yml +7 -0
  51. silkroute/config/refseq/fields.yml +6 -0
  52. silkroute/config/rhea/fields.yml +0 -0
  53. silkroute/config/sabiork/fields.yml +0 -0
  54. silkroute/config/string/fields.yml +18 -0
  55. silkroute/config/uniprot_crossref/config_endpoints.yml +148 -0
  56. silkroute/constants/__init__.py +1 -0
  57. silkroute/constants/biodbnet.py +40 -0
  58. silkroute/constants/brenda.py +166 -0
  59. silkroute/constants/databases.py +69 -0
  60. silkroute/constants/interpro.py +19 -0
  61. silkroute/constants/kegg.py +26 -0
  62. silkroute/constants/pathwaycommons.py +47 -0
  63. silkroute/constants/pubchem.py +53 -0
  64. silkroute/constants/reactome.py +14 -0
  65. silkroute/constants/refseq.py +3 -0
  66. silkroute/constants/stringdb.py +59 -0
  67. silkroute/constants/uniprot.py +189 -0
  68. silkroute/core/__init__.py +1 -0
  69. silkroute/core/cache.py +241 -0
  70. silkroute/core/credentials.py +127 -0
  71. silkroute/core/crossref_enricher.py +962 -0
  72. silkroute/core/dbconfig.py +21 -0
  73. silkroute/core/exceptions.py +42 -0
  74. silkroute/core/export.py +210 -0
  75. silkroute/core/interfaces/__init__.py +1 -0
  76. silkroute/core/interfaces/alphafold.py +323 -0
  77. silkroute/core/interfaces/base.py +1496 -0
  78. silkroute/core/interfaces/biodbnet.py +62 -0
  79. silkroute/core/interfaces/biogrid.py +122 -0
  80. silkroute/core/interfaces/brenda.py +148 -0
  81. silkroute/core/interfaces/chebi.py +162 -0
  82. silkroute/core/interfaces/chembl.py +669 -0
  83. silkroute/core/interfaces/genontology.py +133 -0
  84. silkroute/core/interfaces/interpro.py +185 -0
  85. silkroute/core/interfaces/kegg.py +322 -0
  86. silkroute/core/interfaces/panther.py +58 -0
  87. silkroute/core/interfaces/pathwaycommons.py +92 -0
  88. silkroute/core/interfaces/pride.py +74 -0
  89. silkroute/core/interfaces/proteindatabank.py +314 -0
  90. silkroute/core/interfaces/pubchem.py +679 -0
  91. silkroute/core/interfaces/reactome.py +131 -0
  92. silkroute/core/interfaces/refseq.py +128 -0
  93. silkroute/core/interfaces/rhea.py +35 -0
  94. silkroute/core/interfaces/sabiork.py +447 -0
  95. silkroute/core/interfaces/stringdb.py +99 -0
  96. silkroute/core/interfaces/uniprot.py +911 -0
  97. silkroute/core/interfacesconfig.py +40 -0
  98. silkroute/core/metadata.py +299 -0
  99. silkroute/core/utils/__init__.py +1 -0
  100. silkroute/core/utils/base_auxiliary_methods.py +111 -0
  101. silkroute/core/utils/blast_search.py +215 -0
  102. silkroute/core/utils/crossref_enrichment.py +410 -0
  103. silkroute/core/utils/frames.py +59 -0
  104. silkroute/core/utils/query_builders.py +415 -0
  105. silkroute/core/utils/structure_files.py +64 -0
  106. silkroute/core/utils/uniprot_auxiliary_methods.py +341 -0
  107. silkroute/core/utils/xmlhandler.py +143 -0
  108. silkroute/core/workflow/__init__.py +1 -0
  109. silkroute/core/workflow/chebi_execution.py +165 -0
  110. silkroute/core/workflow/chebi_query_catalog.py +119 -0
  111. silkroute/core/workflow/chebi_query_parser.py +78 -0
  112. silkroute/core/workflow/chembl_query_catalog.py +416 -0
  113. silkroute/core/workflow/chembl_query_parser.py +97 -0
  114. silkroute/core/workflow/main_workflow.py +1696 -0
  115. silkroute/core/workflow/pubchem_execution.py +204 -0
  116. silkroute/core/workflow/pubchem_query_catalog.py +176 -0
  117. silkroute/core/workflow/pubchem_query_parser.py +145 -0
  118. silkroute/core/workflow/query_field_catalog.py +244 -0
  119. silkroute/core/workflow/query_interpreter.py +716 -0
  120. silkroute/core/workflow/query_prefixes.py +53 -0
  121. silkroute/core/workflow/schema.py +1078 -0
  122. silkroute/gui/__init__.py +24 -0
  123. silkroute/gui/form_helpers.py +828 -0
  124. silkroute/gui/nicegui_app.py +2053 -0
  125. silkroute/gui/query_builders/__init__.py +5 -0
  126. silkroute/gui/query_builders/chebi.py +71 -0
  127. silkroute/gui/query_builders/chembl.py +356 -0
  128. silkroute/gui/query_builders/common.py +17 -0
  129. silkroute/gui/query_builders/metadata.py +550 -0
  130. silkroute/gui/query_builders/pubchem.py +107 -0
  131. silkroute/gui/query_builders/registry.py +230 -0
  132. silkroute/gui/query_builders/uniprot.py +140 -0
  133. silkroute/gui/uniprot_return_fields.py +99 -0
  134. silkroute/gui/yaml_builder.py +1795 -0
  135. silkroute/logging/__init__.py +16 -0
  136. silkroute/logging/logger.py +266 -0
  137. silkroute-0.1.0.dist-info/METADATA +510 -0
  138. silkroute-0.1.0.dist-info/RECORD +141 -0
  139. silkroute-0.1.0.dist-info/WHEEL +4 -0
  140. silkroute-0.1.0.dist-info/entry_points.txt +3 -0
  141. 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
+ ]
@@ -0,0 +1,8 @@
1
+ """CLI entry points for SilkRoute."""
2
+
3
+ import typer
4
+
5
+ app = typer.Typer(name="silkroute", add_completion=False, help="Description")
6
+
7
+ if __name__ == "__main__":
8
+ app()
@@ -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."""