aiscalator.jupyter.cli.new()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 23
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 23
rs 9.4
c 0
b 0
f 0
cc 3
nop 3
1
# -*- coding: utf-8 -*-
2
# Apache Software License 2.0
3
#
4
# Copyright (c) 2018, Christophe Duong
5
#
6
# Licensed under the Apache License, Version 2.0 (the "License");
7
# you may not use this file except in compliance with the License.
8
# You may obtain a copy of the License at
9
#
10
# http://www.apache.org/licenses/LICENSE-2.0
11
#
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS,
14
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
# See the License for the specific language governing permissions and
16
# limitations under the License.
17
"""
18
CLI module for Jupyter related commands.
19
"""
20
import logging
21
import os
22
import sys
23
from subprocess import PIPE  # nosec
24
from subprocess import Popen
25
from subprocess import TimeoutExpired
26
from threading import Thread
27
28
import click
29
30
from aiscalator import __version__
31
from aiscalator.core.config import AiscalatorConfig
32
from aiscalator.jupyter import command
33
34
35
@click.group()
36
@click.version_option(version=__version__)
37
def jupyter():
38
    """Notebook environment to explore and handle data."""
39
40
41
@jupyter.command()
42
@click.version_option(version=__version__)
43
def setup():
44
    """Setup the current docker image to run notebooks."""
45
    update_aiscalator()
46
    # TODO to implement
47
    logging.error("Not implemented yet")
48
49
50
@jupyter.command()
51
@click.version_option(version=__version__)
52
def update():
53
    """
54
    Checks and tries to update the current docker image
55
    to run notebooks to a newer version.
56
57
    Initiates a docker pull of the latest images we are depending on
58
    and build the next aiscalator images from there.
59
    Before replacing the version tags in the Dockerfile, we make sure
60
    to do a maximum in the background while still having a working
61
    image in the meantime.
62
63
    """
64
    update_aiscalator()
65
    # TODO to implement
66
    logging.error("Not implemented yet")
67
68
69
@jupyter.command()
70
@click.option('--name', prompt='What is the name of your step?',
71
              help="Name of the new step to create",
72
              metavar='<STEP>')
73
@click.option('-f', '--format', 'output_format',
74
              help="format of the configuration file (default is hocon)",
75
              type=click.Choice(['json', 'hocon']),
76
              default='hocon')
77
# TODO: import an existing notebook and create a new aiscalate step from it
78
@click.argument('path', type=click.Path())
79
@click.version_option(version=__version__)
80
def new(name, output_format, path):
81
    """Create a new notebook associated with a new aiscalate step config."""
82
    update_aiscalator()
83
    file_conf = os.path.join(path, name, name) + '.conf'
84
    file_json = os.path.join(path, name, name) + '.json'
85
    if os.path.exists(file_conf):
86
        prompt_edit(file_conf)
87
    elif os.path.exists(file_json):
88
        prompt_edit(file_json)
89
    else:
90
        click.echo(command.jupyter_new(name, path,
91
                                       output_format=output_format))
92
93
94
def prompt_edit(file):
95
    """
96
    When creating a new step, if it is already defined,
97
    ask to edit instead
98
99
    Parameters
100
    ----------
101
    file : str
102
        existing configuration file
103
104
    """
105
    msg = file + ' already exists. Did you mean to run:\n'
106
    for i in sys.argv:
107
        if i != "new":
108
            msg += i + ' '
109
        else:
110
            break
111
    msg += "edit " + file + " instead?"
112
    if click.confirm(msg, abort=True):
113
        conf = AiscalatorConfig(config=file)
114
        click.echo(command.jupyter_edit(conf))
115
116
117
@jupyter.command()
118
@click.argument('conf', type=click.Path(exists=True))
119
@click.argument('notebook', nargs=-1)
120
@click.option('-p', '--param', type=(str, str), multiple=True)
121
@click.option('-r', '--param_raw', type=(str, str), multiple=True)
122
@click.version_option(version=__version__)
123
# TODO add parameters override from CLI
124
def edit(conf, notebook, param, param_raw):
125
    """Edit the notebook from an aiscalate config with JupyterLab."""
126
    update_aiscalator()
127
    if len(notebook) < 2:
128
        notebook = notebook[0] if notebook else None
129
        app_config = AiscalatorConfig(config=conf,
130
                                      step_selection=notebook)
131
        click.echo(command.jupyter_edit(app_config,
132
                                        param=param, param_raw=param_raw))
133
    else:
134
        raise click.BadArgumentUsage("Expecting one or less notebook names")
135
136
137
@jupyter.command()
138
@click.argument('conf', type=click.Path(exists=True))
139
@click.argument('notebook', nargs=-1)
140
@click.option('-p', '--param', type=(str, str), multiple=True)
141
@click.option('-r', '--param_raw', type=(str, str), multiple=True)
142
@click.version_option(version=__version__)
143
# TODO add parameters override from CLI
144
def run(conf, notebook, param, param_raw):
145
    """Run the notebook from an aiscalate config without GUI."""
146
    update_aiscalator()
147
    if notebook:
148
        for note in notebook:
149
            app_config = AiscalatorConfig(config=conf,
150
                                          step_selection=note)
151
            click.echo(command.jupyter_run(app_config,
152
                                           param=param, param_raw=param_raw))
153
    else:
154
        app_config = AiscalatorConfig(config=conf)
155
        click.echo(command.jupyter_run(app_config,
156
                                       param=param, param_raw=param_raw))
157
158
159
def update_aiscalator():
160
    """
161
    Create and run Thread to execute auto update in the background
162
    """
163
    worker = Thread(name='autoUpdate', target=run_auto_update)
164
    worker.start()
165
166
167
def run_auto_update():
168
    """
169
    Checks and tries to update Aiscalator itself from Pypi if necessary
170
    """
171
    version = pip_list = grep = sed = pip_install = None
172
    try:
173
        cmd = ["pip", "list", "--outdated"]
174
        pip_list = Popen(cmd, stdout=PIPE)
175
        pip_list_out, _ = pip_list.communicate(timeout=60)
176
177
        cmd = ["grep", "aiscalator"]
178
        grep = Popen(cmd, stdin=PIPE, stdout=PIPE)
179
        grep_out, _ = grep.communicate(pip_list_out, timeout=15)
180
181
        cmd = ["sed", "-E", "s/aiscalator[ \\t]+([0-9.]+)/\\1/"]
182
        sed = Popen(cmd, stdin=PIPE, stdout=PIPE)
183
        version, _ = sed.communicate(grep_out, timeout=15)
184
        if version:
185
            version = version.decode("utf-8")
186
    except TimeoutExpired:
187
        if pip_list:
188
            pip_list.kill()
189
        if grep:
190
            grep.kill()
191
        if sed:
192
            sed.kill()
193
    if version and version < __version__:
194
        msg = "A new update of AIscalator (v" + version.strip("\n")
195
        msg += ") is now available!\nShould we upgrade? Current is v"
196
        msg += __version__
197
        try:
198
            cmd = ["pip", "install", "--upgrade", "aiscalator"]
199
            pip_install = Popen(cmd)
200
            pip_install.communicate(timeout=120)
201
        except TimeoutExpired:
202
            if pip_install:
203
                pip_install.kill()
204