Permalink
Browse files

Introduce profile commands and improved profiles.

 - Add new commands for explicit Galaxy profile management.
 - ``profile_create`` creates a new profile - and can now automatically setup and configure a Postgres database for the profile.
 - ``profile_list`` shows created profiles.
 - ``profile_delete`` deletes a created profile and cleans up the database created for the profile.

xref #447
  • Loading branch information...
jmchilton committed May 16, 2016
1 parent 89f032a commit a87899ba1218f770322fb1739ebb89a30ea3f523
@@ -9,11 +9,7 @@


@click.command('database_create')
@click.argument(
'identifier',
metavar="IDENTIFIER",
type=str,
)
@options.database_identifier_argument()
@options.database_source_options()
@command_function
def cli(ctx, identifier, **kwds):
@@ -8,11 +8,7 @@


@click.command('database_delete')
@click.argument(
'identifier',
metavar="IDENTIFIER",
type=str,
)
@options.database_identifier_argument()
@options.database_source_options()
@command_function
def cli(ctx, identifier, **kwds):
@@ -0,0 +1,18 @@
"""Module describing the planemo ``profile_create`` command."""
from __future__ import print_function

import click

from planemo.cli import command_function
from planemo import options
from planemo.galaxy import profiles


@click.command('profile_create')
@options.profile_name_argument()
@options.profile_database_options()
@command_function
def cli(ctx, profile_name, **kwds):
"""Create a profile."""
profiles.create_profile(ctx, profile_name, **kwds)
print("Profile created.")
@@ -0,0 +1,18 @@
"""Module describing the planemo ``profile_delete`` command."""
from __future__ import print_function

import click

from planemo.cli import command_function
from planemo import options
from planemo.galaxy import profiles


@click.command('profile_delete')
@options.profile_name_argument()
@options.profile_database_options()
@command_function
def cli(ctx, profile_name, **kwds):
"""Delete a profile."""
profiles.delete_profile(ctx, profile_name, **kwds)
print("Profile deleted.")
@@ -0,0 +1,15 @@
"""Module describing the planemo ``profile_list`` command."""
from __future__ import print_function

import click

from planemo.cli import command_function
from planemo.galaxy import profiles


@click.command('profile_list')
@command_function
def cli(ctx, **kwds):
"""List configured profile names."""
profile_names = profiles.list_profiles(ctx, **kwds)
print(profile_names)
@@ -4,7 +4,15 @@

def create_database_source(**kwds):
"""Return a :class:`planemo.database.DatabaseSource` for configuration."""
return LocalPostgresDatabaseSource(**kwds)
database_type = kwds.get("database_type", "postgres")
if database_type == "postgres":
return LocalPostgresDatabaseSource(**kwds)
# TODO
# from .sqlite import SqliteDatabaseSource
# elif database_type == "sqlite":
# return SqliteDatabaseSource(**kwds)
else:
raise Exception("Unknown database type [%s]." % database_type)


__all__ = ['create_database_source']
@@ -11,7 +11,7 @@ class LocalPostgresDatabaseSource(DatabaseSource):
"""Local postgres database source managed through psql application."""

def __init__(self, **kwds):
"""Construct a postgres datasource from planemo configuration."""
"""Construct a postgres database source from planemo configuration."""
self.psql_path = kwds.get("postgres_psql_path", None) or 'psql'
self.database_user = kwds.get("postgres_database_user", None)
self.database_host = kwds.get("postgres_database_host", None)
@@ -0,0 +1,32 @@
"""Module describes an sqlite implementation of the :class:`DatabaseSource` interface."""
import os

from .interface import DatabaseSource

from .config import (
DATABASE_LOCATION_TEMPLATE,
attempt_database_preseed,
)


class SqliteDatabaseSource(DatabaseSource):
""":class:`DatabaseSource` implementation for creating sqlite databases."""

def __init__(self, **kwds):
"""Construct a sqlite database source from planemo configuration."""
self.sqlite_path = kwds.get("sqlite_path", None) or 'psql'
self._kwds = kwds

