text: from __future__ import absolute_import, print_function
  
  import hashlib
  import inspect
  import itertools
  import logging
  import os
  import sys
  from abc import abstractmethod
  
  import jinja2
  import jinja2.meta
  import sqlparse
  from decorator import decorator
  from interface_meta import quirk_docs, override
  
  from omniduct.caches.base import cached_method
  from omniduct.duct import Duct
  from omniduct.filesystems.local import LocalFsClient
  from omniduct.utils.debug import logger, logging_scope
  from omniduct.utils.decorators import require_connection
  from omniduct.utils.magics import (MagicsProvider, process_line_arguments,
                                     process_line_cell_arguments)
  
  from . import _cursor_formatters
  from ._cursor_serializer import CursorSerializer
  from ._namespaces import ParsedNamespaces
  
  logging.getLogger('requests').setLevel(logging.WARNING)
  
  
  @decorator
  def render_statement(method, self, statement, *args, **kwargs):
      """
      Pre-render a statement template prior to wrapped function execution.
  
      This decorator expects to act as wrapper on functions which
      takes statements as the second argument.
      """
      if kwargs.pop('template', True):
          statement = self.template_render(
              statement,
              context=kwargs.pop('context', {}),
              by_name=False,
          )
      return method(self, statement, *args, **kwargs)
  
  
  class DatabaseClient(Duct, MagicsProvider):
      """
      An abstract class providing the common API for all database clients.
  
      Note: `DatabaseClient` subclasses are callable, so that one can use
      `DatabaseClient(...)` as a short-hand for `DatabaseClient.query(...)`.
  
      Class Attributes:
          DUCT_TYPE (`Duct.Type`): The type of `Duct` protocol implemented by this class.
          DEFAULT_PORT (int): The default port for the database service (defined
              by subclasses).
          CURSOR_FORMATTERS (dict<str, CursorFormatter): asdsd
          DEFAULT_CURSOR_FORMATTER (str): ...
      """
  
      DUCT_TYPE = Duct.Type.DATABASE
      DEFAULT_PORT = None
  
      CURSOR_FORMATTERS = {
          'pandas': _cursor_formatters.PandasCursorFormatter,
          'hive': _cursor_formatters.HiveCursorFormatter,
          'csv': _cursor_formatters.CsvCursorFormatter,
          'tuple': _cursor_formatters.TupleCursorFormatter,
          'dict': _cursor_formatters.DictCursorFormatter,
          'raw': _cursor_formatters.RawCursorFormatter,
      }
      DEFAULT_CURSOR_FORMATTER = 'pandas'
      SUPPORTS_SESSION_PROPERTIES = False
      NAMESPACE_NAMES = ['database', 'table']
      NAMESPACE_QUOTECHAR = '"'
      NAMESPACE_SEPARATOR = '.'
  
      @property
      def NAMESPACE_DEFAULT(self):
          pass
  
      @quirk_docs('_init', mro=True)
      def __init__(
          self, session_properties=None, templates=None, template_context=None, default_format_opts=None,
          **kwargs
      ):
          """
          session_properties (dict): A mapping of default session properties
              to values. Interpretation is left up to implementations.
          templates (dict): A dictionary of name to template mappings. Additional
              templates can be added using `.template_add`.
          template_context (dict): The default template context to use when
              rendering templates.
          default_format_opts (dict): The default formatting options passed to
              cursor formatter.
          """
          Duct.__init_with_kwargs__(self, kwargs, port=self.DEFAULT_PORT)
  
          self.session_properties = session_properties or {}
          self._templates = templates or {}
          self._template_context = template_context or {}
          self._sqlalchemy_engine = None
          self._sqlalchemy_metadata = None
          self._default_format_opts = default_format_opts or {}
  
          self._init(**kwargs)
  
      @abstractmethod
      def _init(self):
          pass
  
      # Session property management and configuration
      @property
      def session_properties(self):
          """dict: The default session properties used in statement executions."""
          return self._session_properties
  
      @session_properties.setter
      def session_properties(self, properties):
          self._session_properties = self._get_session_properties(default=properties)
  
      def _get_session_properties(self, overrides=None, default=None):
          """
          Retrieve the default session properties with optional overrides.
  
          Properties with a value of None will be skipped, in order to allow
          overrides to remove default properties.
  
          Args:
              overrides (dict, None): A dictionary of session property overrides.
              default (dict, None): A dictionary of default session properties, if
                  it is necessary to override `self.session_properties`.
  
          Returns:
              dict: A dictionary of session properties.
          """
          if (default or overrides) and not self.SUPPORTS_SESSION_PROPERTIES:
              raise RuntimeError("Session properties are not supported by this backend.")
  
          props = (default if default is not None else self.session_properties).copy()
          props.update(overrides or {})
  
          # Remove any properties with value set to None.
          for key, value in props.items():
              if value is None:
                  del props[key]
  
          return props
  
      def __call__(self, query, **kwargs):
          return self.query(query, **kwargs)
  
      # Querying
      def _statement_prepare(self, statement, session_properties, **kwargs):
          """
          Prepare a statement for execution.
  
          This is a hook that can be used by subclasses to transform and/or
          validate statements before execution in `_execute`; for example
          inserting session properties in query headers.
  
          Args:
              statement (str): The statement to be executed.
              session_properties (dict): A mutable dictionary of session properties
                  and their values (this method can mutate it depending on statement
                  contents).
              **kwargs (dict): Any additional keyword arguments passed through to
                  `self.execute` (will match the extra keyword arguments added to
                  `self._execute`).
  
          Returns:
              statement (str): The statement to be executed (potentially transformed).
          """
          return statement
  
      def _statement_split(self, statements):
          """
          Split a statement into separate SQL statements.
  
          This method converts a single string containing one or more SQL
          statements into an iterator of strings, each corresponding to one SQL
          statement. If the statement's language is not to be SQL, this method
          should be overloaded appropriately.
  
          Args:
              statements (str): A string containing one or more SQL statements.
  
          Returns:
              iterator<str>: An iterator of SQL statements.
          """
          for statement in sqlparse.split(statements):
              statement = statement.strip()
              if statement.endswith(';'):
                  statement = statement[:-1].strip()
              if statement:  # remove empty statements
                  yield statement
  
      @classmethod
      def statement_hash(cls, statement, cleanup=True):
          """
          Retrieve the hash to use to identify query statements to the cache.
  
          Args:
              statement (str): A string representation of the statement to be
                  hashed.
              cleanup (bool): Whether the statement should first be consistently
                  reformatted using `statement_cleanup`.
  
          Returns:
              str: The hash used to identify a statement to the cache.
          """
          if cleanup:
              statement = cls.statement_cleanup(statement)
          if (
              sys.version_info.major == 3
              or sys.version_info.major == 2 and isinstance(statement, unicode)  # noqa: F821
          ):
              statement = statement.encode('utf8')
          return hashlib.sha256(statement).hexdigest()
  
      @classmethod
      def statement_cleanup(cls, statement):
          """
          Clean up statements prior to hash computation.
  
          This classmethod takes an SQL statement and reformats it by consistently
          removing comments and replacing all whitespace. It is used by the
          `statement_hash` method to avoid functionally identical queries hitting
          different cache keys. If the statement's language is not to be SQL, this
          method should be overloaded appropriately.
  
          Args:
              statement (str): The statement to be reformatted/cleaned-up.
  
          Returns:
              str: The new statement, consistently reformatted.
          """
          statement = sqlparse.format(statement, strip_comments=True, reindent=True)
          statement = os.linesep.join([line for line in statement.splitlines() if line])
          return statement
  
      @render_statement
      @cached_method(
          key=lambda self, kwargs: self.statement_hash(
              statement=kwargs['statement'],
              cleanup=kwargs.pop('cleanup', True)
          ),
          serializer=lambda self, kwargs: CursorSerializer(),
          use_cache=lambda self, kwargs: kwargs.pop('use_cache', False),
          metadata=lambda self, kwargs: {
              'statement': kwargs['statement'],
              'session_properties': kwargs['session_properties']
          }
      )
      @quirk_docs('_execute')
      @require_connection
      def execute(self, statement, wait=True, cursor=None, session_properties=None, **kwargs):
          """
          Execute a statement against this database and return a cursor object.
  
          Where supported by database implementations, this cursor can the be used
          in future executions, by passing it as the `cursor` keyword argument.
  
          Args:
              statement (str): The statement to be executed by the query client
                  (possibly templated).
              wait (bool): Whether the cursor should be returned before the
                  server-side query computation is complete and the relevant
                  results downloaded.
              cursor (DBAPI2 cursor):  Rather than creating a new cursor, execute
                  the statement against the provided cursor.
              session_properties (dict): Additional session properties and/or
                  overrides to use for this query. Setting a session property
                  value to `None` will cause it to be omitted.
              **kwargs (dict): Extra keyword arguments to be passed on to
                  `_execute`, as implemented by subclasses.
              template (bool): Whether the statement should be treated as a Jinja2
                  template. [Used by `render_statement` decorator.]
              context (dict): The context in which the template should be
                  evaluated (a dictionary of parameters to values). [Used by
                  `render_statement` decorator.]
              use_cache (bool): True or False (default). Whether to use the cache
                  (if present). [Used by `cached_method` decorator.]
              renew (bool): True or False (default). If cache is being used, renew
                  it before returning stored value. [Used by `cached_method`
                  decorator.]
              cleanup (bool): Whether statement should be cleaned up before
                  computing the hash used to cache results. [Used by `cached_method`
                  decorator.]
  
          Returns:
              DBAPI2 cursor: A DBAPI2 compatible cursor instance.
          """
  
          session_properties = self._get_session_properties(overrides=session_properties)
  
          statements = list(self._statement_split(
              self._statement_prepare(statement, session_properties=session_properties, **kwargs)
          ))
          assert len(statements) > 0, "No non-empty statements were provided."
  
          for statement in statements[:-1]:
              cursor = self._execute(statement, cursor=cursor, wait=True, session_properties=session_properties, **kwargs)
          cursor = self._execute(statements[-1], cursor=cursor, wait=wait, session_properties=session_properties, **kwargs)
  
          return cursor
  
      @logging_scope("Query", timed=True)
      @render_statement
      def query(self, statement, format=None, format_opts={}, use_cache=True, **kwargs):
          """
          Execute a statement against this database and collect formatted data.
  
          Args:
              statement (str): The statement to be executed by the query client
                  (possibly templated).
              format (str): A subclass of CursorFormatter, or one of: 'pandas',
                  'hive', 'csv', 'tuple' or 'dict'. Defaults to
                  `self.DEFAULT_CURSOR_FORMATTER`.
              format_opts (dict): A dictionary of format-specific options.
              use_cache (bool): Whether to cache the cursor returned by
                  `DatabaseClient.execute()` (overrides the default of False
                  for `.execute()`). (default=True)
              **kwargs (dict): Additional arguments to pass on to
                  `DatabaseClient.execute()`.
  
          Returns:
              The results of the query formatted as nominated.
          """
          cursor = self.execute(statement, wait=True, template=False, use_cache=use_cache, **kwargs)
  
          # Some DBAPI2 cursor implementations error if attempting to extract
          # data from an empty cursor, and if so, we simply return None.
          if self._cursor_empty(cursor):
              return None
  
          formatter = self._get_formatter(format, cursor, **format_opts)
          return formatter.dump()
  
      def stream(self, statement, format=None, format_opts={}, batch=None, **kwargs):
          """
          Execute a statement against this database and stream formatted results.
  
          This method returns a generator over objects representing rows in the
          result set. If `batch` is not `None`, then the iterator
          will be over lists of length `batch` containing formatted rows.
  
          Args:
              statement (str): The statement to be executed against the database.
              format (str): A subclass of CursorFormatter, or one of: 'pandas',
                  'hive', 'csv', 'tuple' or 'dict'. Defaults to
                  `self.DEFAULT_CURSOR_FORMATTER`.
              format_opts (dict): A dictionary of format-specific options.
              batch (int): If not `None`, the number of rows from the resulting
                  cursor to be returned at once.
              **kwargs (dict): Additional keyword arguments to pass onto
                  `DatabaseClient.execute`.
  
          Returns:
              iterator: An iterator over objects of the nominated format or, if
                  batched, a list of such objects.
          """
          cursor = self.execute(statement, wait=True, **kwargs)
          formatter = self._get_formatter(format, cursor, **format_opts)
  
          for row in formatter.stream(batch=batch):
              yield row
  
      def _get_formatter(self, formatter, cursor, **kwargs):
          formatter = formatter or self.DEFAULT_CURSOR_FORMATTER
          if not (inspect.isclass(formatter) and issubclass(formatter, _cursor_formatters.CursorFormatter)):
              assert formatter in self.CURSOR_FORMATTERS, "Invalid format '{}'. Choose from: {}".format(formatter, ','.join(self.CURSOR_FORMATTERS.keys()))
              formatter = self.CURSOR_FORMATTERS[formatter]
          format_opts = dict(itertools.chain(self._default_format_opts.items(), kwargs.items()))
          return formatter(cursor, **format_opts)
  
      def stream_to_file(self, statement, file, format='csv', fs=None, **kwargs):
          """
          Execute a statement against this database and stream results to a file.
  
          This method is a wrapper around `DatabaseClient.stream` that enables the
          iterative writing of cursor results to a file. This is especially useful
          when there are a very large number of results, and loading them all into
          memory would require considerable resources. Note that 'csv' is the
          default format for this method (rather than `pandas`).
  
          Args:
              statement (str): The statement to be executed against the database.
              file (str, file-like-object): The filename where the data should be
                  written, or an open file-like resource.
              format (str): The format to be used ('csv' by default). Format
                  options can be passed via `**kwargs`.
              fs (None, FileSystemClient): The filesystem wihin which the
                  nominated file should be found. If `None`, the local filesystem
                  will be used.
              **kwargs: Additional keyword arguments to pass onto
                  `DatabaseClient.stream`.
          """
          close_later = False
          if isinstance(file, str):
              file = (fs or LocalFsClient()).open(file, 'w')
              close_later = True
  
          try:
              file.writelines(self.stream(statement, format=format, **kwargs))
          finally:
              if close_later:
                  file.close()
  
      def execute_from_file(self, file, fs=None, **kwargs):
          """
          Execute a statement stored in a file.
  
          Args:
              file (str, file-like-object): The path of the file containing the
                  query statement to be executed against the database, or an open
                  file-like resource.
              fs (None, FileSystemClient): The filesystem wihin which the
                  nominated file should be found. If `None`, the local filesystem
                  will be used.
              **kwargs (dict): Extra keyword arguments to pass on to
                  `DatabaseClient.execute`.
  
          Returns:
              DBAPI2 cursor: A DBAPI2 compatible cursor instance.
          """
          close_later = False
          if isinstance(file, str):
              file = (fs or LocalFsClient()).open(file, 'r')
              close_later = True
  
          try:
              return self.execute(file.read(), **kwargs)
          finally:
              if close_later:
                  file.close()
  
      def query_from_file(self, file, fs=None, **kwargs):
          """
          Query using a statement stored in a file.
  
          Args:
              file (str, file-like-object): The path of the file containing the
                  query statement to be executed against the database, or an open
                  file-like resource.
              fs (None, FileSystemClient): The filesystem wihin which the
                  nominated file should be found. If `None`, the local filesystem
                  will be used.
              **kwargs (dict): Extra keyword arguments to pass on to
                  `DatabaseClient.query`.
  
          Returns:
              object: The results of the query formatted as nominated.
          """
          close_later = False
          if isinstance(file, str):
              file = (fs or LocalFsClient()).open(file, 'r')
              close_later = True
  
          try:
              return self.query(file.read(), **kwargs)
          finally:
              if close_later:
                  file.close()
  
      @property
      def template_names(self):
          """
          list: A list of names associated with the templates associated with this
          client.
          """
          return list(self._templates)
  
      def template_add(self, name, body):
          """
          Add a named template to the internal dictionary of templates.
  
          Note: Templates are interpreted as `jinja2` templates. See
          `.template_render` for more information.
  
          Args:
              name (str): The name of the template.
              body (str): The (typically) multiline body of the template.
  
          Returns:
              PrestoClient: A reference to this object.
          """
          self._templates[name] = body
          return self
  
      def template_get(self, name):
          """
          Retrieve a named template.
  
          Args:
              name (str): The name of the template to retrieve.
  
          Raises:
              ValueError: If `name` is not associated with a template.
  
          Returns:
              str: The requested template.
          """
          if name not in self._templates:
              raise ValueError("No such template named: '{}'.".format(name))
          return self._templates[name]
  
      def template_variables(self, name_or_statement, by_name=False):
          """
          Return the set of undeclared variables required for this template.
  
          Args:
              name_or_statement (str): The name of a template (if `by_name` is True)
                  or else a string representation of a `jinja2` template.
              by_name (bool): `True` if `name_or_statement` should be interpreted as a
                  template name, or `False` (default) if `name_or_statement` should be
                  interpreted as a template body.
  
          Returns:
              set<str>: A set of names which the template requires to be rendered.
          """
          ast = jinja2.Environment().parse(
              self.template_render(name_or_statement, by_name=by_name, meta_only=True)
          )
          return jinja2.meta.find_undeclared_variables(ast)
  
      def template_render(self, name_or_statement, context=None, by_name=False,
                          cleanup=False, meta_only=False):
          """
          Render a template by name or value.
  
          In addition to the `jinja2` templating syntax, described in more detail
          in the official `jinja2` documentation, a meta-templating extension is
          also provided. This meta-templating allows you to reference other
          reference other templates. For example, if you had two SQL templates
          named 'template_a' and 'template_b', then you could render them into one
          SQL query using (for example):
  
          .. code-block:: python
  
              .template_render('''
              WITH
                  a AS (
                      {{{template_a}}}
                  ),
                  b AS (
                      {{{template_b}}}
                  )
              SELECT *
              FROM a
              JOIN b ON a.x = b.x
              ''')
  
          Note that template substitution in this way is iterative, so you can
          chain template embedding, provided that such embedding is not recursive.
  
          Args:
              name_or_statement (str): The name of a template (if `by_name` is True)
                  or else a string representation of a `jinja2` template.
              context (dict, None): A dictionary to use as the template context.
                  If not specified, an empty dictionary is used.
              by_name (bool): `True` if `name_or_statement` should be interpreted as a
                  template name, or `False` (default) if `name_or_statement` should be
                  interpreted as a template body.
              cleanup (bool): `True` if the rendered statement should be formatted,
                  `False` (default) otherwise
              meta_only (bool): `True` if rendering should only progress as far as
                  rendering nested templates (i.e. don't actually substitute in
                  variables from the context); `False` (default) otherwise.
  
          Returns:
              str: The rendered template.
          """
          if by_name:
              if name_or_statement not in self._templates:
                  raise ValueError("No such template of name: '{}'.".format(name_or_statement))
              statement = self._templates[name_or_statement]
          else:
              statement = name_or_statement
  
          try:
              from sqlalchemy.sql.base import Executable
              if isinstance(statement, Executable):
                  statement = str(statement.compile(compile_kwargs={"literal_binds": True}))
          except ImportError:
              pass
  
          if context is None or context is False:
              context = {}
  
          template_context = {}
          template_context.update(self._template_context)  # default context
          template_context.update(context)  # context passed in
          intersection = set(self._template_context.keys()) & set(context.keys())
          if intersection:
              logger.warning(
                  "The following default template context keys have been overridden "
                  "by the local context: {}."
                  .format(intersection)
              )
  
          # Substitute in any other named statements recursively
          while '{{{' in statement or '{{%' in statement:
              statement = (
                  jinja2.Template(
                      statement,
                      block_start_string='{{%',
                      block_end_string='%}}',
                      variable_start_string='{{{',
                      variable_end_string='}}}',
                      comment_start_string='{{#',
                      comment_end_string='#}}',
                      undefined=jinja2.StrictUndefined
                  )
                  .render(getattr(self, '_templates', {}))
              )
  
          if not meta_only:
              statement = (
                  jinja2.Template(statement, undefined=jinja2.StrictUndefined)
                  .render(template_context)
              )
  
          if cleanup:
              statement = self.statement_cleanup(statement)
  
          return statement
  
      def execute_from_template(self, name, context=None, **kwargs):
          """
          Render and then execute a named template.
  
          Args:
              name (str): The name of the template to be rendered and executed.
              context (dict): The context in which the template should be rendered.
              **kwargs (dict): Additional parameters to pass to `.execute()`.
  
          Returns:
              DBAPI2 cursor: A DBAPI2 compatible cursor instance.
          """
          statement = self.template_render(name, context, by_name=True)
          return self.execute(statement, **kwargs)
  
      def query_from_template(self, name, context=None, **kwargs):
          """
          Render and then query using a named tempalte.
  
          Args:
              name (str): The name of the template to be rendered and used to query
                  the database.
              context (dict): The context in which the template should be rendered.
              **kwargs (dict): Additional parameters to pass to `.query()`.
  
          Returns:
             object: The results of the query formatted as nominated.
          """
          statement = self.template_render(name, context, by_name=True)
          return self.query(statement, **kwargs)
  
      # Uploading/querying data into data store
      @logging_scope('Query [CTAS]', timed=True)
      @quirk_docs('_query_to_table')
      def query_to_table(self, statement, table, if_exists='fail', **kwargs):
          """
          Run a query and store the results in a table in this database.
  
          Args:
              statement: The statement to be executed.
              table (str): The name of the table into which the dataframe should
                  be uploaded.
              if_exists (str): if nominated table already exists: 'fail' to do
                  nothing, 'replace' to drop, recreate and insert data into new
                  table, and 'append' to add data from this table into the
                  existing table.
              **kwargs (dict): Additional keyword arguments to pass onto
                  `DatabaseClient._query_to_table`.
  
          Returns:
              DB-API cursor: The cursor object associated with the execution.
          """
          assert if_exists in {'fail', 'replace', 'append'}
          table = self._parse_namespaces(table)
          return self._query_to_table(statement, table, if_exists=if_exists, **kwargs)
  
      @logging_scope('Dataframe Upload', timed=True)
      @quirk_docs('_dataframe_to_table')
      @require_connection
      def dataframe_to_table(self, df, table, if_exists='fail', **kwargs):
          """
          Upload a local pandas dataframe into a table in this database.
  
          Args:
              df (pandas.DataFrame): The dataframe to upload into the database.
              table (str, ParsedNamespaces): The name of the table into which the
                  dataframe should be uploaded.
              if_exists (str): if nominated table already exists: 'fail' to do
                  nothing, 'replace' to drop, recreate and insert data into new
                  table, and 'append' to add data from this table into the
                  existing table.
              **kwargs (dict): Additional keyword arguments to pass onto
                  `DatabaseClient._dataframe_to_table`.
          """
          assert if_exists in {'fail', 'replace', 'append'}
          self._dataframe_to_table(df, self._parse_namespaces(table), if_exists=if_exists, **kwargs)
  
      # Table properties
  
      @abstractmethod
      def _execute(self, statement, cursor, wait, session_properties):
          pass
  
      def _query_to_table(self, statement, table, if_exists, **kwargs):
          raise NotImplementedError
  
      def _dataframe_to_table(self, df, table, if_exists='fail', **kwargs):
          raise NotImplementedError
  
      def _cursor_empty(self, cursor):
          return cursor is None
  
      def _parse_namespaces(self, name, level=0, defaults=None):
          return ParsedNamespaces.from_name(
              name,
              self.NAMESPACE_NAMES[:-level] if level > 0 else self.NAMESPACE_NAMES,
              quote_char=self.NAMESPACE_QUOTECHAR,
              separator=self.NAMESPACE_SEPARATOR,
              defaults=self.NAMESPACE_DEFAULT if defaults is None else defaults
          )
  
      @quirk_docs('_table_list')
      def table_list(self, namespace=None, renew=True, **kwargs):
          """
          Return a list of table names in the data source as a DataFrame.
  
          Args:
              namespace (str): The namespace in which to look for tables.
              renew (bool): Whether to renew the table list or use cached results
                  (default: True).
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              list<str>: The names of schemas in this database.
          """
          return self._table_list(self._parse_namespaces(namespace, level=1), renew=renew, **kwargs)
  
      @abstractmethod
      def _table_list(self, namespace, **kwargs):
          pass
  
      @quirk_docs('_table_exists')
      def table_exists(self, table, renew=True, **kwargs):
          """
          Check whether a table exists.
  
          Args:
              table (str): The table for which to check.
              renew (bool): Whether to renew the table list or use cached results
                  (default: True).
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              bool: `True` if table exists, and `False` otherwise.
          """
          return self._table_exists(table=self._parse_namespaces(table), renew=renew, **kwargs)
  
      @abstractmethod
      def _table_exists(self, table, **kwargs):
          pass
  
      @quirk_docs('_table_drop')
      def table_drop(self, table, **kwargs):
          """
          Remove a table from the database.
  
          Args:
              table (str): The table to drop.
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              DB-API cursor: The cursor associated with this execution.
          """
          return self._table_drop(table=self._parse_namespaces(table), **kwargs)
  
      @abstractmethod
      def _table_drop(self, table, **kwargs):
          pass
  
      @quirk_docs('_table_desc')
      def table_desc(self, table, renew=True, **kwargs):
          """
          Describe a table in the database.
  
          Args:
              table (str): The table to describe.
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              pandas.DataFrame: A dataframe description of the table.
          """
          return self._table_desc(table=self._parse_namespaces(table), renew=renew, **kwargs)
  
      @abstractmethod
      def _table_desc(self, table, **kwargs):
          pass
  
      @quirk_docs('_table_head')
      def table_head(self, table, n=10, renew=True, **kwargs):
          """
          Retrieve the first `n` rows from a table.
  
          Args:
              table (str): The table from which to extract data.
              n (int): The number of rows to extract.
              renew (bool): Whether to renew the table list or use cached results
                  (default: True).
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              pandas.DataFrame: A dataframe representation of the first `n` rows
                  of the nominated table.
          """
          return self._table_head(table=self._parse_namespaces(table), n=n, renew=renew, **kwargs)
  
      @abstractmethod
      def _table_head(self, table, n=10, **kwargs):
          pass
  
      @quirk_docs('_table_props')
      def table_props(self, table, renew=True, **kwargs):
          """
          Retrieve the properties associated with a table.
  
          Args:
              table (str): The table from which to extract data.
              renew (bool): Whether to renew the table list or use cached results
                  (default: True).
              **kwargs (dict): Additional arguments passed through to implementation.
  
          Returns:
              pandas.DataFrame: A dataframe representation of the table
                  properties.
          """
          return self._table_props(table=self._parse_namespaces(table), renew=renew, **kwargs)
  
      @abstractmethod
      def _table_props(self, table, **kwargs):
          pass
  
      @override
      def _register_magics(self, base_name):
          """
          The following magic functions will be registered (assuming that
          the base name is chosen to be 'hive'):
          - Cell Magics:
              - `%%hive`: For querying the database.
              - `%%hive.execute`: For executing a statement against the database.
              - `%%hive.stream`: For executing a statement against the database,
                  and streaming the results.
              - `%%hive.template`: The defining a new template.
              - `%%hive.render`: Render a provided query statement.
          - Line Magics:
              - `%hive`: For querying the database using a named template.
              - `%hive.execute`: For executing a named template statement against
                  the database.
              - `%hive.stream`: For executing a named template against the database,
                  and streaming the results.
              - `%hive.render`: Render a provided a named template.
              - `%hive.desc`: Describe the table nominated.
              - `%hive.head`: Return the first rows in a specified table.
              - `%hive.props`: Show the properties specified for a nominated table.
  
          Documentation for these magics is provided online.
          """
          from IPython import get_ipython
          from IPython.core.magic import register_line_magic, register_cell_magic, register_line_cell_magic
  
          def statement_executor_magic(executor, statement, variable=None, show='head', transpose=False, template=True, context=None, **kwargs):
  
              ip = get_ipython()
  
              if context is None:
                  context = ip.user_ns
  
              # Line magic
              if statement is None:
                  return self.query_from_template(variable, context=context, **kwargs)
  
              # Cell magic
              result = getattr(self, executor)(statement, template=template, context=context, **kwargs)
  
              if variable is not None:
                  ip.user_ns[variable] = result
  
              if executor != 'query':
                  if variable is None:
                      return result
                  return
              elif variable is None:
                  return result
  
              format = kwargs.get('format', self.DEFAULT_CURSOR_FORMATTER)
              if show == 'head':
                  show = 10
              if isinstance(show, int):
                  r = result.head(show) if format == 'pandas' else result[:show]
              elif show == 'all':
                  r = result
              elif show in (None, 'none'):
                  return None
              else:
                  raise ValueError("Omniduct does not recognise the argument show='{0}' in cell magic.".format(show))
  
              if format == 'pandas' and transpose:
                  return r.T
              return r
  
          @register_line_cell_magic(base_name)
          @process_line_cell_arguments
          def query_magic(*args, **kwargs):
              return statement_executor_magic('query', *args, **kwargs)
  
          @register_line_cell_magic("{}.{}".format(base_name, 'execute'))
          @process_line_cell_arguments
          def execute_magic(*args, **kwargs):
              return statement_executor_magic('execute', *args, **kwargs)
  
          @register_line_cell_magic("{}.{}".format(base_name, 'stream'))
          @process_line_cell_arguments
          def stream_magic(*args, **kwargs):
              return statement_executor_magic('stream', *args, **kwargs)
  
          @register_cell_magic("{}.{}".format(base_name, 'template'))
          @process_line_arguments
          def template_add(body, name):
              self.template_add(name, body)
  
          @register_line_cell_magic("{}.{}".format(base_name, 'render'))
          @process_line_cell_arguments
          def template_render_magic(body=None, name=None, context=None, show=True,
                                    cleanup=False, meta_only=False):
  
              ip = get_ipython()
  
              if body is None:
                  assert name is not None, "Name must be specified in line-mode."
                  rendered = self.template_render(
                      name, context=context or ip.user_ns, by_name=True,
                      cleanup=cleanup, meta_only=meta_only
                  )
              else:
                  rendered = self.template_render(
                      body, context=context or ip.user_ns, by_name=False,
                      cleanup=cleanup, meta_only=meta_only
                  )
                  if name is not None:
                      ip.user_ns[name] = rendered
  
              if show:
                  print(rendered)
              else:
                  return rendered
  
          @register_line_magic("{}.{}".format(base_name, 'desc'))
          @process_line_arguments
          def table_desc(table_name, **kwargs):
              return self.table_desc(table_name, **kwargs)
  
          @register_line_magic("{}.{}".format(base_name, 'head'))
          @process_line_arguments
          def table_head(table_name, **kwargs):
              return self.table_head(table_name, **kwargs)
  
          @register_line_magic("{}.{}".format(base_name, 'props'))
          @process_line_arguments
          def table_props(table_name, **kwargs):
              return self.table_props(table_name, **kwargs)
  