@click.command('civis', help="Print Civis")
def civis_ascii_art():
    print(_CIVIS_ASCII_ART) @click.command('download')
@click.argument('file_id', type=int)
@click.argument('path')
def files_download_cmd(file_id, path):
    """Download a Civis File to a specified local path."""
    with open(path, 'wb') as f:
        civis_to_file(file_id, f) @click.command('upload')
@click.argument('path')
@click.option('--name', type=str, default=None,
              help="A name for the Civis File (defaults to the base file name")
@click.option('--expires-at', type=str, default=None,
              help="The date and time the file will expire "
                   "(ISO-8601 format, e.g., \"2017-01-15\" or "
                   "\"2017-01-15T15:25:10Z\"). "
                   "Set \"never\" for the file to not expire."
                   "The default is the default in Civis (30 days).")
def files_upload_cmd(path, name, expires_at):
    """Upload a local file to Civis and get back the File ID."""

    if name is None:
        name = os.path.basename(path)

    if expires_at is None:
        # Use the default in Civis platform (30 days).
        expires_kwarg = {}
    elif expires_at.lower() == "never":
        expires_kwarg = {"expires_at": None}
    else:
        expires_kwarg = {"expires_at": expires_at}

    with open(path, 'rb') as f:
        file_id = file_to_civis(f, name=name, **expires_kwarg)
    print(file_id) @click.command(
    'follow-log',
    help='Output live log from the most recent job run.' + _FOLLOW_LOG_NOTE)
@click.argument('id', type=int)
def jobs_follow_log(id):
    client = civis.APIClient()
    runs = client.jobs.list_runs(id, limit=1, order='id', order_dir='desc')
    if not runs:
        raise click.ClickException('No runs found for that job ID.')
    run_id = runs[0].id
    print('Run ID: ' + str(run_id))
    _jobs_follow_run_log(id, run_id) @click.command(
    'follow-run-log',
    help='Output live run log.' + _FOLLOW_LOG_NOTE)
@click.argument('id', type=int)
@click.argument('run_id', type=int)
def jobs_follow_run_log(id, run_id):
    _jobs_follow_run_log(id, run_id) @click.command('download')
@click.argument('notebook_id', type=int)
@click.argument('path')
def notebooks_download_cmd(notebook_id, path):
    """Download a notebook to a specified local path."""
    client = civis.APIClient()
    info = client.notebooks.get(notebook_id)
    response = requests.get(info['notebook_url'], stream=True)
    response.raise_for_status()
    chunk_size = 32 * 1024
    chunked = response.iter_content(chunk_size)
    with open(path, 'wb') as f:
        for lines in chunked:
            f.write(lines) @click.command('new')
@click.argument('language', type=click.Choice(['python3', 'r']),
                default='python3')
@click.option('--mem', type=int, default=None,
              help='Memory allocated for this notebook in MiB.')
@click.option('--cpu', type=int, default=None,
              help='CPU available for this notebook in 1/1000 of a core.')
def notebooks_new_cmd(language='python3', mem=None, cpu=None):
    """Create a new notebook and open it in the browser."""
    client = civis.APIClient()
    kwargs = {'memory': mem, 'cpu': cpu}
    kwargs = {k: v for k, v in kwargs.items() if v is not None}
    new_nb = client.notebooks.post(language=language, **kwargs)
    print("Created new {language} notebook with ID {id} in Civis Platform"
          " (https://platform.civisanalytics.com/#/notebooks/{id})."
          .format(language=language, id=new_nb.id))
    _notebooks_up(new_nb.id)
    _notebooks_open(new_nb.id) @click.command('up')
@click.argument('notebook_id', type=int)
@click.option('--mem', type=int, default=None,
              help='Memory allocated for this notebook in MiB.')
@click.option('--cpu', type=int, default=None,
              help='CPU available for this notebook in 1/1000 of a core.')
def notebooks_up(notebook_id, mem=None, cpu=None):
    """Start an existing notebook and open it in the browser."""
    client = civis.APIClient()
    kwargs = {'memory': mem, 'cpu': cpu}
    kwargs = {k: v for k, v in kwargs.items() if v is not None}
    client.notebooks.patch(notebook_id, **kwargs)
    _notebooks_up(notebook_id)
    _notebooks_open(notebook_id) @click.command('down')
