Skip to content

Commit

Permalink
Merge pull request ckan#3384 from ckan/3384-datastore-cli
Browse files Browse the repository at this point in the history
datastore cli: add dump command
  • Loading branch information
amercader authored Apr 4, 2017
2 parents 6c8a178 + 20785b9 commit 9db6005
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 168 deletions.
167 changes: 107 additions & 60 deletions ckan/lib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import paste.script
from paste.registry import Registry
from paste.script.util.logging_config import fileConfig
import click

import ckan.logic as logic
import ckan.model as model
Expand Down Expand Up @@ -133,6 +134,111 @@ def ungettext(self, singular, plural, n):
return singular


def _get_config(config=None):
from paste.deploy import appconfig

if config:
filename = os.path.abspath(config)
config_source = '-c parameter'
elif os.environ.get('CKAN_INI'):
filename = os.environ.get('CKAN_INI')
config_source = '$CKAN_INI'
else:
default_filename = 'development.ini'
filename = os.path.join(os.getcwd(), default_filename)
if not os.path.exists(filename):
# give really clear error message for this common situation
msg = 'ERROR: You need to specify the CKAN config (.ini) '\
'file path.'\
'\nUse the --config parameter or set environment ' \
'variable CKAN_INI or have {}\nin the current directory.' \
.format(default_filename)
exit(msg)

if not os.path.exists(filename):
msg = 'Config file not found: %s' % filename
msg += '\n(Given by: %s)' % config_source
exit(msg)

fileConfig(filename)
return appconfig('config:' + filename)


def load_config(config, load_site_user=True):
conf = _get_config(config)
assert 'ckan' not in dir() # otherwise loggers would be disabled
# We have now loaded the config. Now we can import ckan for the
# first time.
from ckan.config.environment import load_environment
load_environment(conf.global_conf, conf.local_conf)

registry = Registry()
registry.prepare()
import pylons
registry.register(pylons.translator, MockTranslator())

if model.user_table.exists() and load_site_user:
# If the DB has already been initialized, create and register
# a pylons context object, and add the site user to it, so the
# auth works as in a normal web request
c = pylons.util.AttribSafeContextObj()

registry.register(pylons.c, c)

site_user = logic.get_action('get_site_user')({'ignore_auth': True}, {})

pylons.c.user = site_user['name']
pylons.c.userobj = model.User.get(site_user['name'])

## give routes enough information to run url_for
parsed = urlparse.urlparse(conf.get('ckan.site_url', 'http://0.0.0.0'))
request_config = routes.request_config()
request_config.host = parsed.netloc + parsed.path
request_config.protocol = parsed.scheme


def paster_click_group(summary):
'''Return a paster command click.Group for paster subcommands
:param command: the paster command linked to this function from
setup.py, used in help text (e.g. "datastore")
:param summary: summary text used in paster's help/command listings
(e.g. "Perform commands to set up the datastore")
'''
class PasterClickGroup(click.Group):
'''A click.Group that may be called like a paster command'''
def __call__(self, ignored_command):
sys.argv.remove(ignored_command)
return super(PasterClickGroup, self).__call__(
prog_name=u'paster ' + ignored_command,
help_option_names=[u'-h', u'--help'],
obj={})

@click.group(cls=PasterClickGroup)
@click.option(
'--plugin',
metavar='ckan',
help='paster plugin (when run outside ckan directory)')
@click_config_option
@click.pass_context
def cli(ctx, plugin, config):
ctx.obj['config'] = config


cli.summary = summary
cli.group_name = u'ckan'
return cli


# common definition for paster ... --config
click_config_option = click.option(
'-c',
'--config',
default=None,
metavar='CONFIG',
help=u'Config file to use (default: development.ini)')


class CkanCommand(paste.script.command.Command):
'''Base class for classes that implement CKAN paster commands to inherit.'''
parser = paste.script.command.Command.standard_parser(verbose=True)
Expand All @@ -145,67 +251,8 @@ class CkanCommand(paste.script.command.Command):
default_verbosity = 1
group_name = 'ckan'

