Untitled
unknown
plain_text
2 years ago
38 kB
12
Indexable
from __future__ import absolute_import, print_function
import getpass
import jinja2
import logging
import os
import pandas as pd
import random
import socket
import sys
import time
import traceback
from builtins import map, str
from contextlib import closing
from logging import ERROR as LOG_ERROR
from threading import Timer
from werkzeug.routing import Map
from flask import (
Flask,
jsonify,
redirect,
render_template,
request,
url_for as flask_url_for,
)
from flask.testing import FlaskClient
import requests
from flask_compress import Compress
from six import PY3
import dtale.auth as auth
import dtale.global_state as global_state
import dtale.config as dtale_config
from dtale import dtale
from dtale.cli.clickutils import retrieve_meta_info_and_version, setup_logging
from dtale.dash_application import views as dash_views
from dtale.utils import (
DuplicateDataError,
build_shutdown_url,
build_url,
dict_merge,
fix_url_path,
get_host,
get_url_unquote,
is_app_root_defined,
make_list,
running_with_flask_debug,
)
from dtale.views import DtaleData, head_endpoint, is_up, kill, startup
if PY3:
import _thread
else:
import thread as _thread
logger = logging.getLogger(__name__)
USE_NGROK = False
USE_COLAB = False
JUPYTER_SERVER_PROXY = False
ACTIVE_HOST = None
ACTIVE_PORT = None
SSL_CONTEXT = None
_basepath = os.path.dirname(__file__)
_filepath = os.path.abspath(os.path.join(_basepath, "static"))
SHORT_LIFE_PATHS = [
"dist",
os.path.join(_filepath, "dist"),
"dash",
os.path.join(_filepath, "dash"),
]
SHORT_LIFE_TIMEOUT = 60
REAPER_TIMEOUT = 60.0 * 60.0 # one-hour
class DtaleFlaskTesting(FlaskClient):
"""
Overriding Flask's implementation of flask.FlaskClient so we
can control the port associated with tests.
This class is required for setting the port on your test so that
we won't have SETTING keys colliding with other tests since the default
for every test would be 80.
:param args: Optional arguments to be passed to :class:`flask:flask.FlaskClient`
:param kwargs: Optional keyword arguments to be passed to :class:`flask:flask.FlaskClient`
"""
def __init__(self, *args, **kwargs):
"""
Constructor method
"""
self.host = kwargs.pop("hostname", "localhost")
self.port = kwargs.pop("port", random.randint(1025, 65535)) or random.randint(
1025, 65535
)
super(DtaleFlaskTesting, self).__init__(*args, **kwargs)
self.application.config["SERVER_NAME"] = "{host}:{port}".format(
host=self.host, port=self.port
)
self.application.config["SESSION_COOKIE_DOMAIN"] = "localhost.localdomain"
def get(self, *args, **kwargs):
"""
:param args: Optional arguments to be passed to :meth:`flask:flask.FlaskClient.get`
:param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.FlaskClient.get`
"""
return super(DtaleFlaskTesting, self).get(url_scheme="http", *args, **kwargs)
def contains_route(routes, route):
return any((r.rule == route.rule and r.endpoint == route.endpoint) for r in routes)
class DtaleFlask(Flask):
"""
Overriding Flask's implementation of
get_send_file_max_age, test_client & run
:param import_name: the name of the application package
:param reaper_on: whether to run auto-reaper subprocess
:type reaper_on: bool
:param args: Optional arguments to be passed to :class:`flask:flask.Flask`
:param kwargs: Optional keyword arguments to be passed to :class:`flask:flask.Flask`
"""
def __init__(
self, import_name, reaper_on=True, url=None, app_root=None, *args, **kwargs
):
"""
Constructor method
:param reaper_on: whether to run auto-reaper subprocess
:type reaper_on: bool
"""
self.reaper_on = reaper_on
self.reaper = None
self._setup_url_props(url)
self.port = None
self.app_root = app_root
super(DtaleFlask, self).__init__(import_name, *args, **kwargs)
def _setup_url_props(self, url):
self.base_url = url
self.shutdown_url = build_shutdown_url(url)
def update_template_context(self, context):
super(DtaleFlask, self).update_template_context(context)
if self.app_root is not None:
context["url_for"] = self.url_for
def url_for(self, endpoint, *args, **kwargs):
if self.app_root is not None and endpoint == "static":
if "filename" in kwargs:
return fix_url_path(
"{}/{}/{}".format(
self.app_root, self.static_url_path, kwargs["filename"]
)
)
return fix_url_path("{}/{}".format(self.app_root, args[0]))
if hasattr(Flask, "url_for"):
return Flask.url_for(self, endpoint, *args, **kwargs)
return flask_url_for(endpoint, *args, **kwargs)
def _override_routes(self, rule):
try:
routes_to_remove = [r for r in self.url_map._rules if r.rule == rule]
if routes_to_remove:
updated_rules = [
r.empty()
for r in self.url_map._rules
if not contains_route(routes_to_remove, r)
]
url_map_keys = [
"default_subdomains",
"charset",
"strict_slashes",
"merge_slashes",
"redirect_defaults",
"converters",
"sort_parameters",
"sort_key",
"encoding_errors",
"host_matching",
]
url_map_data = {
k: getattr(self.url_map, k)
for k in url_map_keys
if hasattr(self.url_map, k)
}
self.url_map = Map(rules=updated_rules, **url_map_data)
self.url_map._remap = True
self.url_map.update()
except BaseException:
logger.exception(
"Could not override routes, if you're trying to specify a route for '/' then it will be ignored..."
)
def route(self, rule, **options):
self._override_routes(rule)
return super(DtaleFlask, self).route(rule, **options)
def run(self, *args, **kwargs):
"""
:param args: Optional arguments to be passed to :meth:`flask:flask.run`
:param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.run`
"""
port_num = kwargs.get("port")
self.port = str(port_num or "")
if not self.base_url:
host = kwargs.get("host")
initialize_process_props(host, port_num)
app_url = build_url(self.port, host)
self._setup_url_props(app_url)
if kwargs.get("debug", False):
self.reaper_on = False
self.build_reaper()
super(DtaleFlask, self).run(
use_reloader=kwargs.get("debug", False), *args, **kwargs
)
def test_client(self, reaper_on=False, port=None, app_root=None, *args, **kwargs):
"""
Overriding Flask's implementation of test_client so we can specify ports for testing and
whether auto-reaper should be running
:param reaper_on: whether to run auto-reaper subprocess
:type reaper_on: bool
:param port: port number of flask application
:type port: int
:param args: Optional arguments to be passed to :meth:`flask:flask.Flask.test_client`
:param kwargs: Optional keyword arguments to be passed to :meth:`flask:flask.Flask.test_client`
:return: Flask's test client
:rtype: :class:`dtale.app.DtaleFlaskTesting`
"""
self.reaper_on = reaper_on
self.app_root = app_root
if app_root is not None:
self.config["APPLICATION_ROOT"] = app_root
self.jinja_env.globals["url_for"] = self.url_for
self.test_client_class = DtaleFlaskTesting
return super(DtaleFlask, self).test_client(
*args, **dict_merge(kwargs, dict(port=port))
)
def clear_reaper(self):
"""
Restarts auto-reaper countdown
"""
if self.reaper:
self.reaper.cancel()
def build_reaper(self, timeout=REAPER_TIMEOUT):
"""
Builds D-Tale's auto-reaping process to cleanup process after an hour of inactivity
:param timeout: time in seconds before D-Tale is shutdown for inactivity, defaults to one hour
:type timeout: float
"""
if not self.reaper_on:
return
self.clear_reaper()
def _func():
logger.info("Executing shutdown due to inactivity...")
if is_up(self.base_url): # make sure the Flask process is still running
requests.get(self.shutdown_url)
sys.exit() # kill off the reaper thread
self.reaper = Timer(timeout, _func)
self.reaper.start()
def get_send_file_max_age(self, name):
"""
Overriding Flask's implementation of
get_send_file_max_age so we can lower the
timeout for javascript and css files which
are changed more often
:param name: filename
:return: Flask's default behavior for get_send_max_age if filename is not in SHORT_LIFE_PATHS
otherwise SHORT_LIFE_TIMEOUT
"""
if name and any([str(name).startswith(path) for path in SHORT_LIFE_PATHS]):
return SHORT_LIFE_TIMEOUT
return super(DtaleFlask, self).get_send_file_max_age(name)
def build_app(
url=None, reaper_on=True, app_root=None, additional_templates=None, **kwargs
):
"""
Builds :class:`flask:flask.Flask` application encapsulating endpoints for D-Tale's front-end
:param url: optional parameter which sets the host & root for any internal endpoints (ex: pinging shutdown)
:type url: str, optional
:param reaper_on: whether to run auto-reaper subprocess
:type reaper_on: bool
:param app_root: Optional path to prepend to the routes of D-Tale. This is used when making use of
Jupyterhub server proxy
:type app_root: str, optional
:param additional_templates: path(s) to any other jinja templates you would like to load. This comes into play if
you're embedding D-Tale into your own Flask app
:type: str, list, optional
:return: :class:`flask:flask.Flask` application
:rtype: :class:`dtale.app.DtaleFlask`
"""
app = DtaleFlask(
"dtale",
reaper_on=reaper_on,
static_url_path="/dtale/static",
url=url,
instance_relative_config=False,
app_root=app_root,
)
app.config["SECRET_KEY"] = "Dtale"
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
if app_root is not None:
app.config["APPLICATION_ROOT"] = app_root
app.jinja_env.globals["url_for"] = app.url_for
app.jinja_env.globals["is_app_root_defined"] = is_app_root_defined
if additional_templates:
loaders = [app.jinja_loader]
loaders += [
jinja2.FileSystemLoader(loc) for loc in make_list(additional_templates)
]
my_loader = jinja2.ChoiceLoader(loaders)
app.jinja_loader = my_loader
app.register_blueprint(dtale)
compress = Compress()
compress.init_app(app)
def _root():
return redirect("/dtale/{}".format(head_endpoint()))
@app.route("/")
def root():
return _root()
@app.route("/dtale")
def dtale_base():
"""
:class:`flask:flask.Flask` routes which redirect to dtale/main
:return: 302 - flask.redirect('/dtale/main')
"""
return _root()
@app.route("/favicon.ico")
def favicon():
"""
:class:`flask:flask.Flask` routes which returns favicon
:return: image/png
"""
return redirect(app.url_for("static", filename="images/favicon.ico"))
@app.route("/missing-js")
def missing_js():
missing_js_commands = (
">> cd [location of your local dtale repo]\n"
">> yarn install\n"
">> yarn run build # or 'yarn run watch' if you're trying to develop"
)
return render_template(
"dtale/errors/missing_js.html", missing_js_commands=missing_js_commands
)
@app.errorhandler(404)
def page_not_found(e=None):
"""
:class:`flask:flask.Flask` routes which returns favicon
:param e: exception
:return: text/html with exception information
"""
return (
render_template(
"dtale/errors/404.html",
page="",
error=e,
stacktrace=str(traceback.format_exc()),
),
404,
)
@app.errorhandler(500)
def internal_server_error(e=None):
"""
:class:`flask:flask.Flask` route which returns favicon
:param e: exception
:return: text/html with exception information
"""
return (
render_template(
"dtale/errors/500.html",
page="",
error=e,
stacktrace=str(traceback.format_exc()),
),
500,
)
def shutdown_server():
global ACTIVE_HOST, ACTIVE_PORT
"""
This function that checks if flask.request.environ['werkzeug.server.shutdown'] exists and
if so, executes that function
"""
logger.info("Executing shutdown...")
func = request.environ.get("werkzeug.server.shutdown")
if func is None:
logger.info(
"Not running with the Werkzeug Server, exiting by searching gc for BaseWSGIServer"
)
import gc
from werkzeug.serving import BaseWSGIServer
for obj in gc.get_objects():
try:
if isinstance(obj, BaseWSGIServer):
obj.shutdown()
break
except Exception as e:
logger.error(e)
else:
func()
global_state.cleanup()
ACTIVE_PORT = None
ACTIVE_HOST = None
@app.route("/shutdown")
def shutdown():
"""
:class:`flask:flask.Flask` route for initiating server shutdown
:return: text/html with server shutdown message
"""
app.clear_reaper()
shutdown_server()
return "Server shutting down..."
@app.before_request
@auth.requires_auth
def before_request():
"""
Logic executed before each :attr:`flask:flask.request`
:return: text/html with server shutdown message
"""
app.build_reaper()
@app.route("/site-map")
def site_map():
"""
:class:`flask:flask.Flask` route listing all available flask endpoints
:return: JSON of all flask enpoints [
[endpoint1, function path1],
...,
[endpointN, function pathN]
]
"""
def has_no_empty_params(rule):
defaults = rule.defaults or ()
arguments = rule.arguments or ()
return len(defaults) >= len(arguments)
links = []
for rule in app.url_map.iter_rules():
# Filter out rules we can't navigate to in a browser
# and rules that require parameters
if "GET" in rule.methods and has_no_empty_params(rule):
url = app.url_for(rule.endpoint, **(rule.defaults or {}))
links.append((url, rule.endpoint))
return jsonify(links)
@app.route("/version-info")
def version_info():
"""
:class:`flask:flask.Flask` route for retrieving version information about D-Tale
:return: text/html version information
"""
_, version = retrieve_meta_info_and_version("dtale")
return str(version)
@app.route("/health")
def health_check():
"""
:class:`flask:flask.Flask` route for checking if D-Tale is up and running
:return: text/html 'ok'
"""
return "ok"
@app.url_value_preprocessor
def handle_data_id(_endpoint, values):
if values and "data_id" in values:
# https://github.com/man-group/dtale/commit/536691d365b69a580df836e617978eb563402ac5
values["data_id"] = get_url_unquote()(
values["data_id"]
) # for handling back-slashes in arcticDB symbols
data_id_from_name = global_state.get_data_id_by_name(values["data_id"])
values["data_id"] = data_id_from_name or values["data_id"]
auth.setup_auth(app)
with app.app_context():
app = dash_views.add_dash(app)
return app
def initialize_process_props(host=None, port=None, force=False):
"""
Helper function to initalize global state corresponding to the host & port being used for your
:class:`flask:flask.Flask` process
:param host: hostname to use otherwise it will default to the output of :func:`python:socket.gethostname`
:type host: str, optional
:param port: port to use otherwise default to the output of :meth:`dtale.app.find_free_port`
:type port: str, optional
:param force: boolean flag to determine whether to ignore the :meth:`dtale.app.find_free_port` function
:type force: bool
:return:
"""
global ACTIVE_HOST, ACTIVE_PORT
if force:
active_host = get_host(ACTIVE_HOST)
curr_base = build_url(ACTIVE_PORT, active_host)
final_host = get_host(host)
new_base = build_url(port, final_host)
if curr_base != new_base:
if is_up(new_base):
try:
kill(new_base) # kill the original process
except BaseException:
raise IOError(
(
"Could not kill process at {}, possibly something else is running at port {}. Please try "
"another port."
).format(new_base, port)
)
while is_up(new_base):
time.sleep(0.01)
ACTIVE_HOST = final_host
ACTIVE_PORT = port
return
if ACTIVE_HOST is None:
ACTIVE_HOST = get_host(host)
if ACTIVE_PORT is None:
ACTIVE_PORT = int(port or find_free_port())
def is_port_in_use(port):
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
try:
s.bind(("localhost", port))
return False
except BaseException:
return True
def find_free_port():
"""
Searches for free port on executing server to run the :class:`flask:flask.Flask` process. Checks ports in range
specified using environment variables:
DTALE_MIN_PORT (default: 40000)
DTALE_MAX_PORT (default: 49000)
The range limitation is required for usage in tools such as jupyterhub. Will raise an exception if an open
port cannot be found.
:return: port number
:rtype: int
"""
min_port = int(os.environ.get("DTALE_MIN_PORT") or 40000)
max_port = int(os.environ.get("DTALE_MAX_PORT") or 49000)
base = min_port
while is_port_in_use(base):
base += 1
if base > max_port:
msg = (
"D-Tale could not find an open port from {} to {}, please increase your range by altering the "
"environment variables DTALE_MIN_PORT & DTALE_MAX_PORT."
).format(min_port, max_port)
raise IOError(msg)
return base
def build_startup_url_and_app_root(app_root=None):
global ACTIVE_HOST, ACTIVE_PORT, SSL_CONTEXT, JUPYTER_SERVER_PROXY, USE_COLAB
if USE_COLAB:
colab_host = use_colab(ACTIVE_PORT)
if colab_host:
return colab_host, None
url = build_url(ACTIVE_PORT, ACTIVE_HOST, SSL_CONTEXT is not None)
final_app_root = app_root
if final_app_root is None and JUPYTER_SERVER_PROXY:
final_app_root = os.environ.get("JUPYTERHUB_SERVICE_PREFIX")
if final_app_root is None:
final_app_root = "/user/{}".format(getpass.getuser())
final_app_root = (
"{}/proxy".format(final_app_root)
if not final_app_root.endswith("/proxy")
else final_app_root
)
if final_app_root is not None:
if JUPYTER_SERVER_PROXY:
final_app_root = fix_url_path("{}/{}".format(final_app_root, ACTIVE_PORT))
return final_app_root, final_app_root
else:
return fix_url_path("{}/{}".format(url, final_app_root)), final_app_root
return url, final_app_root
def use_colab(port):
try:
from google.colab.output import eval_js
colab_host = eval_js(
'google.colab.kernel.proxyPort(%d, {"cache": false})' % port
)
return colab_host[:-1] if colab_host.endswith("/") else colab_host
except BaseException:
return None
def show(data=None, data_loader=None, name=None, context_vars=None, **options):
"""
Entry point for kicking off D-Tale :class:`flask:flask.Flask` process from python process
:param data: data which D-Tale will display
:type data: :class:`pandas:pandas.DataFrame` or :class:`pandas:pandas.Series`
or :class:`pandas:pandas.DatetimeIndex` or :class:`pandas:pandas.MultiIndex`, optional
:param host: hostname of D-Tale, defaults to 0.0.0.0
:type host: str, optional
:param port: port number of D-Tale process, defaults to any open port on server
:type port: str, optional
:param name: optional label to assign a D-Tale process
:type name: str, optional
:param debug: will turn on :class:`flask:flask.Flask` debug functionality, defaults to False
:type debug: bool, optional
:param subprocess: run D-Tale as a subprocess of your current process, defaults to True
:type subprocess: bool, optional
:param data_loader: function to load your data
:type data_loader: func, optional
:param reaper_on: turn on subprocess which will terminate D-Tale after 1 hour of inactivity
:type reaper_on: bool, optional
:param open_browser: if true, this will try using the :mod:`python:webbrowser` package to automatically open
your default browser to your D-Tale process
:type open_browser: bool, optional
:param notebook: if true, this will try displaying an :class:`ipython:IPython.display.IFrame`
:type notebook: bool, optional
:param force: if true, this will force the D-Tale instance to run on the specified host/port by killing any
other process running at that location
:type force: bool, optional
:param context_vars: a dictionary of the variables that will be available for use in user-defined expressions,
such as filters
:type context_vars: dict, optional
:param ignore_duplicate: if true, this will not check if this data matches any other data previously loaded to
D-Tale
:type ignore_duplicate: bool, optional
:param app_root: Optional path to prepend to the routes of D-Tale. This is used when making use of
Jupyterhub server proxy
:type app_root: str, optional
:param allow_cell_edits: If false, this will not allow users to edit cells directly in their D-Tale grid
:type allow_cell_edits: bool, optional
:param inplace: If true, this will call `reset_index(inplace=True)` on the dataframe used as a way to save memory.
Otherwise this will create a brand new dataframe, thus doubling memory but leaving the dataframe
input unchanged.
:type inplace: bool, optional
:param drop_index: If true, this will drop any pre-existing index on the dataframe input.
:type drop_index: bool, optional
:param hide_shutdown: If true, this will hide the "Shutdown" buton from users
:type hide_shutdown: bool, optional
:param github_fork: If true, this will display a "Fork me on GitHub" ribbon in the upper right-hand corner of the
app
:type github_fork: bool, optional
:param hide_drop_rows: If true, this will hide the "Drop Rows" button from users
:type hide_drop_rows: bool, optional
:param hide_header_editor: If true, this will hide the header editor when editing cells
:type hide_header_editor: bool, optional
:param lock_header_menu: if true, this will always the display the header menu which usually only displays when you
hover over the top
:type lock_header_menu: bool, optional
:param hide_header_menu: If true, this will hide the header menu from the screen
:type hide_header_menu: bool, optional
:param hide_main_menu: If true, this will hide the main menu from the screen
:type hide_main_menu: bool, optional
:param hide_column_menus: If true, this will hide the column menus from the screen
:type hide_column_menus: bool, optional
:param column_edit_options: The options to allow on the front-end when editing a cell for the columns specified
:type column_edit_options: dict, optional
:param auto_hide_empty_columns: if True, then auto-hide any columns on the front-end that are comprised entirely of
NaN values
:type auto_hide_empty_columns: boolean, optional
:param highlight_filter: if True, then highlight rows on the frontend which will be filtered when applying a filter
rather than hiding them from the dataframe
:type highlight_filter: boolean, optional
:param enable_custom_filters: If true, this will enable users to make custom filters from the UI
:type enable_custom_filters: bool, optional
:Example:
>>> import dtale
>>> import pandas as pd
>>> df = pandas.DataFrame([dict(a=1,b=2,c=3)])
>>> dtale.show(df)
D-Tale started at: http://hostname:port
..link displayed in logging can be copied and pasted into any browser
"""
global ACTIVE_HOST, ACTIVE_PORT, SSL_CONTEXT, USE_NGROK
if name:
if global_state.get_data_id_by_name(name):
print(
"Data has already been loaded to D-Tale with the name '{}', please try another one.".format(
name
)
)
return
if any(not c.isalnum() and not c.isspace() for c in name):
print(
"'name' property cannot contain any special characters only letters, numbers or spaces."
)
return
try:
final_options = dtale_config.build_show_options(options)
logfile, log_level, verbose = map(
final_options.get, ["logfile", "log_level", "verbose"]
)
setup_logging(logfile, log_level or "info", verbose)
if USE_NGROK:
if not PY3:
raise Exception(
"In order to use ngrok you must be using Python 3 or higher!"
)
from flask_ngrok import _run_ngrok
ACTIVE_HOST = _run_ngrok()
ACTIVE_PORT = None
else:
initialize_process_props(
final_options["host"], final_options["port"], final_options["force"]
)
SSL_CONTEXT = options.get("ssl_context")
app_url = build_url(ACTIVE_PORT, ACTIVE_HOST)
startup_url, final_app_root = build_startup_url_and_app_root(
final_options["app_root"]
)
instance = startup(
startup_url,
data=data,
data_loader=data_loader,
name=name,
context_vars=context_vars,
ignore_duplicate=final_options["ignore_duplicate"],
allow_cell_edits=final_options["allow_cell_edits"],
inplace=final_options["inplace"],
drop_index=final_options["drop_index"],
precision=final_options["precision"],
show_columns=final_options["show_columns"],
hide_columns=final_options["hide_columns"],
column_formats=final_options["column_formats"],
nan_display=final_options["nan_display"],
sort=final_options["sort"],
locked=final_options["locked"],
background_mode=final_options["background_mode"],
range_highlights=final_options["range_highlights"],
vertical_headers=final_options["vertical_headers"],
is_proxy=JUPYTER_SERVER_PROXY,
app_root=final_app_root,
hide_shutdown=final_options.get("hide_shutdown"),
column_edit_options=final_options.get("column_edit_options"),
auto_hide_empty_columns=final_options.get("auto_hide_empty_columns"),
highlight_filter=final_options.get("highlight_filter"),
hide_header_editor=final_options.get("hide_header_editor"),
lock_header_menu=final_options.get("lock_header_menu"),
hide_header_menu=final_options.get("hide_header_menu"),
hide_main_menu=final_options.get("hide_main_menu"),
hide_column_menus=final_options.get("hide_column_menus"),
enable_custom_filters=final_options.get("enable_custom_filters"),
)
instance.started_with_open_browser = final_options["open_browser"]
is_active = not running_with_flask_debug() and is_up(app_url)
if is_active:
def _start():
if final_options["open_browser"]:
instance.open_browser()
else:
if USE_NGROK:
thread = Timer(1, _run_ngrok)
thread.setDaemon(True)
thread.start()
def _start():
try:
app = build_app(
app_url,
reaper_on=final_options["reaper_on"],
host=ACTIVE_HOST,
app_root=final_app_root,
)
if final_options["debug"] and not USE_NGROK:
app.jinja_env.auto_reload = True
app.config["TEMPLATES_AUTO_RELOAD"] = True
else:
logging.getLogger("werkzeug").setLevel(LOG_ERROR)
if final_options["open_browser"]:
instance.open_browser()
# hide banner message in production environments
cli = sys.modules.get("flask.cli")
if cli is not None:
cli.show_server_banner = lambda *x: None
run_kwargs = {}
if options.get("ssl_context"):
run_kwargs["ssl_context"] = options.get("ssl_context")
if USE_NGROK:
app.run(threaded=True, **run_kwargs)
else:
app.run(
host="0.0.0.0",
port=ACTIVE_PORT,
debug=final_options["debug"],
threaded=True,
**run_kwargs
)
except BaseException as ex:
logger.exception(ex)
if final_options["subprocess"]:
if is_active:
_start()
else:
_thread.start_new_thread(_start, ())
if final_options["notebook"]:
instance.notebook()
else:
# Need to use logging.info() because for some reason other libraries like arctic seem to break logging
logging.info("D-Tale started at: {}".format(app_url))
_start()
return instance
except DuplicateDataError as ex:
print(
"It looks like this data may have already been loaded to D-Tale based on shape and column names. Here is "
"URL of the data that seems to match it:\n\n{}\n\nIf you still want to load this data please use the "
"following command:\n\ndtale.show(df, ignore_duplicate=True)".format(
DtaleData(ex.data_id, build_url(ACTIVE_PORT, ACTIVE_HOST)).main_url()
)
)
return None
def instances():
"""
Prints all urls to the current pieces of data being viewed
"""
if global_state.size() > 0:
def _instance_msgs():
for data_id in global_state.keys():
startup_url, final_app_root = build_startup_url_and_app_root()
instance = DtaleData(
data_id,
startup_url,
is_proxy=JUPYTER_SERVER_PROXY,
app_root=final_app_root,
)
name = global_state.get_name(data_id)
yield [data_id, name or "", instance.build_main_url()]
if name is not None:
yield [
global_state.convert_name_to_url_path(name),
name,
instance.build_main_url(
global_state.convert_name_to_url_path(name)
),
]
data = pd.DataFrame(
list(_instance_msgs()), columns=["ID", "Name", "URL"]
).to_string(index=False)
print(
(
"To gain access to an instance object simply pass the value from 'ID' to dtale.get_instance(ID)\n\n{}"
).format(data)
)
else:
print("currently no running instances...")
def get_instance(data_id):
"""
Returns a :class:`dtale.views.DtaleData` object for the data_id passed as input, will return None if the data_id
does not exist
:param data_id: integer identifier for a D-Tale process's data
:type data_id: int
:return: :class:`dtale.views.DtaleData`
"""
final_data_id = global_state.get_data_id_by_name(data_id) or data_id
if not global_state.contains(final_data_id):
return None
if data_id is not None:
startup_url, final_app_root = build_startup_url_and_app_root()
return DtaleData(
final_data_id,
startup_url,
is_proxy=JUPYTER_SERVER_PROXY,
app_root=final_app_root,
)
return None
def offline_chart(
df,
chart_type=None,
query=None,
x=None,
y=None,
z=None,
group=None,
agg=None,
window=None,
rolling_comp=None,
barmode=None,
barsort=None,
yaxis=None,
filepath=None,
title=None,
**kwargs
):
"""
Builds the HTML for a plotly chart figure to saved to a file or output to a jupyter notebook
:param df: integer string identifier for a D-Tale process's data
:type df: :class:`pandas:pandas.DataFrame`
:param chart_type: type of chart, possible options are line|bar|pie|scatter|3d_scatter|surface|heatmap
:type chart_type: str
:param query: pandas dataframe query string
:type query: str, optional
:param x: column to use for the X-Axis
:type x: str
:param y: columns to use for the Y-Axes
:type y: list of str
:param z: column to use for the Z-Axis
:type z: str, optional
:param group: column(s) to use for grouping
:type group: list of str or str, optional
:param agg: specific aggregation that can be applied to y or z axes. Possible values are: count, first, last mean,
median, min, max, std, var, mad, prod, sum. This is included in label of axis it is being applied to.
:type agg: str, optional
:param window: number of days to include in rolling aggregations
:type window: int, optional
:param rolling_comp: computation to use in rolling aggregations
:type rolling_comp: str, optional
:param barmode: mode to use for bar chart display. possible values are stack|group(default)|overlay|relative
:type barmode: str, optional
:param barsort: axis name to sort the bars in a bar chart by (default is the 'x', but other options are any of
columns names used in the 'y' parameter
:type barsort: str, optional
:param filepath: location to save HTML output
:type filepath: str, optional
:param title: Title of your chart
:type title: str, optional
:param kwargs: optional keyword arguments, here in case invalid arguments are passed to this function
:type kwargs: dict
:return: possible outcomes are:
- if run within a jupyter notebook and no 'filepath' is specified it will print the resulting HTML
within a cell in your notebook
- if 'filepath' is specified it will save the chart to the path specified
- otherwise it will return the HTML output as a string
"""
instance = startup(url=None, data=df, data_id=999, is_proxy=JUPYTER_SERVER_PROXY)
output = instance.offline_chart(
chart_type=chart_type,
query=query,
x=x,
y=y,
z=z,
group=group,
agg=agg,
window=window,
rolling_comp=rolling_comp,
barmode=barmode,
barsort=barsort,
yaxis=yaxis,
filepath=filepath,
title=title,
**kwargs
)
global_state.cleanup()
return output
Editor is loading...
Leave a Comment