Skip to content

Commit

Permalink
feat: Update CLI structure and add dataset management commands with l…
Browse files Browse the repository at this point in the history
…ogging configuration
  • Loading branch information
jjjermiah committed Dec 10, 2024
1 parent 112242b commit 6d85aab
Show file tree
Hide file tree
Showing 16 changed files with 928 additions and 627 deletions.
7 changes: 5 additions & 2 deletions config/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ warn_unused_configs = True

# Require type annotations for all function definitions
# Enforces explicit type annotations, improving code clarity and type safety
disallow_untyped_defs = True
; disallow_untyped_defs = True

# Disallow functions with incomplete type annotations
# Ensures all function arguments and return types are properly annotated
Expand Down Expand Up @@ -60,4 +60,7 @@ warn_unreachable = True
[mypy-pytest.*]
# Ignore missing type hints in pytest package
# Prevents errors when pytest's types are not available
ignore_missing_imports = True
ignore_missing_imports = True

[mypy-orcestradownloader.cli.__main__]
ignore_errors = True
1 change: 1 addition & 0 deletions config/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fix = true
extend-exclude = [
"docs/*", # Skip documentation files
"tests/*", # Skip test files
"src/orcestradownloader/cli/__main__.py",
]

# Set the ruff cache directory
Expand Down
2 changes: 1 addition & 1 deletion pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
myscript = "orcestra-downloader.main:main"

orcestra = "orcestradownloader.cli.__main__:cli"

[tool.hatch.version]
# Path to file containing version string
Expand Down
9 changes: 7 additions & 2 deletions src/orcestradownloader/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(
self.cache_file = cache_dir / cache_file
self.cache_days_to_keep = cache_days_to_keep

def get_cached_response(self) -> Optional[List[dict]]:
def get_cached_response(self, name: str) -> Optional[List[dict]]:
"""Retrieve cached response if it exists and is up-to-date."""
log.debug('Checking for cached response...')
if not self.cache_file.exists():
Expand All @@ -35,7 +35,12 @@ def get_cached_response(self) -> Optional[List[dict]]:
daysago = f'{hours} hours ago'
else:
daysago = f'{minutes} minutes ago'
log.info('Using cached response from %s', daysago)
log.info(
'[bold magenta]%s:[/] Using cached response from %s from file://%s',
name,
daysago,
self.cache_file,
)
response_data: List[dict] = cached_data['data']
return response_data
else:
Expand Down
111 changes: 111 additions & 0 deletions src/orcestradownloader/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Type

import click
from click import Group, MultiCommand

from orcestradownloader.logging_config import set_log_verbosity
from orcestradownloader.managers import REGISTRY, DatasetManager, UnifiedDataManager
from orcestradownloader.models import ICBSet, PharmacoSet, RadioSet, ToxicoSet, XevaSet


@dataclass
class DatasetConfig:
url: str
cache_file: str
dataset_type: Type


DATASET_CONFIG: Dict[str, DatasetConfig] = {
'pharmacosets': DatasetConfig(
url='https://orcestra.ca/api/psets/available',
cache_file='pharmacosets.json',
dataset_type=PharmacoSet
),
'icbsets': DatasetConfig(
url='https://orcestra.ca/api/clinical_icb/available',
cache_file='icbsets.json',
dataset_type=ICBSet
),
'radiosets': DatasetConfig(
url='https://orcestra.ca/api/radiosets/available',
cache_file='radiosets.json',
dataset_type=RadioSet
),
'xevasets': DatasetConfig(
url='https://orcestra.ca/api/xevasets/available',
cache_file='xevasets.json',
dataset_type=XevaSet
),
'toxicosets': DatasetConfig(
url='https://orcestra.ca/api/toxicosets/available',
cache_file='toxicosets.json',
dataset_type=ToxicoSet
),
}

# Register all dataset managers automatically
for name, config in DATASET_CONFIG.items():
manager = DatasetManager(
url=config.url,
cache_file=config.cache_file,
dataset_type=config.dataset_type
)
REGISTRY.register(name, manager)


CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])


