├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── jupytab-large.png ├── jupytab-medium.png ├── jupytab-server ├── MANIFEST.in ├── README.md ├── docs │ ├── resources │ │ ├── AirFlights.png │ │ ├── ConfigSection.png │ │ ├── RealEstateCrime.png │ │ ├── SKLearnClassifier-Calculation.png │ │ ├── SKLearnClassifier.png │ │ ├── TableauStart.png │ │ └── component-overview.png │ └── source │ │ └── development-guide.md ├── jupytab_server │ ├── __init__.py │ ├── __version__.py │ ├── jupytab.py │ ├── jupytab_api.py │ ├── kernel_executor.py │ ├── log_pipe.py │ ├── static │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── notebook.js │ │ └── vendor │ │ │ ├── bootstrap │ │ │ ├── bootstrap-4.2.1.min.css │ │ │ └── bootstrap-4.2.1.min.js │ │ │ ├── jquery │ │ │ └── jquery-3.3.1.min.js │ │ │ ├── popper │ │ │ └── popper-1.14.6.min.js │ │ │ └── tableau │ │ │ └── tableauwdc-2.3.latest.js │ └── structures.py ├── requirements-dev.txt ├── requirements.txt ├── samples │ ├── air-flights │ │ ├── AirFlights.ipynb │ │ └── AirFlights.twbx │ ├── config.ini │ ├── real-estate_crime │ │ ├── RealEstateCrime.ipynb │ │ ├── RealEstateCrime.twbx │ │ ├── sacramento_crime.csv │ │ └── sacramento_realestate.csv │ └── sklearn-classifier │ │ ├── SKLearnClassifier.twbx │ │ └── sklearn-classifier.ipynb ├── setup.cfg ├── setup.py └── tests │ ├── __init__.py │ ├── config │ ├── example.cert │ ├── example.key │ ├── ssl_bad_file.ini │ ├── ssl_disabled.ini │ ├── ssl_none.ini │ └── ssl_ok.ini │ ├── test_config.py │ └── test_nb_run.py ├── jupytab-small.png ├── jupytab.pdn └── jupytab ├── MANIFEST.in ├── README.md ├── jupytab ├── __init__.py ├── __version__.py ├── dataframe_table.py ├── function.py └── table.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── resources ├── sacramento_crime.csv └── sacramento_realestate.csv ├── test_dataframe.py ├── test_function.py └── test_util.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | main: 5 | jobs: 6 | - build: 7 | python_version: '3.6' 8 | - build: 9 | python_version: '3.7' 10 | - build: 11 | python_version: '3.8' 12 | 13 | jobs: 14 | build: 15 | parameters: 16 | python_version: 17 | type: string 18 | working_directory: ~/circleci-jupytab-python-conda 19 | docker: 20 | - image: continuumio/miniconda3 21 | environment: 22 | BASH_ENV: ~/.bashrc 23 | steps: 24 | - checkout # checkout source code to working directory 25 | - run: 26 | name: Install conda jupytab environment 27 | command: | 28 | conda create -y -n jupytab-conda python=<< parameters.python_version >> make 29 | conda init bash 30 | - run: 31 | name: Install required dependencies and run tests 32 | no_output_timeout: 3m 33 | command: | 34 | conda activate jupytab-conda 35 | make conda-develop 36 | make samples-kernel 37 | make test 38 | - run: 39 | name: Static Analysis 40 | no_output_timeout: 3m 41 | command: | 42 | conda activate jupytab-conda 43 | make flake8 44 | - store_test_results: 45 | path: _build/tests 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | test_*.sh 3 | build.sh 4 | deploy.sh 5 | .pypirc 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | _build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # Pycharm or Eclipse 76 | .idea 77 | .project 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | /jupytab/VERSION 116 | /jupytab-server/VERSION 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.11 - (2020-09-11) 4 | * #45 Add jupytab.__version__ and jupytab_server.__version__ to diagnose version easily + display at start 5 | * #44 Remove tests packages from released artifact 6 | * #43 Fix TabPy protocol that changes with version 2020.1+ of Tableau 7 | 8 | ## 0.9.10 - (2020-08-26) 9 | * #18 Use pagination to transfer data, in order to allow huge dataframe (>10M rows) 10 | * #41 Tableau stuck on Creating Extract (Loading data...) for slow retrieved tables 11 | 12 | ## 0.9.9 - (2020-06-26) 13 | * Internal version for conda packaging (no code change) 14 | 15 | ## 0.9.8 (2020-06-26) 16 | * #38 Rework version integration for packaging purpose 17 | 18 | ## 0.9.7 (2020-05-28) 19 | * #14 TabPy / External Service compatibility to add on the fly computation 20 | 21 | ## 0.9.5 (2020-03-24) 22 | * #24 Quick-fix for SSL support 23 | 24 | ## 0.9.4 (2020-03-23) 25 | * #24 Add support for SSL 26 | 27 | ## 0.9.3 (2020-02-25) 28 | * Fix incorrect release on pypi following jupytab split 29 | 30 | ## 0.9.2 (2020-02-24) 31 | * #19 Create a tool library for jupytab, allowing separated download for jupytab util 32 | * #7 Jupytab must be install in kernel as no-deps 33 | * #16 notebook.js use parent path instead of same level path 34 | 35 | ## 0.9.1 (2019-12-23) 36 | * #3 Multiindex column dataframe fails in Tableau 37 | * #8 Jupytab not working on windows 38 | * #10 Improve Table column name management 39 | * #11 Add option to export also index from dataframe 40 | 41 | ## 0.9.0 (2019-06-30) 42 | * Jupytab public release 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Capital Fund Management 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean_pyc test develop conda-develop flake8 demo 2 | 3 | BUILD=_build 4 | 5 | clean: 6 | python jupytab-server/setup.py clean 7 | python jupytab/setup.py clean 8 | 9 | clean_pyc: 10 | find . -name '*.pyc' -deletemake test 11 | 12 | conda-develop_jupytab-server: 13 | conda install -y --file jupytab-server/requirements-dev.txt 14 | conda install -y --file jupytab-server/requirements.txt 15 | (cd jupytab-server && python setup.py develop --no-deps) 16 | 17 | conda-develop_jupytab: 18 | conda install -y --file jupytab/requirements-dev.txt 19 | conda install -y --file jupytab/requirements.txt 20 | (cd jupytab && python setup.py develop --no-deps) 21 | 22 | conda-develop: 23 | $(MAKE) conda-develop_jupytab 24 | $(MAKE) conda-develop_jupytab-server 25 | 26 | samples-kernel: 27 | ipython kernel install --name jupytab-demo --user 28 | 29 | test_jupytab-server: 30 | pytest ./jupytab-server/tests --junitxml=_build/tests/results.xml 31 | 32 | test_jupytab: 33 | pytest ./jupytab/tests --junitxml=_build/tests/results.xml 34 | 35 | test: 36 | $(MAKE) test_jupytab 37 | python -m ipykernel install --user --name jupytab-demo 38 | $(MAKE) test_jupytab-server 39 | 40 | flake8_jupytab-server: 41 | (cd jupytab-server && flake8) 42 | 43 | flake8_jupytab: 44 | (cd jupytab && flake8) 45 | 46 | flake8: 47 | $(MAKE) flake8_jupytab 48 | $(MAKE) flake8_jupytab-server 49 | 50 | demo: 51 | python -m ipykernel install --user --name jupytab-demo 52 | python -m jupytab --config ./tests/data/config.ini 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupytab 2 | 3 | [![CircleCI](https://circleci.com/gh/CFMTech/Jupytab.svg?style=svg)](https://circleci.com/gh/CFMTech/Jupytab) 4 |  [![PyPI](https://badge.fury.io/py/jupytab.svg)](https://badge.fury.io/py/jupytab) 5 |   [![Anaconda-Server Badge](https://anaconda.org/conda-forge/jupytab/badges/version.svg)](https://anaconda.org/conda-forge/jupytab) 6 |  [![Anaconda-Server Badge](https://anaconda.org/conda-forge/jupytab/badges/platforms.svg)](https://anaconda.org/conda-forge/jupytab) 7 |  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | Jupytab allows you to **explore in [Tableau](https://www.tableau.com/) data which is generated dynamically by a Jupyter Notebook**. You can thus create Tableau data sources in a very flexible way using all the power of Python. This is achieved by having Tableau access data through a **web server created by Jupytab**. 10 | 11 | **New** : Jupytab 0.9.7 now implements the [TabPy](https://github.com/tableau/TabPy) protocol, you can create your datasource and compute data on the fly from your notebook functions ! 12 | 13 | Jupytab is built on **solid foundations**: Tableau's [Web Data Connector](https://tableau.github.io/webdataconnector/) and the [Jupyter Kernel Gateway](https://github.com/jupyter/kernel_gateway). 14 | 15 | ![Jupytab Logo](jupytab-medium.png) 16 | 17 | ## Overview 18 | 19 | Features: 20 | 21 | * **Expose multiple pandas dataframes** to Tableau from a Jupyter notebook 22 | * Access **several notebooks** from Tableau through a **single entry point** (web server) 23 | * Manage your notebooks using a **web interface** 24 | * **Secure access** to your data 25 | * **Compute data on the fly** using the [TabPy](https://github.com/tableau/TabPy) protocol 26 | 27 | ## Articles 28 | 29 | * **[Interactive simulation with Tableau and Jupytab](https://btribonde.medium.com/interactive-simulation-with-tableau-and-jupytab-c26adb1be564)** published on [Toward Datascience](https://towardsdatascience.com) 30 | * **[Optimise an equity portfolio in Tableau](https://devpost.com/software/portfolio-optimisation)** by [Anya Prosvetova](https://devpost.com/anyalitica). Submitted to [DataDev Hackathon 2021](https://datadev-hackathon.devpost.com) 31 | 32 | ## Examples 33 | 34 | You can find the example Jupyter notebooks and Tableau workbooks below are available in the [samples](samples) folder of 35 | the Jupytab project. 36 | 37 | ### Preparation 38 | 39 | If you want to run the example notebooks, it is necessary to define the Jupyter kernel that they run with: 40 | ``` 41 | python -m ipykernel install --user --name jupytab-demo 42 | ``` 43 | You can then launch the Jupytab server as instructed below. 44 | 45 | ### Air Flights 46 | 47 | The first example illustrates how Jupytab allows you to **directly display realtime data** in Tableau (without going through the hassle of creating intermediate files or database tables). 48 | We will display the position and altitude of all planes from the freely available [OpenSky](https://opensky-network.org/) service. (_This service does not show planes currently flying over the 49 | ocean or uninhabited area!_) 50 | 51 | The [AirFlights notebook](jupytab-server/samples/air-flights/AirFlights.ipynb) uses the [Requests](https://2.python-requests.org/en/master/) library to **access the OpenSky HTTP Rest API** and then exposes multiple metrics in a dataframe. 52 | The provided [Tableau workbook](jupytab-server/samples/air-flights/AirFlights.twbx) gives the result below: 53 | 54 | ![AirFlights](jupytab-server/docs/resources/AirFlights.png) 55 | 56 | ### Real Estate Price, and Crime 57 | 58 | The second example illustrates how simple it is to use Jupytab and **create a custom data source from multiple CSV files**. This is particularly convenient, because there is **no need to configure a new storage area** for these files in Tableau: the data is accessed through Jupytab's web service. 59 | 60 | The [example notebook](jupytab-server/samples/real-estate_crime/RealEstateCrime.ipynb) exposes real estate and crime data for Sacramento, with a bit of [Pandas](http://pandas.pydata.org/) magic to combine several data sources. 61 | 62 | Thanks to the combination of data in a single dataframe, the [Tableau workbook](jupytab-server/samples/air-flights/AirFlights.twbx) can automatically show **maps over the same area of the city**: 63 | 64 | ![RealEstateCrime](jupytab-server/docs/resources/RealEstateCrime.png) 65 | 66 | ### SkLearn Iris Predictor 67 | 68 | The third example illustrate how you can use Jupytab to create your datasource and interact in real-time with your datas. This an ideal companion for your machine learning projects, as it allows you to keep all your python code in the notebook while offering the ability for Tableau users to freely interact with your datas and understand the impact of parameters change. 69 | 70 | The [Iris Predictor notebook](jupytab-server/samples/sklearn-classifier/sklearn-classifier.ipynb) shows how you can combine data and code to create a all-in-one Tableau data source. 71 | 72 | ![SKLearnClassifier](jupytab-server/docs/resources/SKLearnClassifier.png) 73 | 74 | The python code is now only in your notebook ! The Tableau calculation is straightforward and do not rely on Python code. 75 | 76 | ![SKLearnClassifier-Calculation](jupytab-server/docs/resources/SKLearnClassifier-Calculation.png) 77 | 78 | # Installation 79 | 80 | ## Requirements 81 | 82 | Python 3.6+ is currently required to run the Jupytab server. 83 | 84 | The notebook code itself requires Python 3.6+ too (but it shouldn't be difficult to adapt Jupytab for Python 2). 85 | 86 | Jupytab server relies on the official [Jupyter Kernel Gateway](https://github.com/jupyter/kernel_gateway). 87 | 88 | ## Automatic installation 89 | 90 | The Jupytab server and its notebook library must both be installed. 91 | 92 | Jupytab server and its dependencies can easily be installed through pip: 93 | 94 | ``` 95 | pip install jupytab-server 96 | ``` 97 | 98 | For notebook kernels, you must install the jupytab library that only have a dependency on Pandas. 99 | 100 | ``` 101 | pip install jupytab 102 | ``` 103 | 104 | # Usage 105 | 106 | ## Configuration file 107 | 108 | You need to create a `config.ini` file in order to tell Jupytab which notebooks contain the tables that should be published for Tableau (this configuration file can be stored anywhere you choose). Here is an example of a working configuration file: 109 | 110 | ``` 111 | [main] 112 | listen_port = 8765 113 | security_token = myToken 114 | notebooks = AirFlights|RealEstateCrime 115 | ssl_enabled = True 116 | ssl_key = /etc/pki/tls/certs/file.crt 117 | ssl_cert = /etc/pki/tls/private/file.key 118 | 119 | [AirFlights] 120 | name = Air Flights 121 | directory = samples/air-flights 122 | path = ./AirFlights.ipynb 123 | description = Realtime Flights Visualisation (API) 124 | 125 | [RealEstateCrime] 126 | name = RealEstateCrime 127 | directory = samples/real-estate_crime 128 | path = ./RealEstateCrime.ipynb 129 | description = Real Estate Crime (static CSV) 130 | ``` 131 | 132 | There is only one mandatory section, `main`, which contains: 133 | 134 | * `listen_port` (mandatory): Numeric port number (it must be available). 135 | * `notebooks` (mandatory): List of notebooks to be executed by Jupytab, provided as a section name in the config file 136 | and separated by the `|` (pipe) symbol. This must be a simple name compliant with [configparser](https://docs.python.org/3/library/configparser.html) sections. 137 | * `security_token` (optional): If provided, an encrypted security token will be required for all exchanges with 138 | Jupytab. 139 | * `ssl_enabled` (optional): Enable or disable SSL 140 | * `ssl_key` (mandatory if ssl_enabled is true): The path name of the server private key file 141 | * `ssl_cert` (mandatory if ssl_enabled is true): The path name of the server public key certificate file 142 | 143 | Additional sections contain information about each notebook to be run: 144 | 145 | * `name` (optional): If provided, replaces the section name by a more friendly notebook name in the Jupytab web interface. 146 | * `directory` (optional): If provided, the notebook will start with `directory` as its working directory instead of the one where the `jupytab` commands is launched (see below). 147 | * `path` (mandatory): Relative (compared to `directory`) or absolute path to your notebook. 148 | * `description` (optional): If provided, adds a description to your notebook in the Jupytab web interface. 149 | 150 | Please make sure that the notebook name in the main section is exactly the same as in the section title! 151 | 152 | ![ConfigSection](jupytab-server/docs/resources/ConfigSection.png) 153 | 154 | ## Notebook preparation 155 | 156 | Publishing dataframes from a notebook is simple. Let's start by importing the necessary module: 157 | 158 | ```python 159 | import pandas as pd 160 | 161 | import jupytab 162 | ``` 163 | 164 | ### Tables definition 165 | 166 | The publication of data sources for Tableau from a notebook is done through two classes: 167 | 168 | * Tables: Contains the publication-ready tables provided by the notebook. There is typically a single instance of this class in a given notebook. 169 | * DataFrameTable: Table for either static or dynamic publication in Tableau. Static tables never change on the Tableau side. Dynamic tables are regenerated for each Tableau Extract. 170 | 171 | ```python 172 | def dynamic_df(): 173 | return pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], columns=['a', 'b', 'c']) 174 | 175 | tables = jupytab.Tables() # Publication-ready tables contained by this notebook 176 | 177 | # Example 1: Static data: it will never change on the Tableau side: 178 | static_df = dynamic_df() 179 | tables['static'] = jupytab.DataFrameTable('A static table', dataframe=static_df) 180 | 181 | # Example 2: Dynamic data: a new DataFrame is generated whenever Extract is requested on Tableau's side: 182 | tables['dynamic'] = jupytab.DataFrameTable('A dynamic table', refresh_method=dynamic_df) 183 | ``` 184 | 185 | The tables listed in the Python variables `tables` now need to be explicitly marked for publication by Jupytab (both their schema and their contents). This is typically done at the very end of the notebook, with two special cells. 186 | 187 | Please note that you can also include the index in the dataframe output using `include_index=True`. Index is not included by default. 188 | 189 | ```python 190 | # Example 3: Static data with index included 191 | static_df = dynamic_df() 192 | tables['static'] = jupytab.DataFrameTable('A static table', dataframe=static_df, include_index=True) 193 | ``` 194 | 195 | ### Functions definition 196 | 197 | Following the same principle, you can also expose your own python functions to Tableau through two classes: 198 | 199 | ```python 200 | def multiply(my_first_number, my_second_number): 201 | return my_first_number * my_second_number 202 | 203 | functions = jupytab.Functions() # Publication-ready functions contained by this notebook 204 | 205 | functions['multiplier'] = jupytab.Function('A multiplier function with two parameters', multiply) 206 | ``` 207 | 208 | The function is now available in Tableau using the following calculation: 209 | 210 | `SCRIPT_REAL("MyNotebook.multiplier", AVG([Value 1]), AVG([Value 2]))` 211 | 212 | You must refer to the notebook code you used in the config section, not the notebook name which is used only for display. 213 | 214 | ``` 215 | [main] 216 | notebooks = MyNotebook 217 | ``` 218 | 219 | ### Expose tables schema 220 | 221 | When Tableau needs to retrieve the schema of all available tables, Jupytab executes the (mandatory) cell that starts with `# GET /schema`: 222 | 223 | ```python 224 | # GET /schema 225 | tables.render_schema() 226 | ``` 227 | 228 | (`tables.render_schema()` will output a JSON string when executed in the notebook.) 229 | 230 | ### Expose tables data 231 | 232 | When Tableau needs to retrieve the data from tables, Jupytab executes the (mandatory) cell that starts with `# GET /data`: 233 | 234 | ```python 235 | # GET /data 236 | tables.render_data(REQUEST) 237 | ``` 238 | 239 | (Note that `tables.render_data(REQUEST)` will throw, as expected, `NameError: name 'REQUEST' is not defined` when executed in the notebook: `REQUEST` will only be defined when running with Jupytab, so the error is harmless.) 240 | 241 | ### Expose functions data 242 | 243 | When Tableau needs to execute function, Jupytab executes the (mandatory) cell that starts with `# POST /evaluate`: 244 | 245 | ```python 246 | # POST /evaluate 247 | functions.render_evaluate(REQUEST) 248 | ``` 249 | 250 | (Note that `functions.render_evaluate(REQUEST)` will throw, as expected, `NameError: name 'REQUEST' is not defined` when executed in the notebook: `REQUEST` will only be defined when running with Jupytab, so the error is harmless.) 251 | 252 | ## Launching the Jupytab server 253 | 254 | Once you have created your notebooks, it should be a matter of second before they become acessible from Tableau. 255 | To start Jupytab, simply run the following command: 256 | ``` 257 | jupytab --config=config.ini 258 | ``` 259 | You should see the following ouput, which contains two important pieces of information: 260 | 261 | * The list of published notebooks. 262 | * The URL to be used in Tableau in order to access the data (including any security token declared in the configuration file). 263 | 264 | ``` 265 | (install-jupytab) user@localhost:~$ jupytab --config=tests/config.ini 266 | Start notebook ~/tests/resources/rt_flights.ipynb on 127.0.0.1:57149 267 | Start notebook ~/tests/resources/csv_reader.ipynb on 127.0.0.1:53351 268 | Your token is 02014868fe0eef123269397c5bc65a9608b3cedb73e3b84d8d02c220 269 | Please open : http://localhost:8765/?security_token=02014868fe0eef123269397c5bc65a9608b3cedb73e3b84d8d02c220 270 | INFO:[KernelGatewayApp] Kernel started: 1befe373-aebd-4b31-9f98-2f90f235f255 271 | INFO:[KernelGatewayApp] Kernel started: 365bfdb6-887b-41b4-ad69-309a200f5137 272 | INFO:[KernelGatewayApp] Registering resource: /schema, methods: (['GET']) 273 | INFO:[KernelGatewayApp] Registering resource: /data, methods: (['GET']) 274 | INFO:[KernelGatewayApp] Registering resource: /_api/spec/swagger.json, methods: (GET) 275 | INFO:[KernelGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:53351 276 | INFO:[KernelGatewayApp] Registering resource: /schema, methods: (['GET']) 277 | INFO:[KernelGatewayApp] Registering resource: /data, methods: (['GET']) 278 | INFO:[KernelGatewayApp] Registering resource: /_api/spec/swagger.json, methods: (GET) 279 | INFO:[KernelGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:57149 280 | ``` 281 | 282 | ## Connect Tableau to your notebooks 283 | 284 | ### Web Data Connector for data sources 285 | 286 | Connecting Tableau to your notebooks is simply done by copying the URL provided by Jupytab upon startup to the Tableau Web Data Connector: 287 | 288 | ![TableauStart](jupytab-server/docs/resources/TableauStart.png) 289 | 290 | You can now use the Tableau Web Data Connector screen and access your data sources through the Jupytab interface. 291 | 292 | ### TabPy Connector to execute functions 293 | 294 | Connecting Tableau to your notebooks to execute code on the fly using the [External Connection Service](https://help.tableau.com/current/pro/desktop/en-us/r_connection_manage.htm). 295 | 296 | The address to use is the host where Jupytab is running. The port is the one you configured in the `config.ini` file. 297 | 298 | Please take care to select the **TabPy / External API** and not RServe. 299 | 300 | ## Troubleshooting 301 | 302 | If you encounter a any problem when using Jupytab, you can find it useful to check the console where you launched 303 | Jupytab for diagnostic messages. The console output can in particular be usefully included when you raise a GitHub issue. 304 | 305 | # Contact and contributing 306 | 307 | Contributions are very welcome. It can be 308 | 309 | - a new GitHub issue, 310 | - a feature request, 311 | - code (see the [Developement Guide](jupytab-server/docs/source/development-guide.md)), 312 | - or simply feedback on this project. 313 | 314 | The main author of Jupytab is Brian Tribondeau, who can be reached at brian.tribondeau@cfm.fr. 315 | 316 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.11 -------------------------------------------------------------------------------- /jupytab-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-large.png -------------------------------------------------------------------------------- /jupytab-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-medium.png -------------------------------------------------------------------------------- /jupytab-server/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include jupytab_server/static * 2 | recursive-exclude tests * -------------------------------------------------------------------------------- /jupytab-server/README.md: -------------------------------------------------------------------------------- 1 | Jupytab-server -------------------------------------------------------------------------------- /jupytab-server/docs/resources/AirFlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/AirFlights.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/ConfigSection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/ConfigSection.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/RealEstateCrime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/RealEstateCrime.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/SKLearnClassifier-Calculation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/SKLearnClassifier-Calculation.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/SKLearnClassifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/SKLearnClassifier.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/TableauStart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/TableauStart.png -------------------------------------------------------------------------------- /jupytab-server/docs/resources/component-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/docs/resources/component-overview.png -------------------------------------------------------------------------------- /jupytab-server/docs/source/development-guide.md: -------------------------------------------------------------------------------- 1 | Development Guide 2 | ================= 3 | 4 | 5 | Install environment 6 | ------------------- 7 | 8 | - `jupytab` is provided with a Makefile so as to ease environment setup. 9 | - `conda` environments are supported for development 10 | 11 | 12 | ### Setup with conda 13 | 14 | From the [jupytab package folder](../../jupytab): 15 | 16 | ```bash 17 | $ conda create -n jupytab python=3 18 | $ conda activate jupytab 19 | (jupytab) $ make conda-develop 20 | ``` 21 | 22 | ### Run demo 23 | 24 | The command `make demo` registers the `jupytab-demo` kernel and runs the examples: 25 | 26 | ```bask 27 | (jupytab) $ make demo 28 | python -m ipykernel install --user --name jupytab-demo 29 | Installed kernelspec jupytab_demo in /home/user/.local/share/jupyter/kernels/jupytab-demo 30 | python -m jupytab --config ./tests/data/config.ini 31 | Start notebook AirFlights.ipynb on 127.0.0.1:51455 32 | Start notebook RealEstateCrime.ipynb on 127.0.0.1:42963 33 | ``` 34 | 35 | Unit tests 36 | ---------- 37 | 38 | - Unit tests are provided for the pytest framework. 39 | - Test cases are located in the `tests` folder. 40 | - The `make test` command runs all tests: 41 | 42 | 43 | ```bash 44 | (jupytab) [user@localhost jupytab]$ make test 45 | pytest ./tests 46 | ==================================================== test session starts ============================ 47 | platform linux -- Python 3.6.2, pytest-3.2.3, py-1.4.33, pluggy-0.4.0 48 | rootdir: /home/user/contrib/jupytab, inifile: 49 | plugins: cov-2.5.1 50 | collected 1 item 51 | 52 | tests/test_util.py . 53 | 54 | ================================================= 1 passed in 0.32 seconds ========================== 55 | ``` 56 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/__init__.py: -------------------------------------------------------------------------------- 1 | from tornado.httpclient import AsyncHTTPClient 2 | from .__version__ import __version__ 3 | 4 | AsyncHTTPClient.configure(None, max_body_size=4000000000) 5 | 6 | __all__ = [__version__] 7 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.11" 2 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/jupytab.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import argparse 5 | import hashlib 6 | import logging 7 | import os.path 8 | import socket 9 | from configparser import ConfigParser, NoSectionError, NoOptionError 10 | 11 | from tornado.ioloop import IOLoop 12 | from tornado.web import StaticFileHandler, Application 13 | 14 | from jupytab_server import __version__ 15 | from jupytab_server.jupytab_api import InfoHandler, RestartHandler, APIHandler, EvaluateHandler, \ 16 | ReverseProxyHandler, root, api_kernel, access_kernel, restart_kernel 17 | from jupytab_server.kernel_executor import KernelExecutor 18 | from jupytab_server.structures import CaseInsensitiveDict 19 | 20 | logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) 21 | 22 | 23 | def __extract_item(config, notebook_key, item_key, default=None): 24 | try: 25 | return config.get(notebook_key, item_key) 26 | except (NoOptionError, NoSectionError): 27 | if default: 28 | return default 29 | else: 30 | raise ValueError(f'Expecting {item_key} in section {notebook_key}') 31 | 32 | 33 | def config_listen_port(config): 34 | return config.getint('main', 'listen_port') 35 | 36 | 37 | def config_security_token(config): 38 | try: 39 | return config.get('main', 'security_token') 40 | except (NoSectionError, NoOptionError): 41 | return None 42 | 43 | 44 | def config_notebooks(config): 45 | notebooks = config.get('main', 'notebooks') 46 | notebook_dict = {} 47 | 48 | for key in notebooks.split('|'): 49 | nb_path = __extract_item(config, key, 'path') 50 | nb_name = __extract_item(config, key, 'name', key) 51 | nb_description = __extract_item(config, key, 'description', "No description") 52 | nb_cwd = __extract_item(config, key, 'directory', ".") 53 | 54 | notebook_dict[key] = {'name': nb_name, 55 | 'file_path': nb_path, 56 | 'description': nb_description, 57 | 'cwd': nb_cwd} 58 | 59 | return notebook_dict 60 | 61 | 62 | def config_ssl(config): 63 | try: 64 | ssl_enabled = config.getboolean('main', 'ssl_enabled') 65 | except (NoSectionError, NoOptionError): 66 | ssl_enabled = False 67 | 68 | if ssl_enabled: 69 | ssl_cert = config.get('main', 'ssl_cert') 70 | ssl_key = config.get('main', 'ssl_key') 71 | 72 | if ssl_enabled and not os.path.isfile(ssl_cert): 73 | raise FileNotFoundError(f"SSL enabled but missing ssl_cert file: {ssl_cert}") 74 | if ssl_enabled and not os.path.isfile(ssl_key): 75 | raise FileNotFoundError(f"SSL enabled but missing ssl_key file: {ssl_key}") 76 | 77 | return { 78 | "certfile": ssl_cert, 79 | "keyfile": ssl_key 80 | } 81 | else: 82 | return None 83 | 84 | 85 | def parse_config(config_file): 86 | if not os.path.isfile(config_file): 87 | raise FileNotFoundError(f"missing configuration: {config_file}") 88 | 89 | config = ConfigParser() 90 | config.optionxform = str 91 | config.read(config_file) 92 | 93 | listen_port = config_listen_port(config) 94 | security_token = config_security_token(config) 95 | notebooks = config_notebooks(config) 96 | ssl = config_ssl(config) 97 | 98 | if not ssl: 99 | print("SSL not enabled") 100 | else: 101 | print("SSL enabled") 102 | 103 | return { 104 | 'listen_port': listen_port, 105 | 'security_token': security_token, 106 | 'notebooks': notebooks, 107 | 'ssl': ssl 108 | } 109 | 110 | 111 | def create_server_app(listen_port, security_token, notebooks, ssl): 112 | notebook_store = CaseInsensitiveDict() 113 | 114 | for key, value in notebooks.items(): 115 | notebook_store[key] = KernelExecutor(**value) 116 | 117 | for key, value in notebook_store.items(): 118 | value.start() 119 | 120 | protocol = "https" if ssl else "http" 121 | 122 | if security_token: 123 | token_digest = hashlib.sha224(security_token.encode('utf-8')).hexdigest() 124 | print(f"""Your token is {token_digest} 125 | Please open : {protocol}://{socket.gethostname()}:{listen_port}""" 126 | f"/?security_token={token_digest}") 127 | else: 128 | token_digest = None 129 | print(f"""You have no defined token. Please note your process is not secured ! 130 | Please open : {protocol}://{socket.gethostname()}:{listen_port}""") 131 | 132 | server_app = Application([ 133 | (r"/info", InfoHandler, 134 | {'notebook_store': notebook_store, 'security_token': token_digest}), 135 | (r"/evaluate", EvaluateHandler, 136 | {'notebook_store': notebook_store, 'security_token': token_digest}), 137 | (r"/" + api_kernel, APIHandler, 138 | {'notebook_store': notebook_store, 'security_token': token_digest}), 139 | (r"/" + restart_kernel + "/(.*)", RestartHandler, 140 | {'notebook_store': notebook_store, 'security_token': token_digest}), 141 | (r"/" + access_kernel + "/(.*)", ReverseProxyHandler, 142 | {'notebook_store': notebook_store, 'security_token': token_digest}), 143 | 144 | (r"/(.*)", StaticFileHandler, {'path': root, "default_filename": "index.html"}), 145 | 146 | ]) 147 | return server_app 148 | 149 | 150 | def main(input_args=None): 151 | print(f"Starting Jupytab-Server {__version__}") 152 | parser = argparse.ArgumentParser(description='The Tableau gateway to notebooks') 153 | parser.add_argument("-c", "--config", dest='config_file', default='config.ini', type=str) 154 | parser.add_argument("-e", "--env", dest='environment', default='UNKNOWN', type=str) 155 | parser.add_argument("-v", "--version", action='version', version=f"Jupytab {__version__}") 156 | args, unknown = parser.parse_known_args(args=input_args) 157 | 158 | params = parse_config(config_file=args.config_file) 159 | 160 | app = create_server_app(**params) 161 | 162 | app.listen(params['listen_port'], ssl_options=params['ssl']) 163 | 164 | IOLoop.instance().start() 165 | 166 | 167 | if __name__ == "__main__": 168 | main() 169 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/jupytab_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import json 5 | import os 6 | import traceback 7 | from typing import Any 8 | from urllib.parse import urlunparse, urlencode 9 | 10 | from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPClientError 11 | from tornado.web import RequestHandler, HTTPError 12 | from jupytab_server import __version__ 13 | import time 14 | 15 | application_start_time = time.time() 16 | root = os.path.dirname(__file__) + '/static' 17 | api_kernel = 'api' 18 | access_kernel = 'kernel' 19 | restart_kernel = 'api/restart' 20 | uri_security_token = 'security_token' 21 | evaluate_method = "evaluate" 22 | 23 | 24 | def transform_url(protocol, host, path, query=''): 25 | new_path = urlunparse((protocol, host, path, '', query, '')) 26 | return new_path 27 | 28 | 29 | class BaseRequestHandler(RequestHandler): 30 | 31 | def initialize(self, notebook_store, security_token): 32 | self.notebook_store = notebook_store 33 | self.security_token = security_token 34 | 35 | def set_default_headers(self) -> None: 36 | self.set_header(name="Content-Type", value="application/json; charset=UTF-8") 37 | 38 | def write_error(self, status_code: int, **kwargs: Any) -> None: 39 | body = { 40 | 'method': self.request.method, 41 | 'uri': self.request.path, 42 | 'code': status_code, 43 | 'message': self._reason 44 | } 45 | if "exc_info" in kwargs: 46 | if self.settings.get("serve_traceback"): 47 | # in debug mode, send a traceback 48 | trace = '\n'.join(traceback.format_exception( 49 | *kwargs['exc_info'] 50 | )) 51 | body['trace'] = trace 52 | if isinstance(kwargs['exc_info'][1], HTTPError): 53 | body['message'] = kwargs['exc_info'][1].log_message 54 | self.finish(body) 55 | 56 | def on_chunk(self, chunk): 57 | self.write(chunk) 58 | self.flush() 59 | 60 | 61 | class InfoHandler(BaseRequestHandler): 62 | def get(self, *args, **kwargs): 63 | self.write( 64 | { 65 | "description": "Jupytab Server, an open source project " 66 | "available at https://github.com/CFMTech/Jupytab", 67 | "creation_time": application_start_time, 68 | "state_path": os.getcwd(), 69 | "server_version": __version__, 70 | "name": "Jupytab Server", 71 | "versions": { 72 | "v1": { 73 | "features": {} 74 | } 75 | } 76 | } 77 | ) 78 | 79 | 80 | class EvaluateHandler(BaseRequestHandler): 81 | async def post(self, *args, **kwargs): 82 | query = json.loads(self.request.body) 83 | 84 | # api_key = query['api_key'] 85 | script = query['script'] 86 | data = query['data'] 87 | 88 | if script == 'return _arg1' and data['_arg1'] == 1: 89 | # Query sent by Tableau to test connection ... 90 | # b'{"api_key":"","script":"return _arg1","data":{"_arg1":1}}' 91 | self.write(json.dumps(1)) 92 | else: 93 | script_params = query['script'].split('.') 94 | if len(script_params) != 2: 95 | raise HTTPError(log_message=f" expected instead of <{script}>") 96 | 97 | notebook_target, method_target = script_params 98 | 99 | if notebook_target not in self.notebook_store: 100 | raise HTTPError(log_message=f"Unknown notebook ({notebook_target})") 101 | 102 | kernel = self.notebook_store[notebook_target] 103 | host = f'{kernel.host}:{kernel.port}' 104 | my_url = transform_url('http', host, evaluate_method) 105 | 106 | body = { 107 | 'function': method_target.lower(), 108 | 'data': data 109 | } 110 | json_body = json.dumps(body) 111 | 112 | request = HTTPRequest( 113 | url=my_url, 114 | method='POST', 115 | headers=self.request.headers, 116 | follow_redirects=True, 117 | body=json_body, 118 | request_timeout=3600.0, 119 | streaming_callback=self.on_chunk) 120 | 121 | response = await AsyncHTTPClient().fetch(request) 122 | 123 | self.set_status(response.code) 124 | self.finish() 125 | 126 | 127 | class RestartHandler(BaseRequestHandler): 128 | def get(self, *args, **kwargs): 129 | if self.security_token and self.get_argument(uri_security_token) != self.security_token: 130 | raise ConnectionRefusedError("Invalid security token") 131 | 132 | kernel_id = self.request.path[len(restart_kernel) + 2:] 133 | 134 | notebook_kernel = self.notebook_store[kernel_id] 135 | 136 | notebook_kernel.restart() 137 | 138 | 139 | class APIHandler(BaseRequestHandler): 140 | def get(self, *args, **kwargs): 141 | if self.security_token and self.get_argument(uri_security_token) != self.security_token: 142 | raise ConnectionRefusedError("Invalid security token") 143 | 144 | notebook_dict = {} 145 | 146 | for key, value in self.notebook_store.items(): 147 | notebook_dict[key] = \ 148 | { 149 | "kernel_id": key, 150 | "name": value.name, 151 | "status": value.kernel_status, 152 | "path": str(value.notebook_file), 153 | "host": value.host, 154 | "port": value.port, 155 | "description": value.description 156 | } 157 | 158 | self.write(notebook_dict) 159 | 160 | 161 | class ReverseProxyHandler(BaseRequestHandler): 162 | 163 | async def get(self, *args, **kwargs): 164 | if self.security_token and self.get_argument(uri_security_token) != self.security_token: 165 | raise ConnectionRefusedError("Invalid security token") 166 | 167 | notebook_path = self.request.path[len(access_kernel) + 2:].split('/', 2) 168 | 169 | notebook_id = notebook_path[0] 170 | notebook_uri = notebook_path[1] if len(notebook_path) > 1 else '' 171 | kernel = self.notebook_store[notebook_id] 172 | 173 | host = f'{kernel.host}:{kernel.port}' 174 | 175 | query_arguments = self.request.query_arguments.copy() 176 | query_arguments.pop(uri_security_token, None) 177 | 178 | my_url = transform_url( 179 | 'http', 180 | host, 181 | notebook_uri, 182 | urlencode(query_arguments, doseq=True)) 183 | 184 | request = HTTPRequest( 185 | url=my_url, 186 | method='GET', 187 | headers=self.request.headers, 188 | follow_redirects=True, 189 | body=None, 190 | request_timeout=3600.0, 191 | streaming_callback=self.on_chunk) 192 | 193 | try: 194 | response = await AsyncHTTPClient().fetch(request) 195 | self.set_status(response.code) 196 | self.finish() 197 | except HTTPClientError as e: 198 | print(f"Error raised: Please visit {e.response.effective_url} from jupytab-server " 199 | f"machine to get more information about notebook execution error") 200 | raise e 201 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/kernel_executor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import logging 5 | import os 6 | import socket 7 | import subprocess 8 | import sys 9 | 10 | from . import log_pipe 11 | 12 | 13 | class KernelCallback: 14 | def on_kernel_start(self, kernel_executor): 15 | pass 16 | 17 | def on_kernel_stop(self, kernel_executor): 18 | pass 19 | 20 | 21 | class KernelExecutor: 22 | def __init__(self, name, file_path, description=None, kernel_callback=None, cwd=None): 23 | self.__notebook_name = name 24 | self.__notebook_file = file_path 25 | self.__notebook_description = description 26 | self.__kernel_callback = kernel_callback 27 | self.__kernel_process = None 28 | self.__host = '127.0.0.1' 29 | self.__port = None 30 | self.__cwd = cwd 31 | 32 | @staticmethod 33 | def get_free_tcp_port(): 34 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 35 | tcp.bind(('', 0)) 36 | host, port = tcp.getsockname() 37 | tcp.close() 38 | 39 | return port 40 | 41 | def start(self): 42 | my_env = dict(os.environ) 43 | my_env['PATH'] = os.path.dirname(sys.executable) + os.pathsep + os.environ.get('PATH') 44 | 45 | self.__port = KernelExecutor.get_free_tcp_port() 46 | 47 | print(f'Start notebook {self.notebook_file} on {self.host}:{self.port}') 48 | 49 | logpipe = log_pipe.LogPipe(logging.INFO) 50 | 51 | command_line = [ 52 | u"jupyter", 53 | u"kernelgateway", 54 | u"--KernelGatewayApp.api='kernel_gateway.notebook_http'", 55 | u"--KernelGatewayApp.seed_uri={filename}".format(filename=self.notebook_file), 56 | u"--KernelGatewayApp.ip='{host}'".format(host=self.host), 57 | u"--KernelGatewayApp.port={port}".format(port=self.port) 58 | ] 59 | 60 | self.__kernel_process = subprocess.Popen(command_line, 61 | env=my_env, 62 | stdout=logpipe, 63 | stderr=logpipe, 64 | cwd=self.cwd) 65 | 66 | if self.__kernel_callback: 67 | self.__kernel_callback.on_kernel_start(self) 68 | 69 | return f'{self.host}:{self.port}' 70 | 71 | def stop(self): 72 | print(f'Stop notebook {self.notebook_file} on {self.host}:{self.port}') 73 | 74 | self.__kernel_process.kill() 75 | self.__kernel_process = None 76 | 77 | if self.__kernel_callback: 78 | self.__kernel_callback.on_kernel_stop(self) 79 | 80 | def restart(self): 81 | print(f'Restart notebook {self.notebook_file} on {self.host}:{self.port}') 82 | 83 | self.stop() 84 | self.start() 85 | 86 | @property 87 | def notebook_file(self): 88 | return self.__notebook_file 89 | 90 | @property 91 | def kernel_process(self): 92 | return self.__kernel_process 93 | 94 | @property 95 | def name(self): 96 | return self.__notebook_name 97 | 98 | @property 99 | def host(self): 100 | return self.__host 101 | 102 | @property 103 | def port(self): 104 | return self.__port 105 | 106 | @property 107 | def description(self): 108 | return self.__notebook_description 109 | 110 | @property 111 | def cwd(self): 112 | return self.__cwd 113 | 114 | @property 115 | def kernel_status(self): 116 | if not self.__kernel_process: 117 | return "UNKNOWN" 118 | elif not self.__kernel_process.returncode: 119 | return "RUNNING" 120 | else: 121 | return f"STOPPED ({self.__kernel_process.returncode})" 122 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/log_pipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import logging 5 | import os 6 | import threading 7 | 8 | 9 | class LogPipe(threading.Thread): 10 | 11 | def __init__(self, level): 12 | """Setup the object with a logger and a loglevel 13 | and start the thread 14 | """ 15 | threading.Thread.__init__(self) 16 | self.daemon = True 17 | self.level = level 18 | self.fdRead, self.fdWrite = os.pipe() 19 | self.pipeReader = os.fdopen(self.fdRead) 20 | self.start() 21 | 22 | def fileno(self): 23 | """Return the write file descriptor of the pipe 24 | """ 25 | return self.fdWrite 26 | 27 | def run(self): 28 | """Run the thread, logging everything. 29 | """ 30 | for line in iter(self.pipeReader.readline, ''): 31 | logging.log(self.level, line.strip('\n')) 32 | 33 | self.pipeReader.close() 34 | 35 | def close(self): 36 | """Close the write end of the pipe. 37 | """ 38 | os.close(self.fdWrite) 39 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/jupytab_server/static/favicon.ico -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/static/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | Notebook to Tableau connector 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

