Skip to content

Commit 40689e1

Browse files
authored
Add cli pathing application (#422)
* Add a cli path application * Add a custom cli config variable for the .pth file directory * Add commands for adding / removing hub-parsers paths from sys.path * Add decorators module ... Moves the operations_mode decorator into the new module. Adds another decorator for checking the python system path and adding any paths specified in our designated .pth file. This applies for any file that might need access to the hub-parsers * Improve async handling in decorators * Fix the hub parsers paths to the parent to correctly import hub * Cleanup documentation for cli pathing application
1 parent 213734b commit 40689e1

File tree

6 files changed

+332
-63
lines changed

6 files changed

+332
-63
lines changed

biothings/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from biothings.cli.commands.admin import build_admin_application
1515
from biothings.cli.commands.config import config_application, load_configuration
1616
from biothings.cli.commands.dataplugin import dataplugin_application
17+
from biothings.cli.commands.pathing import path_application
1718

1819

1920
def setup_logging_configuration(logging_level: Literal[10, 20, 30, 40, 50]) -> None:
@@ -63,4 +64,5 @@ def main():
6364

6465
admin_application.add_typer(dataplugin_application, name="dataplugin")
6566
admin_application.add_typer(config_application, name="config")
67+
admin_application.add_typer(path_application, name="path")
6668
return admin_application()

biothings/cli/commands/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,13 @@ def default_biothings_configuration() -> dict:
268268
"HUB_MAX_WORKERS": os.cpu_count(),
269269
"MAX_QUEUED_JOBS": 1000
270270
}
271+
272+
# specific attributes to the biothings-cli application
273+
cli_configuration = {
274+
"BIOTHINGS_CLI_PATH": "biothings_hub/path",
275+
}
276+
configuration.update(cli_configuration)
277+
271278
return configuration
272279

273280

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
Collection of decorators for usage within the biothings-cli
3+
4+
These are often method we want associated with many of the plugin methods we
5+
use, but don't directly impact the logic of the actual operation. Typically things
6+
related to paths and configurations that apply to large swaths of the cli
7+
would make sense as a decorator
8+
"""
9+
10+
import functools
11+
import inspect
12+
import logging
13+
import pathlib
14+
import sys
15+
from typing import Callable
16+
17+
from biothings.cli.exceptions import MissingPluginName
18+
19+
20+
logger = logging.getLogger(name="biothings-cli")
21+
22+
23+
def operation_mode(operation: Callable):
24+
"""
25+
Based off the directory structure for where the biothings-cli
26+
was invoked we set the "mode" to one of two states:
27+
28+
0) singular
29+
The current working directory contains a singular data-plugin
30+
31+
In this case we don't require a plugin_name argument to be passed
32+
at the command-line
33+
34+
1) hub
35+
The current working directory contains N directories operating as a
36+
"hub" or collection of data-plugins under one umbrella
37+
38+
In this case we do require a plugin_name argument to be passed
39+
at the command-line. Otherwise we have no idea which data-plugin to
40+
refer to
41+
42+
We attempt to load the plugin from this working directory. If we sucessfully load
43+
either a manifest or advanced plugin, then we can safely say this is a singular
44+
dataplugin
45+
46+
If we cannot load either a manifest or advanced plugin then we default assume that
47+
the mode is hub
48+
"""
49+
50+
@functools.wraps(operation)
51+
def determine_operation_mode(*args, **kwargs):
52+
53+
def determine_hub_mode():
54+
working_directory = pathlib.Path.cwd()
55+
working_directory_files = {file.name for file in working_directory.iterdir()}
56+
57+
mode = None
58+
if "manifest.json" in working_directory_files or "manifest.yaml" in working_directory_files:
59+
logger.debug("Inferring singular manifest plugin from directory structure")
60+
mode = "SINGULAR"
61+
elif "__init__.py" in working_directory_files:
62+
logger.debug("Inferring singular advanced plugin from directory structure")
63+
mode = "SINGULAR"
64+
else:
65+
logger.debug("Inferring multiple plugins from directory structure")
66+
mode = "HUB"
67+
68+
if mode == "SINGULAR":
69+
if kwargs.get("plugin_name", None) is not None:
70+
kwargs["plugin_name"] = None
71+
elif mode == "HUB":
72+
if kwargs.get("plugin_name", None) is None:
73+
raise MissingPluginName(working_directory)
74+
75+
@functools.wraps(operation)
76+
def handle_function(*args, **kwargs):
77+
operation_result = operation(*args, **kwargs)
78+
return operation_result
79+
80+
@functools.wraps(operation)
81+
async def handle_corountine(*args, **kwargs):
82+
operation_result = await operation(*args, **kwargs)
83+
return operation_result
84+
85+
determine_hub_mode()
86+
87+
if inspect.iscoroutinefunction(operation):
88+
return handle_corountine(*args, **kwargs)
89+
else:
90+
return handle_function(*args, **kwargs)
91+
92+
return determine_operation_mode
93+
94+
95+
def cli_system_path(operation: Callable): # pylint: disable=unused-argument
96+
"""
97+
Used for ensuring that if we've appended files to biothings-cli
98+
path file (stored under config.BIOTHINGS_CLI_PATH), then we need to update
99+
the system path so we can discover the modules at runtime
100+
"""
101+
102+
@functools.wraps(operation)
103+
def update_system_path(*args, **kwargs):
104+
105+
def update_system_path_from_file():
106+
from biothings import config
107+
108+
discovery_path = pathlib.Path(config.BIOTHINGS_CLI_PATH).resolve().absolute()
109+
path_file = discovery_path.joinpath("biothings_cli.pth")
110+
111+
if path_file.exists():
112+
with open(path_file, "r", encoding="utf-8") as handle:
113+
path_entries = handle.readlines()
114+
path_entries = [entry.strip("\n") for entry in path_entries]
115+
sys.path.extend(path_entries)
116+
for path in path_entries:
117+
logger.debug("Adding %s to system path", path)
118+
119+
@functools.wraps(operation)
120+
def handle_function(*args, **kwargs):
121+
operation_result = operation(*args, **kwargs)
122+
return operation_result
123+
124+
@functools.wraps(operation)
125+
async def handle_corountine(*args, **kwargs):
126+
operation_result = await operation(*args, **kwargs)
127+
return operation_result
128+
129+
update_system_path_from_file()
130+
131+
if inspect.iscoroutinefunction(operation):
132+
return handle_corountine(*args, **kwargs)
133+
else:
134+
return handle_function(*args, **kwargs)
135+
136+
return update_system_path

biothings/cli/commands/operations.py

Lines changed: 13 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"""
4848

