diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..10c9e08d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,124 @@
+# local env files
+.env.local
+.env.*.local
+
+# OS files
+.DS_Store
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+dist/
+downloads/
+eggs/
+.eggs/
+sdist/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..93f59b8a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2022 Kitware Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..b36543b9
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include trame/LICENSE
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..5a6dfb1f
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,140 @@
+trame: simple, powerful, innovative
+===========================================================
+
+**trame** - a web framework that weaves together open source components into customized visual analytics easily.
+
+**trame** is French for
+
+* the core that ties things together
+* a guide providing the essence of a task
+
+.. image:: https://kitware.github.io/trame/examples/MultiFilter.jpg
+ :alt: Welcome to trame and 3D visualization
+
+With **trame**, create stunning, interactive web applications compactly and intuitively.
+
+|image_1| |image_2| |image_3|
+
+.. |image_1| image:: https://kitware.github.io/trame/examples/CarotidFlow.jpg
+ :width: 30%
+.. |image_2| image:: https://kitware.github.io/trame/examples/UberPickupsNYC.jpg
+ :width: 30%
+.. |image_3| image:: https://kitware.github.io/trame/examples/FiniteElementAnalysis.jpg
+ :width: 30%
+
+3D Visualization
+-----------------------------------------------------------
+
+With best-in-class VTK and ParaView platforms at its core, **trame** provides complete control of 3D visualizations and data movements.
+Developers benefit from a write-once environment while **trame** simply exposes both local and remote rendering through a single method.
+
+Rich Features
+-----------------------------------------------------------
+
+**trame** leverages existing libraries and tools such as Vuetify, Altair, Vega, deck.gl, VTK, ParaView, and more, to create vivid content for visual analytics applications.
+
+Problem Focused
+-----------------------------------------------------------
+
+By relying simply on Python, **trame** focuses on one's data and associated analysis and visualizations while hiding the complications of web app development.
+
+Desktop to cloud
+-----------------------------------------------------------
+
+The resulting **trame** applications can act as local desktop applications or remote cloud applications both accessed through a browser.
+
+
+Installing
+-----------------------------------------------------------
+
+trame can be installed with `pip `_:
+
+.. code-block:: bash
+
+ pip install --upgrade trame --pre
+
+Usage
+-----------------------------------------------------------
+
+The `Trame Tutorial `_ is the place to go to learn how to use the library and start building your own application.
+
+The `API Reference `_ documentation provides API-level documentation.
+
+
+License
+-----------------------------------------------------------
+
+trame is made available under the Apache License, Version 2.0. For more details, see `LICENSE `_
+
+
+Community
+-----------------------------------------------------------
+
+`Trame `_ | `Discussions `_ | `Issues `_ | `RoadMap `_ | `Contact Us `_
+
+.. image:: https://zenodo.org/badge/410108340.svg
+ :target: https://zenodo.org/badge/latestdoi/410108340
+
+
+Enjoying trame?
+-----------------------------------------------------------
+
+Share your experience `with a testimonial `_ or `with a brand approval `_.
+
+
+Optional dependencies
+-----------------------------------------------------------
+
+When installing trame using pip (`pip install trame`) you will get the core infrastructure for any trame application to work but more advanced usage may require additional dependencies.
+The list below capture which may need to add depending on your usage:
+
+* **pywebview** : Needed for desktop usage (--app)
+* **jupyterlab** : Needed to run inside jupyter-lab
+* **notebook** : Needed to run inside jupyter-notebook
+* **requests** : Needed when using remote assets such as GDrive files
+
+
+Environments variables
+-----------------------------------------------------------
+
+* **TRAME_LOG_NETWORK** : Path to log file for capturing network exchange. (default: None)
+* **TRAME_WS_MAX_MSG_SIZE** : Maximum size in bytes of any ws message. (default: 10MB)
+* **TRAME_WS_HEART_BEAT** : Time in second before assuming the server is non-responsive. (default: 30s)
+
+
+Life cycle callbacks
+--------------------------------------------------------------------------
+
+Life cycle events are directly managed on the application controller
+and are prefixed with `on_*`.
+
+* **on_server_ready** : All protocols initialized and available for client to connect
+* **on_client_connected** : Connection established to server
+* **on_client_exited** : Linked to browser "beforeunload" event
+* **on_server_exited** : Trame is exiting its event loop
+
+* **on_server_reload** : If callback registered it is use for reloading server side modules
+
+
+Reserved state entries
+--------------------------------------------------------------------------
+
+The shared state allow us to synchronize the server with the client.
+Rather than creating another mechanism to handle similar needs throughout
+the application we purposely reuse that state for internal purpose.
+To prevent any conflict with any user we are prefixing our internal
+variable with `trame__*`. In general those state values should not be use
+or changed by the user except for the one listed below:
+
+Read/Write:
+ - **trame__favicon**: Update it to replace the displayed favicon in your
+ browser. The content needs to be a image encoded url.
+ - **trame__title**: Update it to replace your page title
+ (tab name / window name).
+
+Read-only:
+ - **trame__busy**: Provide information if we have pending request waiting
+ for the server to respond.
+ - **tts**: Template Time Stamp to regenerate sub elements when a template
+ get's updated. Usually used as `:key="tts"` to force some component
+ rebuild.
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..e857f6e2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,39 @@
+[metadata]
+name = trame
+version = 2.0.0rc3
+description = Trame, a framework to build applications in plain Python
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = Kitware Inc.
+license = Apache License 2.0
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Environment :: Web Environment
+ License :: OSI Approved :: Apache Software License
+ Natural Language :: English
+ Operating System :: OS Independent
+ Programming Language :: Python :: 3 :: Only
+ Topic :: Software Development :: Libraries :: Application Frameworks
+ Topic :: Software Development :: Libraries :: Python Modules
+keywords =
+ Python
+ Interactive
+ Web
+ Application
+ Framework
+
+[options]
+packages = find:
+include_package_data = True
+install_requires =
+ trame-client
+ trame-components
+ trame-deckgl
+ trame-markdown
+ trame-matplotlib
+ trame-plotly
+ trame-router
+ trame-server
+ trame-vega
+ trame-vtk
+ trame-vuetify
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..60684932
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()
diff --git a/trame/LICENSE b/trame/LICENSE
new file mode 120000
index 00000000..ea5b6064
--- /dev/null
+++ b/trame/LICENSE
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/trame/__init__.py b/trame/__init__.py
new file mode 100644
index 00000000..4dc06ec0
--- /dev/null
+++ b/trame/__init__.py
@@ -0,0 +1,3 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
+
+__license__ = "Apache License 2.0"
diff --git a/trame/app/__init__.py b/trame/app/__init__.py
new file mode 100644
index 00000000..9ac5eaa9
--- /dev/null
+++ b/trame/app/__init__.py
@@ -0,0 +1,27 @@
+from trame_server import Server
+from trame_client import module
+
+DEFAULT_NAME = "trame"
+AVAILABLE_SERVERS = {}
+
+
+def get_server(name=None, create_if_missing=True, **kwargs):
+ """
+ Return a server for serving trame applications.
+ If a name is given and such server is not available yet,
+ it will be created otherwise the previsouly created instance will be returned.
+ """
+ if name is None:
+ name = DEFAULT_NAME
+
+ if name in AVAILABLE_SERVERS:
+ return AVAILABLE_SERVERS[name]
+
+ if create_if_missing:
+ server = Server(name, **kwargs)
+ server.enable_module(module) # Always load html module first
+ AVAILABLE_SERVERS[name] = server
+ return server
+
+ # No server available for given name
+ return None
diff --git a/trame/app/asynchronous.py b/trame/app/asynchronous.py
new file mode 100644
index 00000000..4fdb3afa
--- /dev/null
+++ b/trame/app/asynchronous.py
@@ -0,0 +1,15 @@
+from trame_server.utils.asynchronous import (
+ create_task,
+ decorate_task,
+ create_state_queue_monitor_task,
+ StateQueue,
+ task,
+)
+
+__all__ = [
+ "create_task",
+ "decorate_task",
+ "create_state_queue_monitor_task",
+ "StateQueue",
+ "task",
+]
diff --git a/trame/app/dev.py b/trame/app/dev.py
new file mode 100644
index 00000000..8ea6eaf7
--- /dev/null
+++ b/trame/app/dev.py
@@ -0,0 +1,29 @@
+def clear_triggers(server):
+ names = list(server._triggers.keys())
+ for name in names:
+ fn = server._triggers.pop(name)
+ server._triggers_fn2name.pop(fn)
+ print(f"unregister trigger {name}")
+
+
+def clear_change_listeners(server):
+ server._change_callbacks.clear()
+
+
+def remove_change_listeners(server, *names):
+ for name in names:
+ if name in server._change_callbacks:
+ server._change_callbacks.pop(name)
+
+
+def reload(*reload_list):
+ """
+ Helper function use to reload python modules that were passed as
+ arguments.
+
+ :param reload_list: positional arguments of the modules to reload when the
+ reload button is pressed.
+ :type reload_list: python modules
+ """
+ for m in reload_list:
+ m.__loader__.exec_module(m)
diff --git a/trame/app/jupyter.py b/trame/app/jupyter.py
new file mode 100644
index 00000000..16cb8dee
--- /dev/null
+++ b/trame/app/jupyter.py
@@ -0,0 +1,56 @@
+import asyncio
+from trame.app import get_server
+from IPython import display
+from trame_server.utils.asynchronous import handle_task_result
+
+
+def show(_server, ui=None, **kwargs):
+ if isinstance(_server, str):
+ _server = get_server(_server)
+
+ def on_ready(**_):
+ params = f"?ui={ui}" if ui else ""
+ src = f"{kwargs.get('protocol', 'http')}://{kwargs.get('host', 'localhost')}:{_server.port}/index.html{params}"
+ loop = asyncio.get_event_loop()
+ loop.call_later(0.1, lambda: display_iframe(src, **kwargs))
+ _server.controller.on_server_ready.discard(on_ready)
+
+ if _server._running_stage == 0:
+ _server.controller.on_server_ready.add(on_ready)
+ _server.start(
+ exec_mode="task",
+ port=0,
+ open_browser=False,
+ show_connection_info=False,
+ disableLogging=True,
+ timeout=0,
+ )
+ elif _server._running_stage == 1:
+ _server.controller.on_server_ready.add(on_ready)
+ elif _server._running_stage == 2:
+ on_ready()
+
+
+def display_iframe(src, **kwargs):
+ """Convenience method to display an iframe for the given url source"""
+
+ # Set some defaults. The kwargs can override these.
+ # width and height are both required.
+ iframe_kwargs = {
+ "width": "100%",
+ "height": 600,
+ **kwargs,
+ }
+ iframe = display.IFrame(src=src, **iframe_kwargs)
+ return display.display(iframe)
+
+
+def run(name, **kwargs):
+ """Run and display a Jupyter server proxy process with the given name
+
+ Note that the proxy process must be registered with Jupyter by setting
+ the `jupyter_serverproxy_servers` entrypoint in its setup.py or setup.cfg
+ file.
+ """
+ src = f"/{name}"
+ return display_iframe(src, **kwargs)
diff --git a/trame/app/singleton.py b/trame/app/singleton.py
new file mode 100644
index 00000000..87aada53
--- /dev/null
+++ b/trame/app/singleton.py
@@ -0,0 +1,13 @@
+from typing import Type, TypeVar, Generic
+
+T = TypeVar("T")
+
+
+class Singleton(Generic[T]):
+ """Singleton decorator"""
+
+ def __init__(self, cls: Type[T]):
+ self._instance: T = cls()
+
+ def __call__(self) -> T:
+ return self._instance
diff --git a/trame/assets/__init__.py b/trame/assets/__init__.py
new file mode 100644
index 00000000..8db66d3d
--- /dev/null
+++ b/trame/assets/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/trame/assets/local.py b/trame/assets/local.py
new file mode 100644
index 00000000..26be885c
--- /dev/null
+++ b/trame/assets/local.py
@@ -0,0 +1,97 @@
+import base64
+import mimetypes
+from pathlib import Path
+
+mimetypes.init()
+
+
+def to_mime(file_path):
+ return mimetypes.guess_type(file_path)[0]
+
+
+def to_txt(full_path):
+ with open(full_path) as txt_file:
+ return str(txt_file.read())
+
+
+def to_base64(full_path):
+ with open(full_path, "rb") as bin_file:
+ return base64.b64encode(bin_file.read()).decode("ascii")
+
+
+def to_url(full_path):
+ encoded = to_base64(full_path)
+ mime = to_mime(full_path)
+ return f"data:{mime};base64,{encoded}"
+
+
+class LocalFileManager:
+ def __init__(self, base_path):
+ _base = Path(base_path)
+
+ # Ensure directory
+ if _base.is_file():
+ _base = _base.parent
+
+ self._root = Path(str(_base.resolve().absolute()))
+ self._assests = {}
+
+ def __getitem__(self, name):
+ return self._assests.get(name)
+
+ def __getattr__(self, name):
+ return self._assests.get(name)
+
+ def _to_path(self, file_path):
+ _input_file = Path(file_path)
+ if _input_file.is_absolute():
+ return str(_input_file.resolve().absolute())
+
+ return str(self._root.joinpath(file_path).resolve().absolute())
+
+ def base64(self, key, file_path=None):
+ if file_path is None:
+ file_path, key = key, file_path
+
+ data = to_base64(self._to_path(file_path))
+
+ if key is not None:
+ self._assests[key] = data
+
+ return data
+
+ def url(self, key, file_path):
+ if file_path is None:
+ file_path, key = key, file_path
+
+ data = to_url(self._to_path(file_path))
+
+ if key is not None:
+ self._assests[key] = data
+
+ return data
+
+ def txt(self, key, file_path):
+ if file_path is None:
+ file_path, key = key, file_path
+
+ data = to_txt(self._to_path(file_path))
+
+ if key is not None:
+ self._assests[key] = data
+
+ return data
+
+ @property
+ def assets(self):
+ return self._assests
+
+ def get_assets(self, *keys):
+ if len(keys) == 0:
+ return self.assets
+
+ _assets = {}
+ for key in keys:
+ _assets[key] = self._assests.get(key)
+
+ return _assets
diff --git a/trame/assets/remote.py b/trame/assets/remote.py
new file mode 100644
index 00000000..e563efe7
--- /dev/null
+++ b/trame/assets/remote.py
@@ -0,0 +1,102 @@
+import os
+from urllib.error import HTTPError
+from urllib.request import urlretrieve
+
+
+def download_file_from_google_drive(id, destination):
+ import requests
+
+ URL = "https://docs.google.com/uc"
+
+ session = requests.Session()
+ response = session.get(
+ URL, params={"id": id, "confirm": "t", "export": "download"}, stream=True
+ )
+ token = get_confirm_token(response)
+
+ if token:
+ params = {"id": id, "confirm": token}
+ response = session.get(URL, params=params, stream=True)
+
+ save_response_content(response, destination)
+
+
+def get_confirm_token(response):
+ for key, value in response.cookies.items():
+ if key.startswith("download_warning"):
+ return value
+
+ return None
+
+
+def save_response_content(response, destination):
+ CHUNK_SIZE = 32768
+
+ with open(destination, "wb") as f:
+ for chunk in response.iter_content(CHUNK_SIZE):
+ if chunk: # filter out keep-alive new chunks
+ f.write(chunk)
+
+
+class AbstractRemoteFile:
+ def __init__(self, local_path=None, local_base=None):
+ # setup base path
+ self._base = os.getcwd()
+ if local_base is not None:
+ if os.path.exists(local_base):
+ if os.path.isfile(local_base):
+ self._base = os.path.abspath(os.path.dirname(local_base))
+ else:
+ self._base = os.path.abspath(local_base)
+ else:
+ self._base = os.path.abspath(local_base)
+
+ # setup local path
+ self._file_path = local_path
+ if not os.path.isabs(local_path):
+ self._file_path = os.path.abspath(os.path.join(self._base, local_path))
+
+ # Make sure target directory exists
+ parent_dir = os.path.dirname(self._file_path)
+ if not os.path.exists(parent_dir):
+ os.makedirs(parent_dir)
+
+ @property
+ def local(self):
+ return os.path.exists(self._file_path)
+
+ def fetch(self):
+ pass
+
+ @property
+ def path(self):
+ if not self.local:
+ self.fetch()
+
+ return self._file_path
+
+
+class GoogleDriveFile(AbstractRemoteFile):
+ def __init__(self, local_path=None, google_id=None, local_base=None):
+ super().__init__(local_path, local_base)
+ self._gid = google_id
+
+ def fetch(self):
+ try:
+ print(f"Downloading:\n - {self._gid}\n - to {self._file_path}")
+ download_file_from_google_drive(self._gid, self._file_path)
+ except HTTPError as e:
+ print(RuntimeError(f"Failed to download {self._gid}. {e.reason}"))
+
+
+class HttpFile(AbstractRemoteFile):
+ def __init__(self, local_path=None, remote_url=None, local_base=None):
+ super().__init__(local_path, local_base)
+ self._url = remote_url
+
+ def fetch(self):
+ try:
+ print(f"Downloading:\n - {self._url}\n - to {self._file_path}")
+ urlretrieve(self._url, self._file_path)
+ except HTTPError as e:
+ print(RuntimeError(f"Failed to download {self._url}. {e.reason}"))
diff --git a/trame/modules/__init__.py b/trame/modules/__init__.py
new file mode 100644
index 00000000..8db66d3d
--- /dev/null
+++ b/trame/modules/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/trame/tools/__init__.py b/trame/tools/__init__.py
new file mode 100644
index 00000000..8db66d3d
--- /dev/null
+++ b/trame/tools/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/trame/tools/www.py b/trame/tools/www.py
new file mode 100644
index 00000000..94c308c2
--- /dev/null
+++ b/trame/tools/www.py
@@ -0,0 +1,30 @@
+import argparse
+import importlib
+from trame.app import get_server
+
+
+def enable_modules(_server, *names):
+ for module_name in names:
+ m = importlib.import_module(f"trame.modules.{module_name}")
+ _server.enable_module(m)
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Client generator for trame")
+
+ parser.add_argument(
+ "--output",
+ help="Directory to fill with trame client code",
+ required=True,
+ )
+
+ args, module_names = parser.parse_known_args()
+
+ server = get_server("www-generator")
+ enable_modules(server, "www")
+ enable_modules(server, *module_names)
+ server.write_www(args.output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/trame/ui/__init__.py b/trame/ui/__init__.py
new file mode 100644
index 00000000..5284146e
--- /dev/null
+++ b/trame/ui/__init__.py
@@ -0,0 +1 @@
+__import__("pkg_resources").declare_namespace(__name__)
diff --git a/trame/widgets/__init__.py b/trame/widgets/__init__.py
new file mode 100644
index 00000000..8db66d3d
--- /dev/null
+++ b/trame/widgets/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__("pkgutil").extend_path(__path__, __name__)