class DatasetMultiCommand(MultiCommand):
"""
A custom MultiCommand that dynamically creates subcommands based on DATASET_CONFIG.
Each dataset type gets its own group with 'list' and 'table' subcommands.
"""

def list_commands(self, ctx):
return list(DATASET_CONFIG.keys())

def get_command(self, ctx, name):
if name in DATASET_CONFIG:
ds_group = Group(name=name, context_settings=CONTEXT_SETTINGS)

@ds_group.command(name='list')
@set_log_verbosity()
@click.option('--force', is_flag=True, help='Force fetch new data')
@click.pass_context
def _list(ctx, force: bool = False, verbose: int = 1, quiet: bool = False):
"""List items for this dataset."""
manager = UnifiedDataManager(force=force)
manager.list_one(name)

@ds_group.command(name='table')
@set_log_verbosity()
@click.option('--force', is_flag=True, help='Force fetch new data')
@click.pass_context
def _table(ctx, force: bool = False, verbose: int = 1, quiet: bool = False):
"""Print a table of items for this dataset."""
manager = UnifiedDataManager(force=force)
manager.print_one_table(name)

return ds_group
return None

@click.command(cls=DatasetMultiCommand, context_settings=CONTEXT_SETTINGS)
@click.pass_context
def cli(ctx, force: bool = False, verbose: int = 1, quiet: bool = False):
"""Manage all datasets dynamically.
\b
Subcommands:
- list
- table
"""
ctx.ensure_object(dict)
ctx.obj['force'] = force


if __name__ == '__main__':
cli()
72 changes: 71 additions & 1 deletion src/orcestradownloader/logging_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import logging
from logging import ERROR, getLevelName, getLogger
from typing import Any, Callable

import click
from click.decorators import FC
from rich.logging import RichHandler


Expand All @@ -8,9 +12,75 @@ def setup_logger(name: str) -> logging.Logger:
level=logging.INFO,
format='%(message)s',
datefmt='[%X]',
handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
handlers=[
RichHandler(rich_tracebacks=True, tracebacks_show_locals=True, markup=True)
],
)
return logging.getLogger(name)


def set_log_verbosity(
*param_decls: str,
logger_name: str = 'orcestra',
quiet_decl: tuple = ('--quiet', '-q'),
**kwargs: Any, # noqa
) -> Callable[[FC], FC]:
"""
Add a `--verbose` option to set the logging level based on verbosity count
and a `--quiet` option to suppress all logging except errors.
Parameters
----------
*param_decls : str
Custom names for the verbosity flag.
quiet_decl : tuple
Tuple containing custom names for the quiet flag.
**kwargs : Any
Additional keyword arguments for the click option.
Returns
-------
Callable
The decorated function with verbosity and quiet options.
"""

def callback(ctx: click.Context, param: click.Parameter, value: int) -> None:
levels = {0: 'ERROR', 1: 'WARNING', 2: 'INFO', 3: 'DEBUG'}
level = levels.get(value, 'DEBUG') # Default to DEBUG if verbosity is high
logger = getLogger(logger_name)
# Check if `--quiet` is passed
if ctx.params.get('quiet', False):
logger.setLevel(ERROR)
return

levelvalue = getLevelName(level)

logger.setLevel(levelvalue)

# Default verbosity options
if not param_decls:
param_decls = ('--verbose', '-v')

# Set default options for verbosity
kwargs.setdefault('count', True)
kwargs.setdefault(
'help',
'Increase verbosity of logging, defaults to WARNING. '
'(0-3: ERROR, WARNING, INFO, DEBUG).',
)
kwargs['callback'] = callback

# Add the `--quiet` option
def decorator(func: FC) -> FC:
func = click.option(*param_decls, **kwargs)(func)
func = click.option(
*quiet_decl,
is_flag=True,
help='Suppress all logging except errors, overrides verbosity options.',
)(func)
return func

return decorator


logger = setup_logger('orcestra')
Loading

0 comments on commit 6d85aab

Please sign in to comment.