def _get_config(self):
from paste.deploy import appconfig

if self.options.config:
self.filename = os.path.abspath(self.options.config)
config_source = '-c parameter'
elif os.environ.get('CKAN_INI'):
self.filename = os.environ.get('CKAN_INI')
config_source = '$CKAN_INI'
else:
default_filename = 'development.ini'
self.filename = os.path.join(os.getcwd(), default_filename)
if not os.path.exists(self.filename):
# give really clear error message for this common situation
msg = 'ERROR: You need to specify the CKAN config (.ini) '\
'file path.'\
'\nUse the --config parameter or set environment ' \
'variable CKAN_INI or have {}\nin the current directory.' \
.format(default_filename)
raise self.BadCommand(msg)

if not os.path.exists(self.filename):
msg = 'Config file not found: %s' % self.filename
msg += '\n(Given by: %s)' % config_source
raise self.BadCommand(msg)

fileConfig(self.filename)
return appconfig('config:' + self.filename)

def _load_config(self, load_site_user=True):
conf = self._get_config()
assert 'ckan' not in dir() # otherwise loggers would be disabled
# We have now loaded the config. Now we can import ckan for the
# first time.
from ckan.config.environment import load_environment
load_environment(conf.global_conf, conf.local_conf)

self.registry = Registry()
self.registry.prepare()
import pylons
self.translator_obj = MockTranslator()
self.registry.register(pylons.translator, self.translator_obj)

if model.user_table.exists() and load_site_user:
# If the DB has already been initialized, create and register
# a pylons context object, and add the site user to it, so the
# auth works as in a normal web request
c = pylons.util.AttribSafeContextObj()

self.registry.register(pylons.c, c)

self.site_user = logic.get_action('get_site_user')({'ignore_auth': True}, {})

pylons.c.user = self.site_user['name']
pylons.c.userobj = model.User.get(self.site_user['name'])

## give routes enough information to run url_for
parsed = urlparse.urlparse(conf.get('ckan.site_url', 'http://0.0.0.0'))
request_config = routes.request_config()
request_config.host = parsed.netloc + parsed.path
request_config.protocol = parsed.scheme
load_config(self.options.config, load_site_user)

def _setup_app(self):
cmd = paste.script.appinstall.SetupCommand('setup-app')
Expand Down
5 changes: 4 additions & 1 deletion ckan/plugins/toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ class _Toolkit(object):
'StopOnError',
# validation invalid exception
'Invalid',
# class for providing cli interfaces
# old class for providing CLI interfaces
'CkanCommand',
# function for initializing CLI interfaces
'load_config',
# base class for IDatasetForm plugins
'DefaultDatasetForm',
# base class for IGroupForm plugins
Expand Down Expand Up @@ -243,6 +245,7 @@ def _initialize(self):
t['Invalid'] = logic_validators.Invalid

t['CkanCommand'] = cli.CkanCommand
t['load_config'] = cli.load_config
t['DefaultDatasetForm'] = lib_plugins.DefaultDatasetForm
t['DefaultGroupForm'] = lib_plugins.DefaultGroupForm
t['DefaultOrganizationForm'] = lib_plugins.DefaultOrganizationForm
Expand Down
1 change: 0 additions & 1 deletion ckan/tests/test_coding_standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,6 @@ def find_unprefixed_string_literals(filename):
u'ckanext/datapusher/tests/test_action.py',
u'ckanext/datapusher/tests/test_default_views.py',
u'ckanext/datapusher/tests/test_interfaces.py',
u'ckanext/datastore/commands.py',
u'ckanext/datastore/controller.py',
u'ckanext/datastore/db.py',
u'ckanext/datastore/helpers.py',
Expand Down
121 changes: 68 additions & 53 deletions ckanext/datastore/commands.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,90 @@
# encoding: utf-8

from __future__ import print_function
import argparse
import os
import sys

import ckan.lib.cli as cli
from ckan.lib.cli import (
load_config,
parse_db_config,
paster_click_group,
click_config_option,
)
from ckanext.datastore.helpers import identifier
from ckanext.datastore.controller import DUMP_FORMATS, dump_to