@click.argument('notebook_id', type=int)
def notebooks_down(notebook_id):
    """Shut down a running notebook."""
    client = civis.APIClient()
    nb = client.notebooks.get(notebook_id)
    state = nb['most_recent_deployment']['state']
    if state not in ['running', 'pending']:
        print('Notebook is in state "{}" and can\'t be stopped.'.format(state))
    deployment_id = nb['most_recent_deployment']['deploymentId']
    client.notebooks.delete_deployments(notebook_id, deployment_id) @click.command('open')
@click.argument('notebook_id', type=int)
def notebooks_open(notebook_id):
    """Open an existing notebook in the browser."""
    _notebooks_open(notebook_id) @click.command('sql')
@click.option('--dbname', '-d', type=str, required=True,
              help='Execute the query on this Civis Platform database')
@click.option('--command', '-c', type=str, default=None,
              help='Execute a single input command string')
@click.option('--filename', '-f', type=click.Path(exists=True),
              help='Execute a query read from the given file')
@click.option('--output', '-o', type=click.Path(),
              help='Download query results to this file')
@click.option('--quiet', '-q', is_flag=True, help='Suppress screen output')
@click.option('-n', type=int, default=100,
              help="Display up to this many rows of the result. Max 100.")
def sql_cmd(dbname, command, filename, output, quiet, n):
    """\b Execute a SQL query in Civis Platform

    If neither a command nor an input file is specified, read
    the SQL command from stdin.
    If writing to an output file, use a Civis SQL script and write the
    entire query output to the specified file.
    If not writing to an output file, use a Civis Query, and return a
    preview of the results, up to a maximum of 100 rows.
    """
    if filename:
        with open(filename, 'rt') as f:
            sql = f.read()
    elif not command:
        # Read the SQL query from user input. This also allows use of a heredoc
        lines = []
        while True:
            try:
                _i = input()
            except (KeyboardInterrupt, EOFError):
                # The end of a heredoc produces an EOFError.
                break
            if not _i:
                break
            else:
                lines.append(_i)
        sql = '\n'.join(lines)
    else:
        sql = command

    if not sql:
        # If the user didn't enter a query, exit.
        if not quiet:
            print('ERROR: Did not receive a SQL query.', file=sys.stderr)
        return

    if not quiet:
        print('\nExecuting query...', file=sys.stderr)
    if output:
        fut = civis.io.civis_to_csv(output, sql, database=dbname)
        fut.result()  # Block for completion and raise exceptions if any
        if not quiet:
            print("Downloaded the result of the query to %s." % output,
                  file=sys.stderr)
    else:
        fut = civis.io.query_civis(sql, database=dbname,
                                   preview_rows=n, polling_interval=3)
        cols = fut.result()['result_columns']
        rows = fut.result()['result_rows']
        if not quiet:
            print('...Query complete.\n', file=sys.stderr)
            print(_str_table_result(cols, rows)) @lru_cache(maxsize=4)