4949
import asyncio
50-
import functools
5150
import logging
5251
import multiprocessing
5352
import os
@@ -57,7 +56,7 @@
5756
import shutil
5857
import sys
5958
import uuid
60-
from typing import Callable, Optional, Union
59+
from typing import Optional, Union
6160

6261
import jsonschema
6362
import rich
@@ -67,8 +66,9 @@
6766
from rich.console import Console
6867
from rich.panel import Panel
6968

69+
from biothings.cli.commands.decorators import cli_system_path, operation_mode
7070
from biothings.cli.structure import TEMPLATE_DIRECTORY
71-
from biothings.cli.exceptions import MissingPluginName, UnknownUploaderSource
71+
from biothings.cli.exceptions import UnknownUploaderSource
7272
from biothings.cli.utils import (
7373
clean_dumped_files,
7474
clean_uploaded_sources,
@@ -87,65 +87,8 @@
8787
logger = logging.getLogger(name="biothings-cli")
8888

8989

90-
def operation_mode(operation_method: Callable):
91-
"""
92-
Based off the directory structure for where the biothings-cli
93-
was invoked we set the "mode" to one of two states:
94-
95-
0) singular
96-
The current working directory contains a singular data-plugin
97-
98-
In this case we don't require a plugin_name argument to be passed
99-
at the command-line
100-
101-
1) hub
102-
The current working directory contains N directories operating as a
103-
"hub" or collection of data-plugins under one umbrella
104-
105-
In this case we do require a plugin_name argument to be passed
106-
at the command-line. Otherwise we have no idea which data-plugin to
107-
refer to
108-
109-
We attempt to load the plugin from this working directory. If we sucessfully load
110-
either a manifest or advanced plugin, then we can safely say this is a singular
111-
dataplugin
112-
113-
If we cannot load either a manifest or advanced plugin then we default assume that
114-
the mode is hub
115-
"""
116-
117-
@functools.wraps(operation_method)
118-
def determine_operation_mode(*args, **kwargs):
119-
working_directory = pathlib.Path.cwd()
120-
working_directory_files = {file.name for file in working_directory.iterdir()}
121-
122-
mode = None
123-
if "manifest.json" in working_directory_files or "manifest.yaml" in working_directory_files:
124-
logger.debug("Inferring singular manifest plugin from directory structure")
125-
mode = "SINGULAR"
126-
elif "__init__.py" in working_directory_files:
127-
logger.debug("Inferring singular advanced plugin from directory structure")
128-
mode = "SINGULAR"
129-
else:
130-
logger.debug("Inferring multiple plugins from directory structure")
131-
mode = "HUB"
132-
133-
if mode == "SINGULAR":
134-
if kwargs.get("plugin_name", None) is not None:
135-
kwargs["plugin_name"] = None
136-
elif mode == "HUB":
137-
if kwargs.get("plugin_name", None) is None:
138-
raise MissingPluginName(working_directory)
139-
140-
operation_result = operation_method(*args, **kwargs)
141-
return operation_result
142-
143-
return determine_operation_mode
144-
145-
14690
# do not apply operation_mode decorator since this operation means to create a new plugin
14791
# regardless what the current working directory has
148-
# @operation_mode
14992
def do_create(plugin_name: str, multi_uploaders: bool = False, parallelizer: bool = False):
15093
"""
15194
Create a new data plugin from the template
@@ -178,6 +121,7 @@ def do_create(plugin_name: str, multi_uploaders: bool = False, parallelizer: boo
178121
logger.info("Successfully created data plugin template at: %s\n", new_plugin_directory)
179122

180123

124+
@cli_system_path
181125
@operation_mode
182126
async def do_dump(plugin_name: Optional[str] = None, show_dumped: bool = True) -> None:
183127
"""
@@ -223,6 +167,7 @@ async def do_dump(plugin_name: Optional[str] = None, show_dumped: bool = True) -
223167
show_dumped_files(data_folder, assistant_instance.plugin_name)
224168

225169

170+
@cli_system_path
226171
@operation_mode
227172
async def do_upload(plugin_name: Optional[str] = None, batch_limit: int = 10000, show_uploaded: bool = True) -> None:
228173
"""
@@ -277,6 +222,7 @@ async def do_upload(plugin_name: Optional[str] = None, batch_limit: int = 10000,
277222
show_uploaded_sources(pathlib.Path(assistant_instance.plugin_directory), assistant_instance.plugin_name)
278223

279224

225+
@cli_system_path
280226
@operation_mode
281227
async def do_parallel_upload(
282228
plugin_name: Optional[str] = None, batch_limit: int = 10000, show_uploaded: bool = True
@@ -344,6 +290,7 @@ async def do_parallel_upload(
344290
show_uploaded_sources(pathlib.Path(assistant_instance.plugin_directory), assistant_instance.plugin_name)
345291

346292

293+
@cli_system_path
347294
@operation_mode
348295
async def do_dump_and_upload(plugin_name: str) -> None:
349296
"""
@@ -354,6 +301,7 @@ async def do_dump_and_upload(plugin_name: str) -> None:
354301
logger.info("[green]Success![/green] :rocket:", extra={"markup": True})
355302

356303

304+
@cli_system_path
357305
@operation_mode
358306
async def do_index(plugin_name: Optional[str] = None, sub_source_name: Optional[str] = None) -> None:
359307
"""
@@ -540,6 +488,7 @@ async def do_index(plugin_name: Optional[str] = None, sub_source_name: Optional[
540488
await show_source_index(index_name, assistant_instance.index_manager, elasticsearch_mapping)
541489

542490

491+
@cli_system_path
543492
@operation_mode
544493
async def do_list(
545494
plugin_name: Optional[str] = None, dump: bool = True, upload: bool = True, hubdb: bool = False
@@ -569,6 +518,7 @@ async def do_list(
569518
show_hubdb_content()
570519

571520

521+
@cli_system_path
572522
@operation_mode
573523
async def do_inspect(
574524
plugin_name: Optional[str] = None,
@@ -633,6 +583,7 @@ async def do_inspect(
633583
write_mapping_to_file(sub_output, inspection_mapping)
634584

635585

586+
@cli_system_path
636587
@operation_mode
637588
async def do_serve(plugin_name: Optional[str] = None, host: str = "localhost", port: int = 9999):
638589
"""
@@ -651,6 +602,7 @@ async def do_serve(plugin_name: Optional[str] = None, host: str = "localhost", p
651602
await main(host=host, port=port, db=src_db, table_space=table_space)
652603

653604

605+
@cli_system_path
654606
@operation_mode
655607
async def do_clean(
656608
plugin_name: Optional[str] = None, dump: bool = False, upload: bool = False, clean_all: bool = False
@@ -714,6 +666,7 @@ async def display_schema():
714666
console.print(panel)
715667

716668

669+
@cli_system_path
717670
@operation_mode
718671
async def validate_manifest(plugin_name: Optional[str] = None):
719672
"""

0 commit comments

Comments
 (0)