Notebook to Tableau connector

29 |

Open this page in Tableau as a Web Data Connector to start exploring your data!

30 |
31 |

Please select your notebook:

32 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/static/notebook.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Capital Fund Management 2 | // SPDX-License-Identifier: MIT 3 | 4 | $.urlParam = function (name, default_val) { 5 | var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); 6 | if (results == null) { 7 | return default_val; 8 | } 9 | return decodeURI(results[1]) || 0; 10 | } 11 | 12 | function display_notebook_info(kernel_info) { 13 | $('#notebook-card-info').css("visibility", "visible"); 14 | $('#notebook-info-title').html(kernel_info.name + " (" + kernel_info.kernel_id + ")") 15 | $('#notebook-info-description').html(kernel_info.description) 16 | $('#notebook-info-hostname').html(kernel_info.host) 17 | $('#notebook-info-port').html(kernel_info.port) 18 | $('#notebook-info-file').html(kernel_info.path) 19 | $('#notebook-info-status').html(kernel_info.status) 20 | } 21 | 22 | function load_notebook_list(token) { 23 | $.getJSON({ 24 | url: "./api?" + token 25 | }).done(function (data) { 26 | $('#notebook-list').empty(); 27 | Object.keys(data).forEach(function (key) { 28 | $added_item = $('').appendTo('#notebook-list') 30 | 31 | $added_item.click(function (e) { 32 | e.preventDefault() 33 | $(this).parent().find('button').removeClass('active'); 34 | $(this).addClass('active'); 35 | 36 | display_notebook_info(data[key]) 37 | define_button_handler(key, token) 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | function explore_in_tableau(active_kernel_id, token) { 44 | var tableObj = { 45 | active_kernel_id: active_kernel_id, 46 | token: token, 47 | }; 48 | 49 | tableau.connectionData = JSON.stringify(tableObj); 50 | tableau.connectionName = active_kernel_id; // This will be the data source name in Tableau 51 | tableau.submit(); // This sends the connector object to Tableau 52 | }; 53 | 54 | function define_button_handler(active_kernel_id, token) { 55 | $('#restartButton').off('click').click(function () { 56 | $.getJSON({ 57 | url: "./api/restart/" + active_kernel_id + "?" + token 58 | }).done(function () { 59 | load_notebook_list(token); 60 | $('#notebook-card-info').css("visibility", "hidden"); 61 | }) 62 | }); 63 | $('#submitButton').off('click').click(function () { 64 | explore_in_tableau(active_kernel_id, token) 65 | }); 66 | 67 | $('#schemaButton').off('click').click(function () { 68 | window.location.href = "./kernel/" + active_kernel_id + "/schema?" + token 69 | }); 70 | } 71 | 72 | $(function () { 73 | security_token = $.urlParam("security_token", "") 74 | 75 | load_notebook_list("security_token=" + security_token); 76 | 77 | // Create the connector object 78 | var myConnector = tableau.makeConnector(); 79 | 80 | // Define the schema 81 | myConnector.getSchema = function (schemaCallback) { 82 | var tableObj = JSON.parse(tableau.connectionData) 83 | 84 | $.getJSON("./kernel/" + tableObj.active_kernel_id + "/schema?" + tableObj.token, function (resp) { 85 | schemaCallback(resp); 86 | }); 87 | }; 88 | 89 | // Download the data 90 | myConnector.getData = function (table, doneCallback) { 91 | let tableObj = JSON.parse(tableau.connectionData) 92 | 93 | let batchSize = 10000 94 | 95 | function load_data(dataChunkIdx) { 96 | let fromIdx = (dataChunkIdx * batchSize) 97 | let toIdx = ((dataChunkIdx + 1) * batchSize) 98 | 99 | $.getJSON("./kernel/" + tableObj.active_kernel_id + "/data?table_name=" + table.tableInfo.id + "&" + tableObj.token 100 | + "&from=" + fromIdx + "&to=" + toIdx + "&refresh=" + (dataChunkIdx == 0 ? "true" : "false")+ "&format=json", function (resp) { 101 | if(resp.length == 0) { 102 | doneCallback(); 103 | } else { 104 | tableau.reportProgress("Getting rows " + fromIdx + " to " + (toIdx - 1)); 105 | let tableData = []; 106 | for (let i = 0; i < resp.length; i++) { 107 | let row = {} 108 | for (let key in resp[i]) { 109 | row[key] = resp[i][key] 110 | } 111 | tableData.push(row); 112 | } 113 | 114 | table.appendRows(tableData); 115 | load_data(dataChunkIdx+1) 116 | } 117 | }); 118 | } 119 | 120 | load_data(0); 121 | }; 122 | 123 | tableau.registerConnector(myConnector); 124 | }); -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/static/vendor/bootstrap/bootstrap-4.2.1.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.2.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("popper.js"),require("jquery")):"function"==typeof define&&define.amd?define(["exports","popper.js","jquery"],e):e(t.bootstrap={},t.Popper,t.jQuery)}(this,function(t,u,g){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},De="show",we="out",Ae={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:"show"+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Ne="fade",Oe="show",ke=".tooltip-inner",Pe=".arrow",Le="hover",je="focus",He="click",Re="manual",Ue=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Oe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(Ne);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Pe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(g(this.tip).hasClass(Ne)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[He]=!1,this._activeTrigger[je]=!1,this._activeTrigger[Le]=!1,g(this.tip).hasClass(Ne)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ce+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(ke)),this.getTitle()),g(t).removeClass(Ne+" "+Oe)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return be[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Re){var e=t===Le?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Le?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?je:Le]=!0),g(e.getTipElement()).hasClass(Oe)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?je:Le]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,g(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(pe,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Te);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(Ne),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(ve),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(ve,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.2.1"}},{key:"Default",get:function(){return Ie}},{key:"NAME",get:function(){return pe}},{key:"DATA_KEY",get:function(){return ve}},{key:"Event",get:function(){return Ae}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}();g.fn[pe]=Ue._jQueryInterface,g.fn[pe].Constructor=Ue,g.fn[pe].noConflict=function(){return g.fn[pe]=ye,Ue._jQueryInterface};var We="popover",xe="bs.popover",Fe="."+xe,qe=g.fn[We],Me="bs-popover",Ke=new RegExp("(^|\\s)"+Me+"\\S+","g"),Qe=l({},Ue.Default,{placement:"right",trigger:"click",content:"",template:''}),Be=l({},Ue.DefaultType,{content:"(string|element|function)"}),Ve="fade",Ye="show",Xe=".popover-header",ze=".popover-body",Ge={HIDE:"hide"+Fe,HIDDEN:"hidden"+Fe,SHOW:"show"+Fe,SHOWN:"shown"+Fe,INSERTED:"inserted"+Fe,CLICK:"click"+Fe,FOCUSIN:"focusin"+Fe,FOCUSOUT:"focusout"+Fe,MOUSEENTER:"mouseenter"+Fe,MOUSELEAVE:"mouseleave"+Fe},Je=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Me+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(Xe),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ze),e),t.removeClass(Ve+" "+Ye)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ke);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=Q(f[o],a[e]-('right'===e?f.width:f.height))),le({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=fe({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!K(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,w=t(e.instance.popper),y=parseFloat(w['margin'+f],10),E=parseFloat(w['border'+f+'Width'],10),v=b-e.offsets.popper[m]-y-E;return v=ee(Q(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},le(n,m,$(v)),le(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ge.FLIP:p=[n,i];break;case ge.CLOCKWISE:p=G(n);break;case ge.COUNTERCLOCKWISE:p=G(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&g||!w&&'end'===r&&u);(m||b||y)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),y&&(r=z(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=fe({},e.offsets.popper,D(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!K(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=C(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.rightwindow.devicePixelRatio||!me),c='bottom'===o?'top':'bottom',g='right'===n?'left':'right',b=H('transform');if(d='bottom'==c?'HTML'===l.nodeName?-l.clientHeight+h.bottom:-f.height+h.bottom:h.top,s='right'==g?'HTML'===l.nodeName?-l.clientWidth+h.right:-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[g]=0,m.willChange='transform';else{var w='bottom'==c?-1:1,y='right'==g?-1:1;m[c]=d*w,m[g]=s*y,m.willChange=c+', '+g}var E={"x-placement":e.placement};return e.attributes=fe({},E,e.attributes),e.styles=fe({},m,e.styles),e.arrowStyles=fe({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return j(e.instance.popper,e.styles),V(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&j(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=L(i,t,e,o.positionFixed),p=O(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),j(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},ue}); 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /jupytab-server/jupytab_server/structures.py: -------------------------------------------------------------------------------- 1 | # Code from requests library 2 | # https://github.com/psf/requests/blob/master/requests/structures.py 3 | 4 | from collections import OrderedDict, MutableMapping, Mapping 5 | 6 | 7 | class CaseInsensitiveDict(MutableMapping): 8 | """A case-insensitive ``dict``-like object. 9 | Implements all methods and operations of 10 | ``MutableMapping`` as well as dict's ``copy``. Also 11 | provides ``lower_items``. 12 | All keys are expected to be strings. The structure remembers the 13 | case of the last key to be set, and ``iter(instance)``, 14 | ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` 15 | will contain case-sensitive keys. However, querying and contains 16 | testing is case insensitive:: 17 | cid = CaseInsensitiveDict() 18 | cid['Accept'] = 'application/json' 19 | cid['aCCEPT'] == 'application/json' # True 20 | list(cid) == ['Accept'] # True 21 | For example, ``headers['content-encoding']`` will return the 22 | value of a ``'Content-Encoding'`` response header, regardless 23 | of how the header name was originally stored. 24 | If the constructor, ``.update``, or equality comparison 25 | operations are given keys that have equal ``.lower()``s, the 26 | behavior is undefined. 27 | """ 28 | 29 | def __init__(self, data=None, **kwargs): 30 | self._store = OrderedDict() 31 | if data is None: 32 | data = {} 33 | self.update(data, **kwargs) 34 | 35 | def __setitem__(self, key, value): 36 | # Use the lowercased key for lookups, but store the actual 37 | # key alongside the value. 38 | self._store[key.lower()] = (key, value) 39 | 40 | def __getitem__(self, key): 41 | return self._store[key.lower()][1] 42 | 43 | def __delitem__(self, key): 44 | del self._store[key.lower()] 45 | 46 | def __iter__(self): 47 | return (casedkey for casedkey, mappedvalue in self._store.values()) 48 | 49 | def __len__(self): 50 | return len(self._store) 51 | 52 | def lower_items(self): 53 | """Like iteritems(), but with all lowercase keys.""" 54 | return ( 55 | (lowerkey, keyval[1]) 56 | for (lowerkey, keyval) 57 | in self._store.items() 58 | ) 59 | 60 | def __eq__(self, other): 61 | if isinstance(other, Mapping): 62 | other = CaseInsensitiveDict(other) 63 | else: 64 | return NotImplemented 65 | # Compare insensitively 66 | return dict(self.lower_items()) == dict(other.lower_items()) 67 | 68 | # Copy is required 69 | def copy(self): 70 | return CaseInsensitiveDict(self._store.values()) 71 | 72 | def __repr__(self): 73 | return str(dict(self.items())) 74 | -------------------------------------------------------------------------------- /jupytab-server/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ipykernel 2 | pytest 3 | flake8 4 | pytest-cov 5 | coverage 6 | -------------------------------------------------------------------------------- /jupytab-server/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyter_kernel_gateway>=2,<3 -------------------------------------------------------------------------------- /jupytab-server/samples/air-flights/AirFlights.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "pycharm": {} 7 | }, 8 | "source": [ 9 | "# Air Flights\n", 10 | "This notebook uses [OpenSky](https://opensky-network.org/)'s HTTP Rest API to detect flights positions. \n", 11 | "Data is issued from a network of land probes so it will not display datas for planes currently flying over the \n", 12 | "ocean or uninhabited area.\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": { 19 | "pycharm": {} 20 | }, 21 | "outputs": [], 22 | "source": [ 23 | "import os\n", 24 | "import requests\n", 25 | "import json\n", 26 | "import urllib3\n", 27 | "import pandas as pd\n", 28 | "\n", 29 | "import jupytab" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "# Proxy configuration\n", 37 | "A proxy may be defined as an environment variable (HTTP_PROXY, HTTPS_PROXY) and used to retrieve datas from OpenSky Rest API" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": { 44 | "pycharm": {} 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "http_proxy = os.environ.get('HTTP_PROXY')\n", 49 | "https_proxy = os.environ.get('HTTPS_PROXY')\n", 50 | "\n", 51 | "urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n", 52 | "proxies = {}\n", 53 | "\n", 54 | "if http_proxy:\n", 55 | " proxies[\"http\"] = http_proxy\n", 56 | " print(\"HTTP proxy defined as {}\".format(http_proxy))\n", 57 | "else:\n", 58 | " print(\"No HTTP proxy defined\")\n", 59 | " \n", 60 | "if https_proxy:\n", 61 | " proxies[\"https\"] = https_proxy\n", 62 | " print(\"HTTPS proxy defined as {}\".format(http_proxy))\n", 63 | "else:\n", 64 | " print(\"No HTTPS proxy defined\")" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "# OpenSky Rest API call and DataFrame creation\n" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": { 78 | "pycharm": {} 79 | }, 80 | "outputs": [], 81 | "source": [ 82 | "\n", 83 | "\n", 84 | "def compute_flights():\n", 85 | " try:\n", 86 | " states = requests.get(url='https://opensky-network.org/api/states/all',\n", 87 | " proxies=proxies,\n", 88 | " verify=False,\n", 89 | " timeout=5)\n", 90 | " states_json = states.json()['states']\n", 91 | " except Exception as e:\n", 92 | " states_json = [{'icao24': str(e)}]\n", 93 | "\n", 94 | " return pd.DataFrame(\n", 95 | " data=states_json,\n", 96 | " columns=['icao24', 'callsign', 'origin_country', 'time_position', 'last_contact',\n", 97 | " 'longitude', 'latitude', 'baro_altitude', 'on_ground', 'velocity',\n", 98 | " 'true_track', 'vertical_rate', 'sensors', 'geo_altitude', 'squawk',\n", 99 | " 'spi', 'position_source'])\\\n", 100 | " .set_index('icao24')\n", 101 | "\n", 102 | "flight_df = compute_flights()\n", 103 | "flight_df.head(5)" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Table definition\n", 111 | "This cell will create a Table repository that will allow to expose data through jupyter kernel gateway" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "tables = jupytab.Tables()\n", 121 | "tables['flights'] = jupytab.DataFrameTable('All flights', dataframe=flight_df, refresh_method=compute_flights)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "## Schema rendering\n", 129 | "\n", 130 | "This cell serves schema in json format. The following content is expected when running in a Jupyter notebook\n", 131 | "\n", 132 | "`[{\"id\": \"flights\", \"alias\": \"All flights\", \"columns\": [{\"id\": \"callsign\", \"dataType\": \"string\"}, {\"id\": \"origin_country\", \"dataType\": \"string\"}, {\"id\": \"time_position\", \"dataType\": \"float\"}, {\"id\": \"last_contact\", \"dataType\": \"int\"}, {\"id\": \"longitude\", \"dataType\": \"float\"}, {\"id\": \"latitude\", \"dataType\": \"float\"}, {\"id\": \"baro_altitude\", \"dataType\": \"float\"}, {\"id\": \"on_ground\", \"dataType\":,...]`\n", 133 | "\n", 134 | "The `# GET /schema` **MUST** be provided as the first line of the cell" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": { 141 | "pycharm": {} 142 | }, 143 | "outputs": [], 144 | "source": [ 145 | "# GET /schema\n", 146 | "tables.render_schema()" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "## Data rendering\n", 154 | "This cell serves dataframe in json format.\n", 155 | "\n", 156 | "The following error is expected when running this cell in a jupyter environment as Jupyter Kernel Gateway will provide the REQUEST object.\n", 157 | "` --------------------------------------------------------------------------\n", 158 | " NameError Traceback (most recent call last)\n", 159 | " in ()\n", 160 | " 1 GET /data\n", 161 | " ----> 2 tables.render_data(REQUEST)\n", 162 | " NameError: name 'REQUEST' is not defined\n", 163 | "`\n", 164 | "\n", 165 | "The `# GET /data` **MUST** be provided as the first line of the cell" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": { 172 | "pycharm": {} 173 | }, 174 | "outputs": [], 175 | "source": [ 176 | "# GET /data\n", 177 | "tables.render_data(REQUEST)" 178 | ] 179 | } 180 | ], 181 | "metadata": { 182 | "kernelspec": { 183 | "display_name": "Jupytab Kernel", 184 | "language": "python", 185 | "name": "jupytab-demo" 186 | }, 187 | "language_info": { 188 | "codemirror_mode": { 189 | "name": "ipython", 190 | "version": 3 191 | }, 192 | "file_extension": ".py", 193 | "mimetype": "text/x-python", 194 | "name": "python", 195 | "nbconvert_exporter": "python", 196 | "pygments_lexer": "ipython3", 197 | "version": "3.6.2" 198 | }, 199 | "toc": { 200 | "base_numbering": 1, 201 | "nav_menu": {}, 202 | "number_sections": true, 203 | "sideBar": true, 204 | "skip_h1_title": false, 205 | "title_cell": "Table of Contents", 206 | "title_sidebar": "Contents", 207 | "toc_cell": false, 208 | "toc_position": {}, 209 | "toc_section_display": true, 210 | "toc_window_display": false 211 | } 212 | }, 213 | "nbformat": 4, 214 | "nbformat_minor": 1 215 | } 216 | -------------------------------------------------------------------------------- /jupytab-server/samples/air-flights/AirFlights.twbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/samples/air-flights/AirFlights.twbx -------------------------------------------------------------------------------- /jupytab-server/samples/config.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | listen_port = 8765 3 | security_token = myToken 4 | notebooks = AirFlights|RealEstateCrime|SKLearnClassifier 5 | 6 | [AirFlights] 7 | name = Air Flights 8 | directory = samples/air-flights 9 | path = AirFlights.ipynb 10 | description = Realtime Flights Visualisation (API) 11 | 12 | [RealEstateCrime] 13 | name = Real Estate Crime 14 | directory = samples/real-estate_crime 15 | path = RealEstateCrime.ipynb 16 | description = Real Estate Crime (static CSV) 17 | 18 | [SKLearnClassifier] 19 | name = SKLearn Classifier 20 | directory = samples/sklearn-classifier 21 | path = sklearn-classifier.ipynb 22 | description = SKLearn Classifier Datasource and real-time reply -------------------------------------------------------------------------------- /jupytab-server/samples/real-estate_crime/RealEstateCrime.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "pycharm": {} 7 | }, 8 | "source": [ 9 | "# Real Estate Crime" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": { 15 | "pycharm": {} 16 | }, 17 | "source": [ 18 | "This notebook exposes crime around Sacramento, via a set of CSV files with a bit of pandas magic to create \n", 19 | "a combined data source for Tableau.\n" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": { 26 | "pycharm": {} 27 | }, 28 | "outputs": [], 29 | "source": [ 30 | "import pandas as pd\n", 31 | "\n", 32 | "import jupytab" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": { 39 | "pycharm": {} 40 | }, 41 | "outputs": [], 42 | "source": [ 43 | "crime_df = pd.read_csv('sacramento_crime.csv')\n", 44 | "real_estate_df = pd.read_csv('sacramento_realestate.csv')\n", 45 | "combined_df = crime_df.append(real_estate_df)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## Table definition\n", 53 | "This cell will create a Table repository that will allow to expose data through jupyter kernel gateway" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": { 60 | "pycharm": {} 61 | }, 62 | "outputs": [], 63 | "source": [ 64 | "tables = jupytab.Tables()\n", 65 | "tables['crime'] = jupytab.DataFrameTable('Crime Statistics sample in Sacramento', crime_df)\n", 66 | "tables['real_estate'] = jupytab.DataFrameTable('Real Estate transaction sample in Sacramento', real_estate_df)\n", 67 | "tables['combined'] = jupytab.DataFrameTable('Combination of data sources', combined_df)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": { 73 | "pycharm": {} 74 | }, 75 | "source": [ 76 | "## Schema rendering\n", 77 | "\n", 78 | "This cell serves schema in json format. The following content is expected when running in a Jupyter notebook\n", 79 | "\n", 80 | "`[{\"id\": \"crime\", \"alias\": \"Crime Statistics sample in Sacramento\", \"columns\": [{\"id\": \"cdatetime\", \"dataType\": \"string\"}, {\"id\": \"address\", \"dataType\": \"string\"}, {\"id\": \"district\", \"dataType\": \"int\"}, {\"id\": \"beat\", \"dataType\": \"string\"}, {\"id\": \"grid\", \"dataType\": \"int\"}, {\"id\": \"crimedescr\", \"dataType\": \"string\"}, {\"id\": \"ucr_ncic_code\", \"dataType\": \"int\"}, {\"id\": \"latitude\", \"dataType\": \"float\"}, {\"id\": \"longitude\", \"dataType\": \"float\"}]},...]`\n", 81 | "\n", 82 | "The `# GET /schema` **MUST** be provided as the first line of the cell" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": { 89 | "pycharm": {} 90 | }, 91 | "outputs": [], 92 | "source": [ 93 | "# GET /schema\n", 94 | "tables.render_schema()" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": { 100 | "pycharm": {} 101 | }, 102 | "source": [ 103 | "## Data rendering\n", 104 | "This cell serves dataframe in json format.\n", 105 | "\n", 106 | "The following error is expected when running this cell in a jupyter environment as Jupyter Kernel Gateway will provide the REQUEST object.\n", 107 | "` --------------------------------------------------------------------------\n", 108 | " NameError Traceback (most recent call last)\n", 109 | " in ()\n", 110 | " 1 GET /data\n", 111 | " ----> 2 tables.render_data(REQUEST)\n", 112 | " NameError: name 'REQUEST' is not defined\n", 113 | "`\n", 114 | "\n", 115 | "The `# GET /data` **MUST** be provided as the first line of the cell" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": { 122 | "pycharm": {} 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "# GET /data\n", 127 | "tables.render_data(REQUEST)" 128 | ] 129 | } 130 | ], 131 | "metadata": { 132 | "kernelspec": { 133 | "display_name": "jupytab-demo", 134 | "language": "python", 135 | "name": "jupytab-demo" 136 | }, 137 | "language_info": { 138 | "codemirror_mode": { 139 | "name": "ipython", 140 | "version": 3 141 | }, 142 | "file_extension": ".py", 143 | "mimetype": "text/x-python", 144 | "name": "python", 145 | "nbconvert_exporter": "python", 146 | "pygments_lexer": "ipython3", 147 | "version": "3.6.8" 148 | } 149 | }, 150 | "nbformat": 4, 151 | "nbformat_minor": 1 152 | } 153 | -------------------------------------------------------------------------------- /jupytab-server/samples/real-estate_crime/RealEstateCrime.twbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/samples/real-estate_crime/RealEstateCrime.twbx -------------------------------------------------------------------------------- /jupytab-server/samples/sklearn-classifier/SKLearnClassifier.twbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/samples/sklearn-classifier/SKLearnClassifier.twbx -------------------------------------------------------------------------------- /jupytab-server/samples/sklearn-classifier/sklearn-classifier.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import numpy as np\n", 11 | "import jupytab\n", 12 | "from sklearn.datasets import load_iris\n", 13 | "from sklearn.neural_network import MLPClassifier" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "metadata": {}, 20 | "outputs": [ 21 | { 22 | "data": { 23 | "text/html": [ 24 | "
\n", 25 | "\n", 38 | "\n", 39 | " \n", 40 | " \n", 41 | " \n", 42 | " \n", 43 | " \n", 44 | " \n", 45 | " \n", 46 | " \n", 47 | " \n", 48 | " \n", 49 | " \n", 50 | " \n", 51 | " \n", 52 | " \n", 53 | " \n", 54 | " \n", 55 | " \n", 56 | " \n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | "
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
1057.63.06.62.1
1236.32.74.91.8
615.93.04.21.5
756.63.04.41.4
395.13.41.50.2
\n", 86 | "
" 87 | ], 88 | "text/plain": [ 89 | " sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)\n", 90 | "105 7.6 3.0 6.6 2.1\n", 91 | "123 6.3 2.7 4.9 1.8\n", 92 | "61 5.9 3.0 4.2 1.5\n", 93 | "75 6.6 3.0 4.4 1.4\n", 94 | "39 5.1 3.4 1.5 0.2" 95 | ] 96 | }, 97 | "execution_count": 2, 98 | "metadata": {}, 99 | "output_type": "execute_result" 100 | } 101 | ], 102 | "source": [ 103 | "iris = load_iris()\n", 104 | "iris_data_df = pd.DataFrame(columns=iris.feature_names, data=iris.data)\n", 105 | "iris_target_df = pd.DataFrame(columns=['target'], data=iris.target)\n", 106 | "iris_target_class_df = pd.DataFrame(columns=['target_name'], data=iris.target_names)\n", 107 | "\n", 108 | "tables = jupytab.Tables()\n", 109 | "\n", 110 | "tables['iris'] = jupytab.DataFrameTable(\"Iris DataSet\", iris_data_df, include_index=True)\n", 111 | "tables['iris_target'] = jupytab.DataFrameTable(\"Iris Classification Target\", iris_target_df, include_index=True)\n", 112 | "tables['iris_target_class'] = jupytab.DataFrameTable(\"Iris Classes\", iris_target_class_df, include_index=True)\n", 113 | "\n", 114 | "iris_data_df.sample(5)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 7, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "name": "stderr", 124 | "output_type": "stream", 125 | "text": [ 126 | "/home/btribonde/conda/envs/jupytab-demo/lib/python3.6/site-packages/sklearn/neural_network/multilayer_perceptron.py:564: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (200) reached and the optimization hasn't converged yet.\n", 127 | " % self.max_iter, ConvergenceWarning)\n" 128 | ] 129 | }, 130 | { 131 | "data": { 132 | "text/plain": [ 133 | "'virginica'" 134 | ] 135 | }, 136 | "execution_count": 7, 137 | "metadata": {}, 138 | "output_type": "execute_result" 139 | } 140 | ], 141 | "source": [ 142 | "clf = MLPClassifier().fit(iris.data, iris.target)\n", 143 | "\n", 144 | "def predictor(sepal_length_cm, sepal_width_cm, petal_length_cm, petal_width_cm):\n", 145 | " class_predict = clf.predict([[sepal_length_cm, sepal_width_cm, petal_length_cm, petal_width_cm]])\n", 146 | " return iris.target_names[class_predict][0]\n", 147 | "\n", 148 | "functions = jupytab.Functions()\n", 149 | "functions['predict'] = jupytab.Function('A predictor for Iris DataSet', predictor)\n", 150 | "\n", 151 | "predictor(0.5, 5, 4, 2)" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 4, 157 | "metadata": {}, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "[{\"id\": \"iris\", \"alias\": \"Iris DataSet\", \"columns\": [{\"id\": \"index\", \"dataType\": \"int\"}, {\"id\": \"sepal_length_cm_\", \"dataType\": \"float\"}, {\"id\": \"sepal_width_cm_\", \"dataType\": \"float\"}, {\"id\": \"petal_length_cm_\", \"dataType\": \"float\"}, {\"id\": \"petal_width_cm_\", \"dataType\": \"float\"}]}, {\"id\": \"iris_target\", \"alias\": \"Iris Classification Target\", \"columns\": [{\"id\": \"index\", \"dataType\": \"int\"}, {\"id\": \"target\", \"dataType\": \"int\"}]}, {\"id\": \"iris_target_class\", \"alias\": \"Iris Classes\", \"columns\": [{\"id\": \"index\", \"dataType\": \"int\"}, {\"id\": \"target_name\", \"dataType\": \"string\"}]}]\n" 164 | ] 165 | } 166 | ], 167 | "source": [ 168 | "# GET /schema\n", 169 | "tables.render_schema()" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 5, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "ename": "NameError", 179 | "evalue": "name 'REQUEST' is not defined", 180 | "output_type": "error", 181 | "traceback": [ 182 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 183 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 184 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# GET /data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mtables\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrender_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mREQUEST\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 185 | "\u001b[0;31mNameError\u001b[0m: name 'REQUEST' is not defined" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "# GET /data\n", 191 | "tables.render_data(REQUEST)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 6, 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "ename": "NameError", 201 | "evalue": "name 'REQUEST' is not defined", 202 | "output_type": "error", 203 | "traceback": [ 204 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 205 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 206 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# POST /evaluate\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mfunctions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrender_evaluate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mREQUEST\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 207 | "\u001b[0;31mNameError\u001b[0m: name 'REQUEST' is not defined" 208 | ] 209 | } 210 | ], 211 | "source": [ 212 | "# POST /evaluate\n", 213 | "functions.render_evaluate(REQUEST)" 214 | ] 215 | } 216 | ], 217 | "metadata": { 218 | "hide_input": false, 219 | "kernelspec": { 220 | "display_name": "jupytab-demo", 221 | "language": "python", 222 | "name": "jupytab-demo" 223 | }, 224 | "language_info": { 225 | "codemirror_mode": { 226 | "name": "ipython", 227 | "version": 3 228 | }, 229 | "file_extension": ".py", 230 | "mimetype": "text/x-python", 231 | "name": "python", 232 | "nbconvert_exporter": "python", 233 | "pygments_lexer": "ipython3", 234 | "version": "3.6.8" 235 | }, 236 | "toc": { 237 | "base_numbering": 1, 238 | "nav_menu": {}, 239 | "number_sections": true, 240 | "sideBar": true, 241 | "skip_h1_title": false, 242 | "title_cell": "Table of Contents", 243 | "title_sidebar": "Contents", 244 | "toc_cell": false, 245 | "toc_position": {}, 246 | "toc_section_display": true, 247 | "toc_window_display": false 248 | } 249 | }, 250 | "nbformat": 4, 251 | "nbformat_minor": 4 252 | } 253 | -------------------------------------------------------------------------------- /jupytab-server/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | 4 | -------------------------------------------------------------------------------- /jupytab-server/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Capital Fund Management 2 | # Distributed under the terms of the MIT License. 3 | 4 | from setuptools import setup, find_packages 5 | 6 | import distutils.cmd 7 | import pathlib 8 | 9 | 10 | class MakeVersionCommand(distutils.cmd.Command): 11 | """Prepare the version of jupytab-server""" 12 | 13 | description = "add extra file to make the version" 14 | user_options = [] 15 | 16 | def initialize_options(self): 17 | pass 18 | 19 | def finalize_options(self): 20 | pass 21 | 22 | def run(self): 23 | version_file = pathlib.Path(__file__).parent.resolve() 24 | version_file = version_file.parent / 'VERSION' 25 | version = version_file.resolve().read_text().strip() 26 | version_py_file = pathlib.Path(__file__).parent / 'jupytab_server' / '__version__.py' 27 | with version_py_file.open('w') as stream: 28 | stream.write(f'__version__ = "{version}"\n') 29 | 30 | 31 | def read_version(): 32 | version = pathlib.Path(__file__).parent.resolve() 33 | version_master = version.parent / 'VERSION' 34 | version_master.resolve() 35 | version_4_release = version / 'jupytab_server' / '__version__.py' 36 | if version_4_release.exists(): 37 | v = version_4_release.read_text().strip().split('=')[1].strip() 38 | # Let's remove quotes 39 | return v[1:len(v) - 1] 40 | elif version_master.exists(): 41 | return version_master.read_text().strip() 42 | else: 43 | raise FileNotFoundError('no version file can be found') 44 | 45 | 46 | README = """ 47 | 48 | Jupytab Server allows you to **explore in Tableau data which is generated 49 | dynamically by a Jupyter Notebook**. You can thus create Tableau data 50 | sources in a very flexible way using all the power of Python. 51 | This is achieved by having Tableau access data through a **web 52 | server created by Jupytab**. 53 | 54 | Jupytab Server is built on **solid foundations**: Tableau's Web Data 55 | Connector and the Jupyter Kernel Gateway. 56 | 57 | Features: 58 | 59 | * **Expose multiple pandas dataframes** to Tableau from a Jupyter 60 | notebook 61 | * Access **several notebooks** from Tableau through a **single 62 | entry point** (web server) 63 | * Manage your notebooks using a **web interface** 64 | * **Secure access** to your data 65 | 66 | The full documentation is available on the project's home page. 67 | """ 68 | 69 | # This call to setup() does all the work 70 | setup( 71 | cmdclass=dict(make_version=MakeVersionCommand), 72 | name="jupytab-server", 73 | version=read_version(), 74 | description="Connect Tableau to your Jupyter Notebook", 75 | long_description_content_type="text/markdown", 76 | long_description=README, 77 | url="https://github.com/CFMTech/Jupytab", 78 | author="Brian Tribondeau", 79 | author_email="brian.tribondeau@cfm.fr", 80 | license="MIT", 81 | keywords="jupytab jupyter notebook wdc tableau", 82 | classifiers=[ 83 | "License :: OSI Approved :: MIT License", 84 | "Programming Language :: Python :: 3", 85 | "Programming Language :: Python :: 3.7", 86 | ], 87 | packages=find_packages(exclude=["tests", "env", "docs", "build"]), 88 | python_requires='>=3.6', 89 | include_package_data=True, 90 | install_requires=["jupyter_kernel_gateway"], 91 | project_urls={ 92 | "Bug Tracker": "https://github.com/CFMTech/Jupytab/issues", 93 | "Documentation": "https://github.com/CFMTech/Jupytab", 94 | "Source Code": "https://github.com/CFMTech/Jupytab" 95 | }, 96 | entry_points={ 97 | "console_scripts": [ 98 | "jupytab=jupytab_server.jupytab:main", 99 | ] 100 | }, 101 | ) 102 | -------------------------------------------------------------------------------- /jupytab-server/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-server/tests/__init__.py -------------------------------------------------------------------------------- /jupytab-server/tests/config/example.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBzCCAe+gAwIBAgIJAK8X55pvCWVBMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV 3 | BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMDAzMjMxMTM0MjhaFw0zMDAzMjExMTM0 4 | MjhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB 5 | BQADggEPADCCAQoCggEBAOoF0nX1ZfQ686G+G7jexm9pz39GRYnlPRg5g/oj9MJl 6 | fpvkWdh3bBEmitsOpZDV8AgHKJlZD6rR6DX9w+Ay0g/QCd9lWKlVNusod8qyu2m2 7 | BM+oFbgex7JKObnd+CZq6Fe5/lWGDKqDkrTTijJes8QfkySEgevdYRUekgCT+ydI 8 | nldZHRnIdckRp3LBFoGdIQkZqtVKht+ECQotSeACW6EE4a88cY+ezupi6AG2I3kM 9 | Bm4UCUdD6x/q4ia6Cj7PKlg0jENdYBaOb6ZyPMKtyzYOikAWUrmA4txFc0uavgpZ 10 | rIspgnR/alZMiVjUPf4l/23cac/epD6li1aQJi0sU40CAwEAAaNQME4wHQYDVR0O 11 | BBYEFMIRQx8EBLmtn8sFy/XM3Ku23fzAMB8GA1UdIwQYMBaAFMIRQx8EBLmtn8sF 12 | y/XM3Ku23fzAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAOeLHtKp 13 | mzjLwzY1HZiLF/xKLYAeSNfQaFEso1BVLezswmMqh5UPQoWtSY6SexqkbGwewt16 14 | oYgI703Zs8SqXmBXTvVPbZ+GKviFat7sWCTZmQHBDjr46XZqbYvc+hbHNsl5Gu00 15 | uujR9GjYnogtvjARWmwLNRp0XCKOytGd2/ggnHV4o/92HoxdFBRp0qcpQJtu9gkd 16 | C/f7ZEx5vtnR70a+aQ4CpMdhoFJTf5pb5iwoHDn3vm0N1yZicKT0IXbEs4FcBAiU 17 | T6bc06cAnTzeIC/lbmTfcrpHw3J3JuG/Af3X1iIptJuYM0kcJOXPd2VU3hFu8xPW 18 | qCUBbTRSF+z6fxk= 19 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /jupytab-server/tests/config/example.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA6gXSdfVl9Drzob4buN7Gb2nPf0ZFieU9GDmD+iP0wmV+m+RZ 3 | 2HdsESaK2w6lkNXwCAcomVkPqtHoNf3D4DLSD9AJ32VYqVU26yh3yrK7abYEz6gV 4 | uB7Hsko5ud34JmroV7n+VYYMqoOStNOKMl6zxB+TJISB691hFR6SAJP7J0ieV1kd 5 | Gch1yRGncsEWgZ0hCRmq1UqG34QJCi1J4AJboQThrzxxj57O6mLoAbYjeQwGbhQJ 6 | R0PrH+riJroKPs8qWDSMQ11gFo5vpnI8wq3LNg6KQBZSuYDi3EVzS5q+ClmsiymC 7 | dH9qVkyJWNQ9/iX/bdxpz96kPqWLVpAmLSxTjQIDAQABAoIBAQCCzcRICGT3MOgy 8 | VIc8OtChP3wqQIXnwIj4fFVnQCezbHVq/yS02HM/1tIwBKzIGrwyUIYByIT4TqFD 9 | ZFbSfrVo/zg1dHktFKNAp3rlgic8u+9Ofj29jv7BiblgSVBFcOXy+tPMy8NSn34l 10 | skOBSeuiyJ8+/w17X16/JjonNo9f8aYvB3HrucemHb5l3pN01G1PyTte742E8L+9 11 | MAKeMobrbbkSdfNXS8OhPTKmAV3VmVr47IkZL4DAa2K1uHTI6Zi2drx23q5eOe6v 12 | 2T6e7Kj7pPNvUgUb2N3CGr6ATBtHT+uoxNYmLEJVLCSCxjbnXgOlpyB1RxtxSt5s 13 | 93s2yd8FAoGBAP7lVk+zd/Rk4VwOpFEXC15kvKn3r2Iq3VWwFPOQ4s7WsGbN4jBd 14 | ZN53OMM0LeeRFqLOhqYD9xkV/XoA33wXdeVnv+pd+DXEtciPh2zdbnSti8kipqMN 15 | WFxqSGtGyCc0P1foOvkVa2r+p9ihnhTW6k8e03wWNSZ9py8+X0g0vrFfAoGBAOsJ 16 | VopQkzH3akLwArx9Lk3o4gRBAzxS4Ok4e0iIdr02/AkatT2oggcAid4ckPpW0VEh 17 | IbT9XicVc10cso5P1ozCyCdjbUcFyX63wr46ItwspyEqJTgyL6gEzwj2Rmoc/nXQ 18 | Rwbr/JpeKQ6Q/iRq+zuLlfja868wRWo7zjbpo8aTAoGBALFpkLSytqg9WvoHGulx 19 | /7C4rvQieEj8isesYjjRPHw4w9kaLff52U5abwC3HchSnQ2+b8u3cNJeEupLF0I4 20 | 1g9RMiv/MdbCzsAE3n6wdMPzUxsw6gkNLdZNB5DbWE6pN/mIoxthhD2Zd9v5SZ05 21 | pSZiz1JL5ryesrHYWNtaEuxDAoGBAIxReN7+l8Ie6cuoqpmJSpmszTKo9ZuQB0J1 22 | O/Tjs6/nIbT1wvpanbY8dhKqj0tFhZWf6BW7pfhDcCpItbkMpRRIPWJ2k4jxRYhn 23 | gNY8sw8rgWPlW28fVyBCLrA1B3jWcnw3qg/R1275hB10JqXrUK4N+a0mWpFeijKQ 24 | Hd7ewa4NAoGAXsZSWXG8etwXTc8/i4f0oNItGxIcHdyYJaLTxjDHWyXbbd0rkyhU 25 | PPenULrwLRBpTU5BytLpWjnJLmEj4hbSrFnr9M/o3O8ccBo5ghYQJZYfO/k5ylp3 26 | liQNI7MYUdZLF0D0csouFMnRn6kA2zGkaaImH+EnIEzZxO1XSLJExfY= 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /jupytab-server/tests/config/ssl_bad_file.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | ssl_enabled = true 3 | ssl_key = config/eXample.key 4 | ssl_cert = config/eXample.cert -------------------------------------------------------------------------------- /jupytab-server/tests/config/ssl_disabled.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | ssl_enabled = false 3 | ssl_key = config/example.key 4 | ssl_cert = config/example.cert -------------------------------------------------------------------------------- /jupytab-server/tests/config/ssl_none.ini: -------------------------------------------------------------------------------- 1 | [main] -------------------------------------------------------------------------------- /jupytab-server/tests/config/ssl_ok.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | ssl_enabled = true 3 | ssl_key = config/example.key 4 | ssl_cert = config/example.cert -------------------------------------------------------------------------------- /jupytab-server/tests/test_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from configparser import ConfigParser 3 | 4 | import pytest 5 | 6 | import jupytab_server 7 | from jupytab_server.jupytab import config_ssl 8 | 9 | dir_path = os.path.dirname(os.path.realpath(__file__)) 10 | os.chdir(dir_path) 11 | 12 | 13 | def config(config_file): 14 | config = ConfigParser() 15 | config.optionxform = str 16 | config.read(config_file) 17 | return config 18 | 19 | 20 | def test_ssl(): 21 | with pytest.raises(FileNotFoundError): 22 | config_ssl(config('config/ssl_bad_file.ini')) 23 | assert config_ssl(config('config/ssl_disabled.ini')) is None 24 | assert config_ssl(config('config/ssl_none.ini')) is None 25 | assert config_ssl(config('config/ssl_ok.ini')) == {'certfile': 'config/example.cert', 26 | 'keyfile': 'config/example.key'} 27 | 28 | 29 | def test_version(): 30 | assert jupytab_server.__version__ is not None 31 | -------------------------------------------------------------------------------- /jupytab-server/tests/test_nb_run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import threading 4 | import time 5 | from json.decoder import JSONDecodeError 6 | 7 | import pytest 8 | import requests 9 | from tornado.ioloop import IOLoop 10 | 11 | from jupytab_server.jupytab import parse_config, create_server_app 12 | 13 | THIS_DIR = os.path.abspath(os.path.dirname(__file__)) 14 | RESOURCES = os.path.abspath(os.path.join(THIS_DIR, '..')) 15 | 16 | PROTOCOL = "http" 17 | HOST = "127.0.0.1" 18 | PORT = "8765" 19 | SECURITY_TOKEN = "02014868fe0eef123269397c5bc65a9608b3cedb73e3b84d8d02c220" 20 | 21 | event_loop_instance = None 22 | 23 | 24 | def run_jupytab(): 25 | # Required as we run the server in a new thread 26 | asyncio.set_event_loop(asyncio.new_event_loop()) 27 | os.chdir(os.path.abspath(RESOURCES)) 28 | 29 | params = parse_config(config_file="samples/config.ini") 30 | 31 | server_app = create_server_app(**params) 32 | server_app.listen(params['listen_port']) 33 | 34 | global event_loop_instance 35 | event_loop_instance = IOLoop.instance() 36 | event_loop_instance.start() 37 | 38 | 39 | @pytest.fixture(scope='session', autouse=True) 40 | def setup_jupytab(): 41 | # Setup 42 | thread = threading.Thread(target=run_jupytab) 43 | thread.daemon = True 44 | thread.start() 45 | 46 | yield 47 | 48 | # Teardown 49 | global event_loop_instance 50 | # Cleanly shutdown the event loop used for testing server 51 | event_loop_instance.add_callback(event_loop_instance.stop) 52 | 53 | 54 | def build_uri(uri, params=None, protocol=PROTOCOL, host=HOST, port=PORT, token=SECURITY_TOKEN): 55 | token = f"security_token={token}" if token else "" 56 | return f"{protocol}://{host}:{port}/{uri}?{token}&{params}" 57 | 58 | 59 | def rget(uri, delay_seconds=5, attempt_count=6): 60 | for attempt in range(attempt_count): 61 | try: 62 | response = requests.get(uri) 63 | if response.status_code == 200: 64 | return response.json() 65 | except requests.exceptions.RequestException as e: 66 | print("RequestException", e) 67 | except JSONDecodeError as e: 68 | print("JSONDecodeError", e) 69 | 70 | time.sleep(delay_seconds) 71 | 72 | raise ConnectionError(f"Unable to retrieve GET datas from {uri}") 73 | 74 | 75 | def rpost(uri, body, delay_seconds=5, attempt_count=6): 76 | for attempt in range(attempt_count): 77 | try: 78 | response = requests.post(uri, json=body) 79 | if response.status_code == 200: 80 | return response.json() 81 | except requests.exceptions.RequestException as e: 82 | print("RequestException", e) 83 | except JSONDecodeError as e: 84 | print("JSONDecodeError", e) 85 | 86 | time.sleep(delay_seconds) 87 | 88 | raise ConnectionError(f"Unable to retrieve POST datas from {uri}") 89 | 90 | 91 | def get_table_by_id(tables, id): 92 | for table in tables: 93 | if table['id'] == id: 94 | return table 95 | return None 96 | 97 | 98 | def get_column_by_id(columns, id): 99 | for column in columns: 100 | if column['id'] == id: 101 | return column 102 | return None 103 | 104 | 105 | def test_airflights_schema(): 106 | response = rget(build_uri("kernel/AirFlights/schema")) 107 | 108 | json_result = response 109 | 110 | flight_table = get_table_by_id(json_result, 'flights') 111 | assert get_column_by_id(flight_table['columns'], "callsign") is not None 112 | velocity = get_column_by_id(flight_table['columns'], "velocity") 113 | assert velocity['dataType'] == "float" 114 | 115 | assert len(json_result[0]['columns']) == 16 116 | 117 | 118 | def test_airflights_data(): 119 | response = rget(build_uri("kernel/AirFlights/data", "table_name=flights")) 120 | 121 | json_result = response 122 | 123 | assert len(json_result) > 0 124 | assert 'callsign' in json_result[0] 125 | assert 'time_position' in json_result[0] 126 | assert 'baro_altitude' in json_result[0] 127 | 128 | 129 | def test_realestatecrime_schema(): 130 | response = rget(build_uri("kernel/RealEstateCrime/schema")) 131 | 132 | json_result = response 133 | 134 | crime_table = get_table_by_id(json_result, 'crime') 135 | assert len(crime_table['columns']) == 9 136 | assert get_column_by_id(crime_table['columns'], 'beat') is not None 137 | 138 | real_estate_table = get_table_by_id(json_result, 'real_estate') 139 | assert len(real_estate_table['columns']) == 12 140 | city = get_column_by_id(real_estate_table['columns'], 'city') 141 | assert city['dataType'] == 'string' 142 | 143 | combined = get_table_by_id(json_result, 'combined') 144 | assert len(combined['columns']) == 19 145 | baths = get_column_by_id(real_estate_table['columns'], 'baths') 146 | assert baths['dataType'] == 'int' 147 | 148 | 149 | def test_realestatecrime_data(): 150 | response = rget(build_uri("kernel/RealEstateCrime/data", "table_name=combined")) 151 | 152 | json_result = response 153 | 154 | assert len(json_result) > 0 155 | assert 'address' in json_result[0] 156 | assert 'beat' in json_result[0] 157 | assert 'city' in json_result[0] 158 | 159 | 160 | def test_sklearn_classifier_schema(): 161 | response = rget(build_uri("kernel/SKLearnClassifier/schema")) 162 | 163 | json_result = response 164 | 165 | assert len(json_result) == 3 166 | 167 | assert get_table_by_id(json_result, 'iris_target') is not None 168 | 169 | 170 | def test_sklearn_classifier_data(): 171 | response = rget(build_uri("kernel/SKLearnClassifier/data", "table_name=iris")) 172 | 173 | json_result = response 174 | 175 | assert len(json_result) > 100 176 | assert len(json_result[0]) == 5 177 | 178 | 179 | def test_sklearn_classifier_function(): 180 | predict_function = { 181 | "script": "SKLearnClassifier.predict", 182 | "data": { 183 | "_arg1": [5.1, 5.8, 6.5], 184 | "_arg2": [3.5, 2.6, 3], 185 | "_arg3": [1.4, 4, 5.5], 186 | "_arg4": [0.2, 1.2, 2.4], 187 | } 188 | } 189 | 190 | response = rpost(build_uri("evaluate"), predict_function) 191 | 192 | assert response == ["setosa", "versicolor", "virginica"] 193 | -------------------------------------------------------------------------------- /jupytab-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab-small.png -------------------------------------------------------------------------------- /jupytab.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab.pdn -------------------------------------------------------------------------------- /jupytab/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * -------------------------------------------------------------------------------- /jupytab/README.md: -------------------------------------------------------------------------------- 1 | Jupytab -------------------------------------------------------------------------------- /jupytab/jupytab/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .table import Tables 5 | from .dataframe_table import DataFrameTable 6 | from .function import Function, Functions 7 | from .__version__ import __version__ 8 | 9 | __all__ = [Tables, DataFrameTable, Functions, Function, __version__] 10 | -------------------------------------------------------------------------------- /jupytab/jupytab/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.9.11" 2 | -------------------------------------------------------------------------------- /jupytab/jupytab/dataframe_table.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | import pandas as pd 4 | from collections import Counter 5 | 6 | from .table import BaseTable 7 | 8 | 9 | class DataFrameTable(BaseTable): 10 | """ 11 | This class represents a jupytab-ready table that exposes a Pandas DataFrame. 12 | """ 13 | 14 | def __init__(self, alias, dataframe=None, refresh_method=None, include_index=False): 15 | """ 16 | alias -- Descriptive name of the table, that will be displayed in Tableau. 17 | 18 | dataframe -- Pandas DataFrame to be accessed from Tableau (may be None if a 19 | refresh_method is provided). 20 | 21 | refresh_method -- Optional method callback that will be called every time 22 | Tableau needs to access the data (for instance when the DataSource is refreshed). 23 | It takes no argument and must return a DataFrame with the same column layout 24 | (schema) as the original DataFrame (if any). 25 | 26 | include_index -- Add Index as column(s) in the output data to Tableau. 27 | """ 28 | BaseTable.__init__(self, alias=alias) 29 | 30 | self._dataframe = dataframe 31 | self._refresh_method = refresh_method 32 | self._include_index = include_index 33 | self._index_separator = '_' 34 | 35 | self.types_mapping = { 36 | 'object': 'string', 37 | 'int64': 'int', 38 | 'float64': 'float', 39 | 'datetime64[ns]': 'datetime', 40 | 'bool': 'bool' 41 | } 42 | 43 | @staticmethod 44 | def clean_column_name(col): 45 | """Remove all forbidden characters from column names""" 46 | 47 | # Try to preserve accented characters 48 | cleaned_col = unicodedata.normalize('NFD', str(col)) \ 49 | .encode('ascii', 'ignore') \ 50 | .decode("utf-8") 51 | # Remove all non matching chars for Tableau WDC 52 | cleaned_col = re.sub(r'[^A-Za-z0-9_]+', '_', cleaned_col) 53 | return cleaned_col 54 | 55 | @staticmethod 56 | def replace_duplicated_column_name(cols): 57 | """Replace duplicated columns names""" 58 | cols_count_dict = dict(Counter(cols)) 59 | # Filter unique items 60 | cols_count_dict = {key: value for (key, value) in cols_count_dict.items() if value > 1} 61 | unique_cols = list() 62 | for col in reversed(cols): 63 | idx = cols_count_dict.get(col, 0) 64 | unique_cols.insert(0, col if idx == 0 else col + '_' + str(idx)) 65 | cols_count_dict[col] = idx - 1 66 | return unique_cols 67 | 68 | def get_schema(self, key): 69 | self.refresh(only_if_undefined=True) 70 | 71 | columns = [ 72 | { 73 | 'id': '.'.join(filter(None, key)) if isinstance(key, tuple) else key, 74 | 'dataType': 75 | self.types_mapping[str(value)] if str(value) in self.types_mapping else 'string' 76 | } 77 | for key, value in (self._prepare_dataframe()).dtypes.items() 78 | ] 79 | 80 | return { 81 | 'id': key, 82 | 'alias': self._alias, 83 | 'columns': columns 84 | } 85 | 86 | def _prepare_dataframe(self, slice_from=None, slice_to=None): 87 | 88 | # Guarantee valid range for slicing 89 | if slice_from is None or slice_from < 0: 90 | slice_from = 0 91 | 92 | if slice_to is None: 93 | slice_to = len(self._dataframe) 94 | 95 | if slice_from > slice_to: 96 | raise IndexError(f"From ({slice_from}) can not be greater than To ({slice_to})") 97 | 98 | # Apply slicing to dataframe 99 | if slice_from < len(self._dataframe): 100 | # If slicing is in dataframe range 101 | output_df = self._dataframe.iloc[slice_from: min(slice_to, len(self._dataframe))] 102 | else: 103 | # If slicing is outside dataframe range, return an empty dataframe 104 | output_df = pd.DataFrame(columns=self._dataframe.columns) 105 | 106 | # Remove index if it is not required 107 | prep_df = output_df.reset_index() \ 108 | if self._include_index \ 109 | else output_df.reset_index(drop=True) 110 | 111 | # Flatten multi-index 112 | if isinstance(prep_df.columns, pd.MultiIndex): 113 | prep_df.columns = [self._index_separator.join(map(str, col)).strip() 114 | for col in prep_df.columns.values] 115 | 116 | prep_df.columns = [DataFrameTable.clean_column_name(col) for col in prep_df.columns] 117 | prep_df.columns = DataFrameTable.replace_duplicated_column_name(prep_df.columns) 118 | 119 | return prep_df 120 | 121 | def refresh(self, only_if_undefined=False): 122 | # If DataFrame exists and it is not requested to update it then we do not need to refresh. 123 | # Otherwise if a refresh method has been set it is required to update the DataFrame. 124 | if (not only_if_undefined or self._dataframe is None) and self._refresh_method is not None: 125 | self._dataframe = self._refresh_method() 126 | 127 | def to_output(self, print_format='json', slice_from=None, slice_to=None): 128 | # Enforce print_format to be lower case 129 | print_format = print_format.lower() 130 | # Retrieve the DataFrame to be sent 131 | output_df = self._prepare_dataframe(slice_from=slice_from, slice_to=slice_to) 132 | 133 | # Directory of available formatter for output 134 | output_formatter = { 135 | 'json': lambda df: df.to_json(orient='records', date_format="iso", date_unit="s") 136 | } 137 | 138 | if print_format in output_formatter.keys(): 139 | return output_formatter[print_format](output_df) 140 | else: 141 | raise NotImplementedError( 142 | f"'{print_format}' format not supported." 143 | f"Please use one of the following : {output_formatter.keys()}" 144 | ) 145 | -------------------------------------------------------------------------------- /jupytab/jupytab/function.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class Function: 5 | def __init__(self, alias, method): 6 | self.alias = alias 7 | self.method = method 8 | 9 | def __call__(self, *args, **kwargs): 10 | return self.method(*args) 11 | 12 | 13 | class Functions: 14 | """ 15 | Function manager exposed as a dictionary to keep track of all registered functions and create a 16 | combined schema. 17 | """ 18 | 19 | def __init__(self, *args): 20 | self.functions = {} 21 | 22 | def __setitem__(self, key, value): 23 | assert isinstance(value, Function) 24 | self.functions[key.lower()] = value 25 | 26 | def __getitem__(self, key): 27 | return self.functions[key.lower()] 28 | 29 | def render_evaluate(self, request, do_print=True): 30 | """ 31 | # POST /evaluate 32 | tables.render_evaluate() 33 | """ 34 | jreq = json.loads(request) 35 | 36 | if 'function' not in jreq['body']: 37 | raise ValueError(f" field is expected to evaluate method -> {jreq['body']}") 38 | 39 | function_name = jreq['body']['function'] 40 | 41 | if 'data' not in jreq['body']: 42 | raise ValueError(f" field is expected to evaluate method -> {jreq['body']}") 43 | 44 | arg_dict = jreq['body']['data'] 45 | 46 | function = self[function_name.lower()] 47 | ret_value = list() 48 | 49 | arg_dict_values = [value for key, value in sorted(arg_dict.items())] 50 | 51 | for arg_values in zip(*arg_dict_values): 52 | try: 53 | invoke_result = function(*arg_values) 54 | ret_value.append(invoke_result) 55 | except Exception as e: 56 | ret_value.append(str(e)) 57 | 58 | json_return = json.dumps(ret_value) 59 | 60 | if do_print: 61 | print(json_return) 62 | else: 63 | return json_return 64 | -------------------------------------------------------------------------------- /jupytab/jupytab/table.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import json 5 | 6 | 7 | class BaseTable: 8 | """ 9 | Abstract class with default methods to be implemented, that represents a table-like object that 10 | can be used by Jupytab. DataFrames, SQL queries, CSV files... could be implemented as BaseTable 11 | child classes. 12 | """ 13 | 14 | def __init__(self, alias): 15 | """ 16 | alias -- A descriptive name of the table that will be displayed in Tableau. 17 | """ 18 | self._alias = alias 19 | 20 | @property 21 | def alias(self): 22 | """ 23 | A descriptive name for the Table. 24 | """ 25 | return self._alias 26 | 27 | def get_schema(self, key): 28 | """ 29 | Provide a table schema for Jupytab. 30 | 31 | Returns a dictionary that describes the table provided by the schema: 32 | { 33 | "id": :obj:`str` 34 | "alias": :obj:`str` 35 | "columns" :obj:`list` 36 | } 37 | 38 | key -- A uniquely identified schema. 39 | """ 40 | raise NotImplementedError 41 | 42 | def refresh(self, only_if_undefined=False): 43 | """ 44 | Refresh the table if an available refresh method is available. 45 | 46 | only_if_undefined -- true for refreshing only if there is no data currently available. 47 | """ 48 | raise NotImplementedError 49 | 50 | def to_output(self, print_format='json', slice_from=None, slice_to=None): 51 | """ 52 | Return the table contents as requested format, and selected slice 53 | """ 54 | raise NotImplementedError 55 | 56 | 57 | class Tables: 58 | """ 59 | Table manager exposed as a dictionary to keep track of all registered tables and create a 60 | combined schema. 61 | """ 62 | 63 | def __init__(self, *args): 64 | self.tables = {} 65 | 66 | def __setitem__(self, key, value): 67 | assert isinstance(value, BaseTable) 68 | self.tables[key.lower()] = value 69 | 70 | def __getitem__(self, key): 71 | return self.tables[key.lower()] 72 | 73 | def schema(self): 74 | """ 75 | Return a list of all registered table schemas. 76 | """ 77 | return [value.get_schema(key) for key, value in self.tables.items()] 78 | 79 | def render_schema(self, do_print=True): 80 | """ 81 | Return the JSON with the table schemas or None (if it is printed [default]). 82 | 83 | To be used in the notebook in a single cell like the code provided below: 84 | 85 | ``` 86 | # GET /schema 87 | tables.render_schema() 88 | ``` 89 | 90 | This will generate a string in the cell output that Jupytab will be able to use. 91 | 92 | do_print -- If true, do not return the JSON object but print it instead. 93 | """ 94 | rendered_schema = json.dumps(self.schema()) 95 | if do_print: 96 | print(rendered_schema) 97 | else: 98 | return rendered_schema 99 | 100 | def render_data(self, request, do_print=True): 101 | """ 102 | Return JSON with the table data or None (if it is printed [default]). 103 | 104 | To be used in the notebook in a single cell like the code provided below: 105 | 106 | ``` 107 | # GET /data 108 | tables.render_data() 109 | ``` 110 | 111 | This will generate a string in the cell output that Jupytab will be able to use. 112 | 113 | do_print -- If true, do not return the JSON object but print it instead. 114 | """ 115 | 116 | request_dict = json.loads(request)['args'] 117 | 118 | table_name = request_dict['table_name'][0] 119 | data_format = request_dict['format'][0] if 'format' in request_dict else 'json' 120 | slice_from = int(request_dict['from'][0]) if 'from' in request_dict else None 121 | slice_to = int(request_dict['to'][0]) if 'to' in request_dict else None 122 | refresh_required = request_dict['refresh'][0] == 'true'\ 123 | if 'refresh' in request_dict else True 124 | 125 | table = self[table_name] 126 | 127 | if refresh_required: 128 | table.refresh() 129 | 130 | rendered_data = table.to_output(print_format=data_format, 131 | slice_from=slice_from, 132 | slice_to=slice_to) 133 | if do_print: 134 | print(rendered_data) 135 | else: 136 | return rendered_data 137 | -------------------------------------------------------------------------------- /jupytab/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | flake8 3 | pytest-cov 4 | coverage 5 | scikit-learn -------------------------------------------------------------------------------- /jupytab/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas -------------------------------------------------------------------------------- /jupytab/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=100 3 | 4 | -------------------------------------------------------------------------------- /jupytab/setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Capital Fund Management 2 | # Distributed under the terms of the MIT License. 3 | 4 | from setuptools import setup, find_packages 5 | 6 | import distutils.cmd 7 | import pathlib 8 | 9 | 10 | class MakeVersionCommand(distutils.cmd.Command): 11 | """Prepare the version of jupytab""" 12 | 13 | description = "add extra file to make the version" 14 | user_options = [] 15 | 16 | def initialize_options(self): 17 | pass 18 | 19 | def finalize_options(self): 20 | pass 21 | 22 | def run(self): 23 | version_file = pathlib.Path(__file__).parent.resolve() 24 | version_file = version_file.parent / 'VERSION' 25 | version = version_file.resolve().read_text().strip() 26 | version_py_file = pathlib.Path(__file__).parent / 'jupytab' / '__version__.py' 27 | with version_py_file.open('w') as stream: 28 | stream.write(f'__version__ = "{version}"\n') 29 | 30 | 31 | def read_version(): 32 | version = pathlib.Path(__file__).parent.resolve() 33 | version_master = version.parent / 'VERSION' 34 | version_master.resolve() 35 | version_4_release = version / 'jupytab' / '__version__.py' 36 | if version_4_release.exists(): 37 | v = version_4_release.read_text().strip().split('=')[1].strip() 38 | # Let's remove quotes 39 | return v[1:len(v) - 1] 40 | elif version_master.exists(): 41 | return version_master.read_text().strip() 42 | else: 43 | raise FileNotFoundError('no version file can be found') 44 | 45 | 46 | README = """ 47 | 48 | Jupytab allows you to **explore in Tableau data which is generated 49 | dynamically by a Jupyter Notebook**. You can thus create Tableau data 50 | sources in a very flexible way using all the power of Python. 51 | This is achieved by having Tableau access data through a **web 52 | server created by Jupytab**. 53 | 54 | This package is an helper package to split dependencies between 55 | Jupytab server and your notebook. It only requires pandas. 56 | 57 | The full documentation is available on the project's home page. 58 | """ 59 | 60 | # This call to setup() does all the work 61 | setup( 62 | cmdclass=dict(make_version=MakeVersionCommand), 63 | name="jupytab", 64 | version=read_version(), 65 | description="Jupytab package to be used in notebooks", 66 | long_description_content_type="text/markdown", 67 | long_description=README, 68 | url="https://github.com/CFMTech/Jupytab", 69 | author="Brian Tribondeau", 70 | author_email="brian.tribondeau@cfm.fr", 71 | license="MIT", 72 | keywords="jupytab jupyter notebook wdc tableau", 73 | classifiers=[ 74 | "License :: OSI Approved :: MIT License", 75 | "Programming Language :: Python :: 3", 76 | "Programming Language :: Python :: 3.7", 77 | ], 78 | packages=find_packages(exclude=["tests", "env", "docs", "build"]), 79 | python_requires='>=3.6', 80 | include_package_data=True, 81 | install_requires=["pandas"], 82 | project_urls={ 83 | "Bug Tracker": "https://github.com/CFMTech/Jupytab/issues", 84 | "Documentation": "https://github.com/CFMTech/Jupytab", 85 | "Source Code": "https://github.com/CFMTech/Jupytab" 86 | } 87 | ) 88 | -------------------------------------------------------------------------------- /jupytab/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CFMTech/Jupytab/bdc2abf6eae2bafe73b75de18aa7f784d1161096/jupytab/tests/__init__.py -------------------------------------------------------------------------------- /jupytab/tests/test_dataframe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import json 7 | from timeit import default_timer as timer 8 | 9 | import jupytab 10 | 11 | 12 | def test_data_schema(): 13 | arrays = [ 14 | ['A', 'A', 15 | 'a', 'a', 16 | 0, 0, 17 | 'a$_!#àz', 'a$_!#àz' 18 | ], 19 | ['A', 'A', 20 | 0, 1, 21 | 'z$_"_àéça"', 'z_èà[|]a', 22 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 'abcdefghijklmnopqrstuvwxyz0123456789' 23 | ] 24 | ] 25 | tuples = list(zip(*arrays)) 26 | index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second']) 27 | complex_df = pd.DataFrame(np.random.randn(len(index), len(index)), index=index, columns=index) 28 | 29 | tables = jupytab.Tables() 30 | tables['complex_df_no_index_{}[]#!'] = \ 31 | jupytab.DataFrameTable('A multi-index Dataframe ({}[]#!)', 32 | dataframe=complex_df) 33 | tables['complex_df_with_index_{}[]#!'] = \ 34 | jupytab.DataFrameTable('A multi-index Dataframe ({}[]#!)', 35 | dataframe=complex_df, 36 | include_index=True) 37 | 38 | schema = tables.schema() 39 | 40 | assert schema[0]['id'] == 'complex_df_no_index_{}[]#!' 41 | assert schema[0]['alias'] == 'A multi-index Dataframe ({}[]#!)' 42 | assert schema[1]['id'] == 'complex_df_with_index_{}[]#!' 43 | assert schema[1]['alias'] == 'A multi-index Dataframe ({}[]#!)' 44 | 45 | raw_output = '[{"id": "complex_df_no_index_{}[]#!", "alias": "A multi-index Dataframe ({}[]#!' \ 46 | ')", "columns": [{"id": "A_A_1", "dataType": "float"}, {"id": "A_A_2", "dataType' \ 47 | '": "float"}, {"id": "a_0", "dataType": "float"}, {"id": "a_1", "dataType": "flo' \ 48 | 'at"}, {"id": "0_z____aeca_", "dataType": "float"}, {"id": "0_z_ea_a", "dataType' \ 49 | '": "float"}, {"id": "a___az_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", "dataType": ' \ 50 | '"float"}, {"id": "a___az_abcdefghijklmnopqrstuvwxyz0123456789", "dataType": "fl' \ 51 | 'oat"}]}, {"id": "complex_df_with_index_{}[]#!", "alias": "A multi-index Datafra' \ 52 | 'me ({}[]#!)", "columns": [{"id": "first_", "dataType": "string"}, {"id": "secon' \ 53 | 'd_", "dataType": "string"}, {"id": "A_A_1", "dataType": "float"}, {"id": "A_A_2' \ 54 | '", "dataType": "float"}, {"id": "a_0", "dataType": "float"}, {"id": "a_1", "dat' \ 55 | 'aType": "float"}, {"id": "0_z____aeca_", "dataType": "float"}, {"id": "0_z_ea_a' \ 56 | '", "dataType": "float"}, {"id": "a___az_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", ' \ 57 | '"dataType": "float"}, {"id": "a___az_abcdefghijklmnopqrstuvwxyz0123456789", "da' \ 58 | 'taType": "float"}]}]' 59 | 60 | raw_schema = tables.render_schema(do_print=False) 61 | 62 | assert raw_output == raw_schema 63 | 64 | 65 | def test_large_data_content(): 66 | row_count = 1000000 67 | col_count = 10 68 | 69 | np.random.seed(0) 70 | 71 | large_df = pd.DataFrame(np.random.randn(row_count, col_count)) 72 | tables = jupytab.Tables() 73 | tables['large_df'] = \ 74 | jupytab.DataFrameTable('A very large Dataframe', 75 | dataframe=large_df) 76 | 77 | request = json.dumps({ 78 | 'args': { 79 | 'table_name': ['large_df'], 80 | 'format': ['json'], 81 | 'from': [5100], 82 | 'to': [5102] 83 | } 84 | }) 85 | 86 | start = timer() 87 | raw_data = tables.render_data(request, do_print=False) 88 | end = timer() 89 | 90 | print(f"Elapsed time in second to retrieve one row in a large dataframe : {(end - start)} s") 91 | 92 | assert (end - start) < 0.1 93 | 94 | print(raw_data) 95 | 96 | assert raw_data == '[{"0":0.2307805099,"1":0.7823326556,"2":0.9507107694,"3":1.4595805778,' \ 97 | '"4":0.6798091111,"5":-0.8676077457,"6":0.3908489554,"7":1.0838125793,' \ 98 | '"8":0.6227587338,"9":0.0919146565},{"0":0.6267312321,"1":0.7369835911,' \ 99 | '"2":-0.4665488934,"3":1.5379716957,"4":-1.0313145219,"5":1.0398963231,' \ 100 | '"6":0.8687854819,"7":0.2055855947,"8":-1.7716643336,"9":0.2428264886}]' 101 | -------------------------------------------------------------------------------- /jupytab/tests/test_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from jupytab.function import Functions, Function 4 | 5 | 6 | def test_function(): 7 | def no_arg_function(): 8 | return True 9 | 10 | def check_eq_10(arg_1): 11 | return arg_1 == 10 12 | 13 | def product(arg_1, arg_2): 14 | return arg_1 * arg_2 15 | 16 | def multi_add(*args): 17 | return sum(*args) 18 | 19 | f_no_arg = Function("No arguments", no_arg_function) 20 | f_check_eq_10 = Function("Check value equals 10", check_eq_10) 21 | f_product = Function("Product of two arguments", product) 22 | f_multi_add = Function("Sum of all arguments", multi_add) 23 | 24 | assert f_no_arg() 25 | assert not f_check_eq_10(5) 26 | assert f_check_eq_10(10) 27 | assert f_product(3, 5) == 15 28 | assert f_multi_add(range(1, 10)) == 45 29 | 30 | 31 | def test_multi_add(): 32 | def multi_add(*args): 33 | return sum(args) 34 | 35 | funcs = Functions() 36 | funcs['MultiAdd'] = Function("Sum of all arguments", multi_add) 37 | 38 | request = json.dumps({ 39 | "body": { 40 | "function": "MultiAdd", 41 | "data": { 42 | "_arg1": list(range(1, 10)), 43 | "_arg2": list(range(1, 10)) 44 | } 45 | } 46 | }) 47 | 48 | x = funcs.render_evaluate(request, do_print=False) 49 | assert x == "[2, 4, 6, 8, 10, 12, 14, 16, 18]" 50 | 51 | 52 | def test_typed_method(): 53 | def typed_method(a_int, a_float, a_string, a_bool): 54 | check_type = \ 55 | isinstance(a_int, int) and \ 56 | isinstance(a_float, float) and \ 57 | isinstance(a_string, str) and \ 58 | isinstance(a_bool, bool) 59 | 60 | return check_type 61 | 62 | funcs = Functions() 63 | funcs['TypedMethod'] = Function("Typed method", typed_method) 64 | 65 | request = json.dumps({ 66 | "body": { 67 | "function": "TypedMethod", 68 | "data": { 69 | "_arg4": [True], 70 | "_arg2": [2.5], 71 | "_arg3": ["Test"], 72 | "_arg1": [10] 73 | } 74 | } 75 | }) 76 | 77 | x = funcs.render_evaluate(request, do_print=False) 78 | assert x == "[true]" 79 | -------------------------------------------------------------------------------- /jupytab/tests/test_util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Capital Fund Management 2 | # SPDX-License-Identifier: MIT 3 | 4 | import os 5 | 6 | import pandas as pd 7 | import jupytab 8 | 9 | THIS_DIR = os.path.abspath(os.path.dirname(__file__)) 10 | RESOURCES = os.path.join(THIS_DIR, 'resources') 11 | 12 | 13 | def test_data_schema(): 14 | crime_df = pd.read_csv(os.path.join(RESOURCES, 'sacramento_crime.csv')) 15 | realestate_df = pd.read_csv(os.path.join(RESOURCES, 'sacramento_realestate.csv')) 16 | 17 | tables = jupytab.Tables() 18 | tables['sacramento_crime'] = \ 19 | jupytab.DataFrameTable('Sacramento Crime', dataframe=crime_df) 20 | tables['sacramento_realestate'] = \ 21 | jupytab.DataFrameTable('Sacramento RealEstate', dataframe=realestate_df) 22 | 23 | schema = tables.schema() 24 | 25 | assert schema[0]['id'] == 'sacramento_crime' 26 | assert schema[0]['alias'] == 'Sacramento Crime' 27 | columns = schema[0]['columns'] 28 | assert len(columns) == 9 29 | 30 | raw_output = '[{"id": "sacramento_crime", "alias": "Sacramento Crime", "columns": [{"id": "cdat\ 31 | etime", "dataType": "string"}, {"id": "address", "dataType": "string"}, {"id": "district", "dataTyp\ 32 | e": "int"}, {"id": "beat", "dataType": "string"}, {"id": "grid", "dataType": "int"}, {"id": "crimed\ 33 | escr", "dataType": "string"}, {"id": "ucr_ncic_code", "dataType": "int"}, {"id": "latitude", "dataT\ 34 | ype": "float"}, {"id": "longitude", "dataType": "float"}]}, {"id": "sacramento_realestate", "alias"\ 35 | : "Sacramento RealEstate", "columns": [{"id": "street", "dataType": "string"}, {"id": "city", "data\ 36 | Type": "string"}, {"id": "zip", "dataType": "int"}, {"id": "state", "dataType": "string"}, {"id": "\ 37 | beds", "dataType": "int"}, {"id": "baths", "dataType": "int"}, {"id": "sq__ft", "dataType": "int"},\ 38 | {"id": "type", "dataType": "string"}, {"id": "sale_date", "dataType": "string"}, {"id": "price", "\ 39 | dataType": "int"}, {"id": "latitude", "dataType": "float"}, {"id": "longitude", "dataType": "float"\ 40 | }]}]' 41 | 42 | assert raw_output == tables.render_schema(do_print=False) 43 | 44 | 45 | def test_clean_column_name(): 46 | assert jupytab.DataFrameTable\ 47 | .clean_column_name(['abéçpo$ù"', 0, 'AaZz_#"\\']) == "_abecpo_u_0_AaZz__" 48 | 49 | 50 | def test_replace_duplicated_column_name(): 51 | assert jupytab.DataFrameTable.replace_duplicated_column_name(['A', 'A', 'a', 'z', 'a', 'Y']) \ 52 | == ['A_1', 'A_2', 'a_1', 'z', 'a_2', 'Y'] 53 | 54 | 55 | def test_version(): 56 | assert jupytab.__version__ is not None 57 | --------------------------------------------------------------------------------