def list_databases(self):
"""List sqlite databases on a path."""
return os.path.basename(self.sqlite_path)

def create_database(self, identifier):
"""Create pre-populated Galxay sqlite database with specified path."""
return os.path.basename(self.sqlite_path)

def delete_database(self, identifier):
"""Use `psql -c "drop database"` to delete a database."""

def sqlalchemy_url(self, identifier):
"""Return URL or form postgresql://username:password@localhost/mydatabase."""
@@ -3,41 +3,126 @@
This is a workspace with a specific default configuration and shed
tool setup. It is meant to be used with various serve commands.
"""
import json
import os
import shutil

from .config import (
DATABASE_LOCATION_TEMPLATE,
attempt_database_preseed,
)
from planemo.database import create_database_source

PROFILE_OPTIONS_JSON_NAME = "planemo_profile_options.json"
ALREADY_EXISTS_EXCEPTION = "Cannot create profile with name [%s], directory [%s] already exists."

def ensure_profile(ctx, profile_name, **kwds):
"""Ensure a Galaxy profile exists and return profile defaults."""

def profile_exists(ctx, profile_name, **kwds):
"""Return a truthy value iff the specified profile already exists."""
profile_directory = _profile_directory(ctx, profile_name)
database_location = os.path.join(profile_directory, "galaxy.sqlite")
return os.path.exists(profile_directory)


if not os.path.exists(profile_directory):
os.makedirs(profile_directory)
def list_profiles(ctx, **kwds):
return os.listdir(ctx.galaxy_profiles_directory)


def delete_profile(ctx, profile_name, **kwds):
"""Delete profile with the specified name."""
profile_directory = _profile_directory(ctx, profile_name)
profile_options = _read_profile_options(profile_directory)
database_type = profile_options.get("database_type")
if database_type != "sqlite":
database_source = create_database_source(**kwds)
database_identifier = _profile_to_database_identifier(profile_name)
database_source.delete_database(
database_identifier,
)
shutil.rmtree(profile_directory)


def create_profile(ctx, profile_name, **kwds):
"""Create a profile with the specified name."""
profile_directory = _profile_directory(ctx, profile_name)
if profile_exists(ctx, profile_name, **kwds):
message = ALREADY_EXISTS_EXCEPTION % (
profile_name, profile_directory
)
raise Exception(message)

os.makedirs(profile_directory)
database_type = kwds.get("database_type", "sqlite")
if database_type != "sqlite":
database_source = create_database_source(**kwds)
database_identifier = _profile_to_database_identifier(profile_name)
database_source.create_database(
database_identifier,
)
database_connection = database_source.sqlalchemy_url(database_identifier)
else:
database_location = os.path.join(profile_directory, "galaxy.sqlite")
attempt_database_preseed(None, database_location, **kwds)
database_connection = DATABASE_LOCATION_TEMPLATE % database_location

stored_profile_options = {
'database_type': database_type,
'database_connection': database_connection,
}
profile_options_path = _stored_profile_options_path(profile_directory)
with open(profile_options_path, "w") as f:
json.dump(stored_profile_options, f)


database_connection = DATABASE_LOCATION_TEMPLATE % database_location
def ensure_profile(ctx, profile_name, **kwds):
"""Ensure a Galaxy profile exists and return profile defaults."""
if not profile_exists(ctx, profile_name, **kwds):
create_profile(ctx, profile_name, **kwds)

return _profile_options(ctx, profile_name, **kwds)


def _profile_options(ctx, profile_name, **kwds):
profile_directory = _profile_directory(ctx, profile_name)
file_path = os.path.join(profile_directory, "files")
shed_tool_path = os.path.join(profile_directory, "shed_tools")
shed_tool_conf = os.path.join(profile_directory, "shed_tool_conf.xml")
tool_dependency_dir = os.path.join(profile_directory, "deps")

return dict(
profile_options = _read_profile_options(profile_directory)
profile_options.update(dict(
file_path=file_path,
database_connection=database_connection,
tool_dependency_dir=tool_dependency_dir,
shed_tool_conf=shed_tool_conf,
shed_tool_path=shed_tool_path,
))

return profile_options


def _profile_to_database_identifier(profile_name):
char_lst = [c if c.isalnum() else "_" for c in profile_name]
return "plnmoprof_%s" % "".join(char_lst)


def _read_profile_options(profile_directory):
profile_options_path = _stored_profile_options_path(profile_directory)
with open(profile_options_path, "r") as f:
profile_options = json.load(f)
return profile_options


def _stored_profile_options_path(profile_directory):
profile_options_path = os.path.join(
profile_directory, PROFILE_OPTIONS_JSON_NAME
)
return profile_options_path


def _profile_directory(ctx, profile_name):
return os.path.join(ctx.galaxy_profiles_directory, profile_name)

__all__ = [
"ensure_profile",
"create_profile",
"delete_profile",
]
@@ -789,6 +789,7 @@ def galaxy_config_options():
conda_auto_init_option(),
# Profile options...
profile_option(),
profile_database_options(),
file_path_option(),
database_connection_option(),
shed_tools_conf_option(),
@@ -968,6 +969,45 @@ def test_report_options():
)


def profile_name_argument():
return click.argument(
'profile_name',
metavar="PROFILE_NAME",
type=str,
)


def database_identifier_argument():
return click.argument(
'identifier',
metavar="IDENTIFIER",
type=str,
)


def postgres_datatype_type_option():
return planemo_option(
"--postgres",
"database_type",
flag_value="postgres",
help=("Use postgres database type."),
)


def database_type_option():
return planemo_option(
"--database_type",
default="postgres",
type=click.Choice([
"postgres",
"sqlite",
]),
use_global_config=True,
help=("Type of database to use for profile - "
"currently only 'postgres' is available."),
)


def database_source_options():
"""Database connection options for commands that utilize a database."""
return _compose(
@@ -998,6 +1038,14 @@ def database_source_options():
)


def profile_database_options():
return _compose(
postgres_datatype_type_option(),
database_type_option(),
database_source_options(),
)


def test_options():
return _compose(
planemo_option(
@@ -11,6 +11,7 @@
from .test_utils import (
CliTestCase,
skip_if_environ,
skip_unless_environ,
TEST_REPOS_DIR,
PROJECT_TEMPLATES_DIR,
TEST_DATA_DIR,
@@ -54,10 +55,21 @@ def test_serve_workflow(self):

@skip_if_environ("PLANEMO_SKIP_GALAXY_TESTS")
def test_serve_profile(self):
self._test_serve_profile()

@skip_if_environ("PLANEMO_SKIP_GALAXY_TESTS")
@skip_unless_environ("PLANEMO_ENABLE_POSTGRES_TESTS")
def test_serve_postgres_profile(self):
self._test_serve_profile("--database_type", "postgres")

def _test_serve_profile(self, *db_options):
new_profile = "planemo_test_profile_%s" % uuid.uuid4()
extra_args = [
"--daemon", "--pid_file", self._pid_file, "--profile", new_profile,
"--daemon",
"--pid_file", self._pid_file,
"--profile", new_profile,
]
extra_args.extend(db_options)
self._launch_thread_and_wait(self._run, extra_args)
user_gi = self._user_gi
assert len(user_gi.histories.get_histories(name=TEST_HISTORY_NAME)) == 0
@@ -10,8 +10,10 @@ class DatabaseCommandsTestCase(CliTestCase):
def test_database_commands(self):
with self._isolate():
result = self._check_exit_code(["database_list"])
assert "test123" not in result.output
self._check_exit_code(["database_create", "test123"])
assert "test1234" not in result.output
self._check_exit_code(["database_create", "test1234"])
result = self._check_exit_code(["database_list"])
assert "test123" in result.output
self._check_exit_code(["database_delete", "test123"])
assert "test1234" in result.output
self._check_exit_code(["database_delete", "test1234"])
result = self._check_exit_code(["database_list"])
assert "test1234" not in result.output
Oops, something went wrong.

0 comments on commit a87899b

Please sign in to comment.