import click

def _abort(message):
print(message, file=sys.stderr)
sys.exit(1)

datastore_group = paster_click_group(
summary=u'Perform commands to set up the datastore')

def _set_permissions(args):
write_url = cli.parse_db_config('ckan.datastore.write_url')
read_url = cli.parse_db_config('ckan.datastore.read_url')
db_url = cli.parse_db_config('sqlalchemy.url')

@datastore_group.command(
u'set-permissions',
help=u'Emit an SQL script that will set the permissions for the '
u'datastore users as configured in your configuration file.')
@click.help_option(u'-h', u'--help')
@click_config_option
@click.pass_context
def set_permissions(ctx, config):
load_config(config or ctx.obj['config'])

write_url = parse_db_config(u'ckan.datastore.write_url')
read_url = parse_db_config(u'ckan.datastore.read_url')
db_url = parse_db_config(u'sqlalchemy.url')

# Basic validation that read and write URLs reference the same database.
# This obviously doesn't check they're the same database (the hosts/ports
# could be different), but it's better than nothing, I guess.
if write_url['db_name'] != read_url['db_name']:
_abort("The datastore write_url and read_url must refer to the same "
"database!")

context = {
'maindb': db_url['db_name'],
'datastoredb': write_url['db_name'],
'mainuser': db_url['db_user'],
'writeuser': write_url['db_user'],
'readuser': read_url['db_user'],
}
exit(u"The datastore write_url and read_url must refer to the same "
u"database!")

sql = _permissions_sql(context)
sql = permissions_sql(
maindb=db_url['db_name'],
datastoredb=write_url['db_name'],
mainuser=db_url['db_user'],
writeuser=write_url['db_user'],
readuser=read_url['db_user'])

print(sql)


def _permissions_sql(context):
def permissions_sql(maindb, datastoredb, mainuser, writeuser, readuser):
template_filename = os.path.join(os.path.dirname(__file__),
'set_permissions.sql')
u'set_permissions.sql')
with open(template_filename) as fp:
template = fp.read()
return template.format(**context)


parser = argparse.ArgumentParser(
prog='paster datastore',
description='Perform commands to set up the datastore',
epilog='Make sure that the datastore URLs are set properly before you run '
'these commands!')
subparsers = parser.add_subparsers(title='commands')

parser_set_perms = subparsers.add_parser(
'set-permissions',
description='Set the permissions on the datastore.',
help='This command will help ensure that the permissions for the '
'datastore users as configured in your configuration file are '
'correct at the database. It will emit an SQL script that '
'you can use to set these permissions.',
epilog='"The ships hung in the sky in much the same way that bricks '
'don\'t."')
parser_set_perms.set_defaults(func=_set_permissions)


class SetupDatastoreCommand(cli.CkanCommand):
summary = parser.description

def command(self):
self._load_config()

args = parser.parse_args(self.args)
args.func(args)
return template.format(
maindb=identifier(maindb),
datastoredb=identifier(datastoredb),
mainuser=identifier(mainuser),
writeuser=identifier(writeuser),
readuser=identifier(readuser))


@datastore_group.command(
u'dump',
help=u'Dump a datastore resource in one of the supported formats.')
@click.argument(u'resource-id', nargs=1)
@click.argument(
u'output-file',
type=click.File(u'wb'),
default=click.get_binary_stream(u'stdout'))
@click.help_option(u'-h', u'--help')
@click_config_option
@click.option(u'--format', default=u'csv', type=click.Choice(DUMP_FORMATS))
@click.option(u'--offset', type=click.IntRange(0, None), default=0)
@click.option(u'--limit', type=click.IntRange(0))
@click.option(u'--bom', is_flag=True) # FIXME: options based on format
@click.pass_context
def dump(ctx, resource_id, output_file, config, format, offset, limit, bom):
load_config(config or ctx.obj['config'])

dump_to(
resource_id,
output_file,
fmt=format,
offset=offset,
limit=limit,
options={u'bom': bom})
Loading

0 comments on commit 9db6005

Please sign in to comment.