def get_api_spec(api_key, api_version="1.0", user_agent="civis-python"):
    """Download the Civis API specification.

    Parameters
    ----------
    api_key : str
        Your API key obtained from the Civis Platform.
    api_version : string, optional
        The version of endpoints to call. May instantiate multiple client
        objects with different versions.  Currently only "1.0" is supported.
    user_agent : string, optional
        Provide this user agent to the the Civis API, along with an
        API client version tag and ``requests`` version tag.
    """
    if api_version == "1.0":
        with open_session(api_key, user_agent=user_agent) as sess:
            request = Request('GET', "{}endpoints".format(get_base_url()))
            pre_request = sess.prepare_request(request)
            response = retry_request('get', pre_request, sess, MAX_RETRIES)
    else:
        msg = "API specification for api version {} cannot be found"
        raise ValueError(msg.format(api_version))
    if response.status_code in (401, 403):
        msg = "{} error downloading API specification. API key may be expired."
        raise requests.exceptions.HTTPError(msg.format(response.status_code))
    response.raise_for_status()
    spec = response.json(object_pairs_hook=OrderedDict)
    return spec CACHED_SPEC_PATH = os.path.join(os.path.expanduser('~'),
                                ".civis_api_spec.json") def parse_method_name(verb, path):
    """ Create method name from endpoint path

    Create method name as the http verb (method) followed by
    any static path parameters. As there is ambiguity where
    a path may optionally have a path parameter passed (i.e.
    GET reports/ and GET reports/{id}), any GET method is changed
    to "list" for endpoints where the final path parameter is
    static.

    Examples
    --------
    >>> parse_method_name("get", "url.com/containers")
    list_containers
    >>> parse_method_name("get", "url.com/containers/{id}")
    get_containers
    >>> parse_method_name("get", "url.com/containers/{id}/runs/{run_id}")
    get_containers_runs
    >>> parse_method_name("post", "containers/{id}/runs/{run_id}")
    post_containers_runs
    """
    path_elems = path.split("/")[1:]
    name_elems = []
    for i, elem in enumerate(path_elems):
        prev_elem = path_elems[i - 1] if i > 0 else None
        if not bracketed(elem):
            name_elems.append(elem)
        elif prev_elem and bracketed(prev_elem):
            name_elems.append(prev_elem.strip('{|}'))
    final_elem = path_elems[-1] if path_elems else ""
    verb = "list" if verb == "get" and (not bracketed(final_elem)) else verb
    path_name = "_".join(name_elems)
    method_name = "_".join((verb, path_name)) if path_name else verb
    return re.sub("-", "_", method_name) def open_session(api_key, user_agent="civis-python"):
    """Create a new Session which can connect with the Civis API"""
    civis_version = civis.__version__
    session = requests.Session()
    session.auth = (api_key, '')
    session_agent = session.headers.get('User-Agent', '')
    ver_string = "{}.{}.{}".format(sys.version_info.major,
                                   sys.version_info.minor,
                                   sys.version_info.micro)
    user_agent = "{}/Python v{} Civis v{} {}".format(
        user_agent, ver_string, civis_version, session_agent)
    headers = {"User-Agent": user_agent.strip()}
    job_id, run_id = os.getenv("CIVIS_JOB_ID"), os.getenv("CIVIS_RUN_ID")
    if job_id:
        headers.update({"X-Civis-Job-ID": job_id, "X-Civis-Run-ID": run_id})
    session.headers.update(headers)

    return session def retry_request(method, prepared_req, session, max_retries=10):
    retry_conditions = None

    def _make_request(req, sess):
        """send the prepared session request"""
        response = sess.send(req)
        return response

    def _return_last_value(retry_state):
        """return the result of the last call attempt
        and let code pick up the error"""
        return retry_state.outcome.result()

    if method.upper() == 'POST':
        retry_conditions = (
            retry_if_result(
                lambda res: res.status_code in civis.civis.POST_RETRY_CODES)
        )
    elif method.upper() in civis.civis.RETRY_VERBS:
        retry_conditions = (
            retry_if_result(
                lambda res: res.status_code in civis.civis.RETRY_CODES)
        )

    if retry_conditions:
        retry_config = Retrying(
            retry=retry_conditions,
            wait=wait_for_retry_after_header(
                fallback=wait_random_exponential(multiplier=2, max=60)),
            stop=(stop_after_delay(600) | stop_after_attempt(max_retries)),
            retry_error_callback=_return_last_value,
        )
        response = retry_config(_make_request, prepared_req, session)
        return response

    response = _make_request(prepared_req, session)
    return response MAX_RETRIES = 10
import calendar
import json
import logging
import os
import re
import sys
import time
import click
import yaml
from collections import OrderedDict
from functools import partial
from warnings import warn
from jsonref import JsonRef
from requests import Request
from civis.cli._cli_commands import (
    civis_ascii_art, files_download_cmd, files_upload_cmd,
    jobs_follow_log, jobs_follow_run_log, notebooks_download_cmd,
    notebooks_new_cmd, notebooks_up, notebooks_down, notebooks_open, sql_cmd)
from civis.resources import get_api_spec, CACHED_SPEC_PATH
from civis.resources._resources import parse_method_name
from civis._utils import open_session, retry_request, MAX_RETRIES
    if path.startswith(resource_name):
        path = path[len(resource_name):].strip("-")

    name = parse_method_name(method, path).replace("_", "-")
    return name


def param_case_map(param_names):
    # Return a map from snake_case parameter names (for click) to camelCase
    # versions for the API. We need this mapping because the API
    # expects/prefers camelCase but click prefers snake_case.
    # We could have make a snake_to_camel function, but ensuring that that
    # was in sync with camel_to_snake might be difficult because there are some
    # edge cases like provideAPIKey <-> provide_api_key.
    result = {camel_to_snake(k): k for k in param_names}
    return result


def invoke(method, path, op, *args, **kwargs):
    """
    If json_output is in `kwargs` then the output is json. Otherwise, it is
    yaml.
    """
    # Remove None b/c click passes everything in as None if it's not set.
    kwargs = {k: v for k, v in kwargs.items()
              if v is not None}
    json_output = kwargs.pop('json_output', False)

    # Construct the body of the request.
    body = {}
    body_params = [p for p in op['parameters'] if p['in'] == 'body']
    if body_params:
        if len(body_params) != 1:
            raise ValueError(
                "There can be only one body parameter, "
                f"but {len(body_params)} are found: {body_params}"
            )
        props = body_params[0]['schema']['properties']
        param_map = param_case_map(props.keys())
        body = {param_map[k]: v for k, v in kwargs.items()
                if k in param_map}

    # Construct the query part of the request.
    query_names = {p['name'] for p in op['parameters'] if p['in'] == 'query'}
    param_map = param_case_map(query_names)
    query = {param_map[k]: v for k, v in kwargs.items()
             if k in param_map}

    # Make the request.
    request_info = dict(
        params=query,
        json=body,
        url=get_base_api_url() + path.format(**kwargs),
        method=method
    )
    with open_session(get_api_key(), user_agent=CLI_USER_AGENT) as sess:
        request = Request(**request_info)
        pre_request = sess.prepare_request(request)
        response = retry_request(method, pre_request, sess, MAX_RETRIES)

    # Print the response to stderr and set exit code to 1 if there was an error
    output_file = sys.stdout
    exit_code = 0
    if not (200 <= response.status_code < 300):
        output_file = sys.stderr
        exit_code = 1

    # Print the output, if there is any.
    # For commands such as DELETE /scripts/containers/{script_id}/runs/{id},
    # response ends up being " " here.
    try:
        if json_output:
            json.dump(response.json(), output_file)
        else:
            yaml.safe_dump(response.json(), output_file,
                           default_flow_style=False)
        output_file.flush()

    # json throws a ValueError if it is passed a blank string to load.
    except ValueError as e:
        # If the message was not blank, print an error message.
        # Otherwise, do nothing.
        if response.text.strip():
            print("Error parsing response: {}".format(e), file=sys.stderr)

    sys.exit(exit_code)


def retrieve_spec_dict(api_version="1.0"):
    """Retrieve the API specification from a cached version or from Civis."""

    refresh_spec = True
    now_timestamp = calendar.timegm(time.gmtime())

    try:
        # If the cached spec is from the last 24 hours, use it.
        modified_time = os.path.getmtime(CACHED_SPEC_PATH)
        if now_timestamp - modified_time < 24 * 3600:
            refresh_spec = False
            with open(CACHED_SPEC_PATH) as f:
                spec_dict = json.load(f, object_pairs_hook=OrderedDict)
    except (FileNotFoundError, ValueError):
        # If the file doesn't exist or we can't parse it, just keep going.
        refresh_spec = True

    # Download the spec and cache it in the user's home directory.
    if refresh_spec:
        spec_dict = get_api_spec(get_api_key(), api_version=api_version,
                                 user_agent=CLI_USER_AGENT)
        with open(CACHED_SPEC_PATH, "w") as f:
            json.dump(spec_dict, f)
    return spec_dict


def add_extra_commands(cli):
    """Add useful commands that are not in the OpenAPI spec."""
    files_cmd = cli.commands['files']
    files_cmd.add_command(files_download_cmd)
    files_cmd.add_command(files_upload_cmd)
    notebooks_cmd = cli.commands['notebooks']

    notebooks_cmd.add_command(notebooks_download_cmd)