├── tests
├── __init__.py
├── unit_tests
│ ├── __init__.py
│ ├── test_webviz_settings.py
│ ├── test_webviz_instance_info.py
│ └── test_webviz_factory_registry.py
├── data
│ ├── example_data.csv
│ └── basic_example.yaml
├── conftest.py
├── test_schema.py
├── test_plugin_metadata.py
├── test_syntax_highlighter.py
├── test_example_wlf_plugin.py
├── test_example_plugin.py
├── test_portable.py
├── test_data_table.py
├── test_docker_setup.py
├── test_plugin_init.py
├── test_docstring.py
└── test_table_plotter.py
├── webviz_config
├── _docs
│ ├── __init__.py
│ ├── static
│ │ ├── webviz-doc.css
│ │ ├── index.html
│ │ └── README.md
│ └── open_docs.py
├── generic_plugins
│ ├── __init__.py
│ ├── _example_wlf_plugin
│ │ ├── _views
│ │ │ ├── __init__.py
│ │ │ ├── _table
│ │ │ │ └── __init__.py
│ │ │ └── _plot
│ │ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── _shared_settings
│ │ │ ├── __init__.py
│ │ │ └── _shared_settings.py
│ │ ├── _shared_view_elements
│ │ │ ├── __init__.py
│ │ │ └── _text_view_element.py
│ │ └── _plugin.py
│ ├── _example_assets.py
│ ├── _example_tour.py
│ ├── _example_plugin.py
│ ├── _example_data_download.py
│ ├── _embed_pdf.py
│ ├── _syntax_highlighter.py
│ ├── _example_portable.py
│ ├── _pivot_table.py
│ ├── _banner_image.py
│ └── _data_table.py
├── _deployment
│ ├── __init__.py
│ ├── radix_configuration.py
│ ├── azure_configuration.py
│ ├── interactive_terminal.py
│ ├── radix_cli.py
│ └── github_cli.py
├── static
│ ├── .dockerignore
│ ├── .gitignore
│ ├── assets
│ │ ├── webviz_config.css
│ │ └── webviz_layout.css
│ └── README.md
├── _dockerize
│ ├── __init__.py
│ ├── _pip_git_url.py
│ └── _create_docker_setup.py
├── py.typed
├── __main__.py
├── utils
│ ├── terminal_colors.py
│ ├── _str_enum.py
│ ├── __init__.py
│ ├── _dash_component_utils.py
│ ├── _deprecate_webviz_settings_attribute_in_dash_app.py
│ ├── _silence_flask_startup.py
│ ├── _available_port.py
│ ├── _localhost_open_browser.py
│ └── _callback_typecheck.py
├── webviz_plugin_subclasses
│ ├── __init__.py
│ ├── _layout_base_abc.py
│ ├── _settings_group_abc.py
│ └── _layout_unique_id.py
├── testing
│ ├── _webviz_ids.py
│ ├── __init__.py
│ ├── _plugin.py
│ └── _composite.py
├── templates
│ ├── sidebar.md.jinja2
│ ├── README.md.jinja2
│ ├── plugin_deprecations.md.jinja2
│ ├── argument_deprecations.md.jinja2
│ ├── webviz-doc.js.jinja2
│ ├── feedback.md.jinja2
│ ├── plugin_docs.md.jinja2
│ ├── Dockerfile.jinja2
│ ├── radixconfig.yaml.jinja2
│ └── copy_data_template.py.jinja2
├── _user_data_dir.py
├── _is_reload_process.py
├── themes
│ ├── _default_theme.py
│ └── __init__.py
├── __init__.py
├── webviz_factory.py
├── plugins
│ ├── __init__.py
│ └── _utils.py
├── common_cache.py
├── _webviz_settings_class.py
├── _user_preferences.py
├── _write_script.py
├── _shared_settings_subscriptions.py
├── _deprecation_store.py
├── deprecation_decorators.py
├── _localhost_token.py
├── webviz_instance_info.py
├── webviz_factory_registry.py
├── webviz_assets.py
└── _build_webviz.py
├── pytest.ini
├── bandit.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
├── pull_request_template.md
└── workflows
│ └── webviz-config.yml
├── examples
├── example_stylesheet.css
├── example_javascript.js
├── example.pdf
├── example_banner.png
├── example_data.csv
├── demo_portable.yaml
├── basic_example_wlf.yaml
├── example-markdown.md
├── basic_example.yaml
└── basic_example_advanced_menu.yaml
├── assets
├── high-level-overview.png
├── before-after-settings.png
├── webviz-layout-overview.png
├── before-after-tabs-views.png
└── before-after-plugin-actions.png
├── .gitignore
├── .pylintrc
├── mypy.ini
├── SECURITY.md
├── package.json
├── LICENSE
├── CODE_OF_CONDUCT.md
└── setup.py
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit_tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webviz_config/_docs/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | testpaths = tests/
3 |
--------------------------------------------------------------------------------
/bandit.yml:
--------------------------------------------------------------------------------
1 | skips: ['B101', 'B113', 'B404', 'B603', 'B607']
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_views/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/example_stylesheet.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: green;
3 | }
4 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/__init__.py:
--------------------------------------------------------------------------------
1 | from .radix import main_radix_deployment
2 |
--------------------------------------------------------------------------------
/webviz_config/static/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | __pycache__
4 |
--------------------------------------------------------------------------------
/webviz_config/static/.gitignore:
--------------------------------------------------------------------------------
1 | resources/
2 | __pycache__/
3 | *.pyc
4 | *~
5 |
--------------------------------------------------------------------------------
/examples/example_javascript.js:
--------------------------------------------------------------------------------
1 | alert("This loaded JavaScript file comes from a plugin.");
2 |
--------------------------------------------------------------------------------
/examples/example.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/examples/example.pdf
--------------------------------------------------------------------------------
/webviz_config/_dockerize/__init__.py:
--------------------------------------------------------------------------------
1 | from ._create_docker_setup import create_docker_setup
2 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/__init__.py:
--------------------------------------------------------------------------------
1 | from ._plugin import ExampleWlfPlugin
2 |
--------------------------------------------------------------------------------
/webviz_config/py.typed:
--------------------------------------------------------------------------------
1 | # Marker file to indicate that this package supports typing, ref PEP 561
2 |
--------------------------------------------------------------------------------
/examples/example_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/examples/example_banner.png
--------------------------------------------------------------------------------
/assets/high-level-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/assets/high-level-overview.png
--------------------------------------------------------------------------------
/assets/before-after-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/assets/before-after-settings.png
--------------------------------------------------------------------------------
/assets/webviz-layout-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/assets/webviz-layout-overview.png
--------------------------------------------------------------------------------
/assets/before-after-tabs-views.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/assets/before-after-tabs-views.png
--------------------------------------------------------------------------------
/webviz_config/__main__.py:
--------------------------------------------------------------------------------
1 | if __name__ == "__main__":
2 | from webviz_config.command_line import main
3 |
4 | main()
5 |
--------------------------------------------------------------------------------
/assets/before-after-plugin-actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equinor/webviz-config/HEAD/assets/before-after-plugin-actions.png
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_shared_settings/__init__.py:
--------------------------------------------------------------------------------
1 | from ._shared_settings import SharedSettingsGroup
2 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_shared_view_elements/__init__.py:
--------------------------------------------------------------------------------
1 | from ._text_view_element import TextViewElement
2 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_views/_table/__init__.py:
--------------------------------------------------------------------------------
1 | from ._view import TableView, TableViewElement, TableViewSettingsGroup
2 |
--------------------------------------------------------------------------------
/examples/example_data.csv:
--------------------------------------------------------------------------------
1 | Well,Segment,Average permeability (D),Initial reservoir pressure (bar)
2 | A-1H,A,1.4,380
3 | A-2H,A,2.2,350
4 | A-3H,A,0.1,252
5 | B-1H,B,0.8,248
6 | C-1H,C,1.2,538
7 |
--------------------------------------------------------------------------------
/tests/data/example_data.csv:
--------------------------------------------------------------------------------
1 | Well,Segment,Average permeability (D),Initial reservoir pressure (bar)
2 | A-1H,A,1.4,380
3 | A-2H,A,2.2,350
4 | A-3H,A,0.1,252
5 | B-1H,B,0.8,248
6 | C-1H,C,1.2,538
7 |
--------------------------------------------------------------------------------
/webviz_config/utils/terminal_colors.py:
--------------------------------------------------------------------------------
1 | RED = "\x1b[37;41m"
2 | GREEN = "\x1b[37;42m"
3 | YELLOW = "\x1b[37;43m"
4 | BLUE = "\x1b[37;44m"
5 | PURPLE = "\x1b[37;45m"
6 |
7 | BOLD = "\x1b[1m"
8 | END = "\x1b[0m"
9 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_views/_plot/__init__.py:
--------------------------------------------------------------------------------
1 | from ._view import (
2 | PlotView,
3 | PlotViewElement,
4 | PlotViewSettingsGroup,
5 | PlotViewElementSettingsGroup,
6 | Kindness,
7 | )
8 |
--------------------------------------------------------------------------------
/webviz_config/webviz_plugin_subclasses/__init__.py:
--------------------------------------------------------------------------------
1 | from ._settings_group_abc import SettingsGroupABC
2 | from ._views import ViewABC, ViewElementABC, ViewLayoutElement, LayoutElementType
3 | from ._layout_base_abc import LayoutBaseABC
4 | from ._layout_unique_id import LayoutUniqueId
5 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Sets the window size of the browser (crucial in --headless mode).
2 | from selenium.webdriver.chrome.options import Options
3 |
4 |
5 | def pytest_setup_options():
6 | options = Options()
7 | options.add_argument("--window-size=1920,1080")
8 | return options
9 |
--------------------------------------------------------------------------------
/webviz_config/utils/_str_enum.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class StrEnum(str, Enum):
5 | def __repr__(self) -> str:
6 | return self._value_ # pylint: disable=no-member
7 |
8 | def __str__(self) -> str:
9 | return self._value_ # pylint: disable=no-member
10 |
--------------------------------------------------------------------------------
/webviz_config/testing/_webviz_ids.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class WebvizIds(str, Enum):
5 | LAYOUT_WRAPPER = "layoutWrapper"
6 | CONTENT_MANAGER = "webviz-content-manager"
7 | SETTINGS_DRAWER = "settings-drawer"
8 | PLUGINS_WRAPPER = "plugins-wrapper"
9 | SETTINGS_DRAWER_TOGGLE = ".WebvizSettingsDrawer__Toggle"
10 |
--------------------------------------------------------------------------------
/webviz_config/templates/sidebar.md.jinja2:
--------------------------------------------------------------------------------
1 | * [Introduction](/)
2 | {%- for package in packages %}
3 | * [{{package}} package]({{package}}.md)
4 | {%- endfor %}
5 | {% if deprecated_plugins %}
6 | * [Deprecated plugins](plugin_deprecations.md)
7 | {% endif %}
8 | {% if deprecated_arguments %}
9 | * [Deprecated arguments](argument_deprecations.md)
10 | {% endif %}
11 |
--------------------------------------------------------------------------------
/webviz_config/templates/README.md.jinja2:
--------------------------------------------------------------------------------
1 | # Plugin project {{ dist_name }}
2 |
3 | ?> :bookmark: This documentation is valid for version `{{ package_doc["dist_version"] }}` of `{{ dist_name}}`.
4 |
5 | {% if package_doc["doc"] is not none %}
6 | {{ package_doc["doc"] }}
7 | {% endif %}
8 |
9 | ---
10 | {% for plugin in package_doc["plugins"] %}
11 | {% include "plugin_docs.md.jinja2" %}
12 | {% endfor %}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .eggs
2 | *.egg-info
3 | *~
4 | .mypy_cache
5 | __pycache__
6 | node_modules
7 | venv
8 | .vscode
9 | .pytest_cache
10 | *.pyc
11 | .DS_Store
12 | dist
13 | build
14 | webviz_config/_docs/static/fonts
15 | webviz_config/_docs/static/INTRODUCTION.md
16 | webviz_config/_docs/static/*.js
17 | webviz_config/_docs/static/*.css
18 | !webviz_config/_docs/static/webviz-doc.js
19 | !webviz_config/_docs/static/webviz-doc.css
20 |
--------------------------------------------------------------------------------
/webviz_config/_user_data_dir.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pathlib import Path
3 |
4 |
5 | def user_data_dir() -> Path:
6 | """Returns platform specific path to store user application data"""
7 |
8 | if sys.platform == "win32":
9 | return Path.home() / "AppData" / "webviz"
10 |
11 | if sys.platform == "darwin":
12 | return Path.home() / "Library" / "Application Support" / "webviz"
13 |
14 | return Path.home() / ".local" / "share" / "webviz"
15 |
--------------------------------------------------------------------------------
/webviz_config/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from ._localhost_open_browser import LocalhostOpenBrowser
2 | from ._available_port import get_available_port
3 | from ._silence_flask_startup import silence_flask_startup
4 | from ._dash_component_utils import calculate_slider_step
5 | from ._deprecate_webviz_settings_attribute_in_dash_app import (
6 | deprecate_webviz_settings_attribute_in_dash_app,
7 | )
8 | from ._str_enum import StrEnum
9 | from ._callback_typecheck import callback_typecheck, ConversionError
10 |
--------------------------------------------------------------------------------
/examples/demo_portable.yaml:
--------------------------------------------------------------------------------
1 | # This file demonstrates the most basic usage of webviz in a FMU setting
2 | # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML).
3 |
4 | title: Reek Webviz Demonstration
5 |
6 | pages:
7 |
8 | - title: Front page
9 | content:
10 | - ExamplePortable:
11 | some_number: 42
12 | - ExampleAssets:
13 | picture_path: ./example_banner.png
14 | css_path: ./example_stylesheet.css
15 | js_path: ./example_javascript.js
16 |
--------------------------------------------------------------------------------
/webviz_config/templates/plugin_deprecations.md.jinja2:
--------------------------------------------------------------------------------
1 | {%- for dist_name, package_doc in packages.items() %}
2 | ### {{ dist_name }} package
3 |
4 | ?> :bookmark: This documentation is valid for version `{{ package_doc["dist_version"] }}` of `{{ dist_name}}`.
5 |
6 | {% if package_doc["doc"] is not none %}
7 | {{ package_doc["doc"] }}
8 | {% endif %}
9 |
10 | ---
11 | {% for plugin in package_doc["plugins"] if plugin["deprecated"]%}
12 | {% include "plugin_docs.md.jinja2" %}
13 | {% endfor %}
14 |
15 |
16 | {% endfor %}
17 |
--------------------------------------------------------------------------------
/webviz_config/templates/argument_deprecations.md.jinja2:
--------------------------------------------------------------------------------
1 | {%- for dist_name, package_doc in packages.items() %}
2 | ### {{ dist_name }} package
3 |
4 | ?> :bookmark: This documentation is valid for version `{{ package_doc["dist_version"] }}` of `{{ dist_name}}`.
5 |
6 | {% if package_doc["doc"] is not none %}
7 | {{ package_doc["doc"] }}
8 | {% endif %}
9 |
10 | ---
11 | {% for plugin in package_doc["plugins"] if plugin["has_deprecated_arguments"]%}
12 | {% include "plugin_docs.md.jinja2" %}
13 | {% endfor %}
14 |
15 | {% endfor %}
16 |
--------------------------------------------------------------------------------
/webviz_config/testing/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Generator
2 |
3 | # pylint: disable=too-few-public-methods
4 | class MissingWebvizTesting:
5 | def __init__(self, **kwargs: Any) -> None:
6 | raise RuntimeError(
7 | "webviz_config[tests] was not installed. "
8 | "Please install to use the webviz testing fixtures."
9 | )
10 |
11 |
12 | try:
13 | from ._composite import WebvizComposite
14 |
15 | except ImportError:
16 | WebvizComposite = MissingWebvizTesting # type: ignore
17 |
--------------------------------------------------------------------------------
/webviz_config/_is_reload_process.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def is_reload_process() -> bool:
5 | """Within the flask reload machinery, it is not straight forward to know
6 | if the code is run as the main process (i.e. the process the user directly
7 | started), or if the code is a "hot reload process" (see Flask
8 | documentation).
9 |
10 | This utility function will use the fact that the reload process
11 | sets an environment variable WERKZEUG_RUN_MAIN.
12 | """
13 |
14 | return os.environ.get("WERKZEUG_RUN_MAIN") == "true"
15 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # As a temporary workaround for https://github.com/PyCQA/pylint/issues/4577
4 | init-hook = "import astroid; astroid.context.InferenceContext.max_inferred = 500"
5 |
6 | [MESSAGES CONTROL]
7 |
8 | disable = bad-continuation, missing-docstring, duplicate-code, unspecified-encoding
9 | enable = useless-suppression
10 |
11 | [DESIGN]
12 |
13 | max-args = 7
14 | max-attributes = 12
15 | min-public-methods = 1
16 |
17 | [BASIC]
18 |
19 | # Variable names which should always be accepted
20 | good-names = i,
21 | df,
22 | _
23 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | *Insert a description of your pull request (PR) here, and check off the boxes below when they are done.*
2 |
3 | ---
4 |
5 | ### Contributor checklist
6 |
7 | - [ ] :tada: This PR closes #ISSUE_NUMBER.
8 | - [ ] :scroll: I have broken down my PR into the following tasks:
9 | - [ ] Task 1
10 | - [ ] Task 2
11 | - [ ] :robot: I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR.
12 | - [ ] :book: I have considered adding a new entry in `CHANGELOG.md`, and added it if should be communicated there.
13 |
--------------------------------------------------------------------------------
/webviz_config/_docs/static/webviz-doc.css:
--------------------------------------------------------------------------------
1 | .app-name-link > img {
2 | width: 120px;
3 | margin-bottom: 10px;
4 | }
5 |
6 | button.docsify-copy-code-button {
7 | border-radius: 5px;
8 | }
9 |
10 | button {
11 | font-family: inherit;
12 | }
13 |
14 | .plugin-doc {
15 | background-color: white;
16 | margin-top: 20px;
17 | margin-bottom: 20px;
18 | padding-left: 10px;
19 | padding-right: 10px;
20 | border: 1px solid rgb(240, 240, 240);
21 | border-radius: 3px;
22 | }
23 |
24 | .plugin-doc:hover {
25 | box-shadow: 5px 5px 10px 3px rgba(230, 230, 230);
26 | }
27 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | disallow_untyped_defs = True
3 | show_error_codes = True
4 | warn_unused_ignores = True
5 |
6 | # Temporarily allow implicit optional until pydantic handles JSON schema generation.
7 | # mypy >= 0.990 has changed its default to no_implicit_optional=True.
8 | # When removed - utilize the following make the code base implicit optional
9 | # type hints PEP 484 compliant:
10 | # https://github.com/hauntsaninja/no_implicit_optional
11 | implicit_optional = True
12 |
13 | # https://github.com/Azure/azure-sdk-for-python/issues/20771
14 | [mypy-azure.storage.blob.*]
15 | ignore_errors = True
16 |
--------------------------------------------------------------------------------
/webviz_config/themes/_default_theme.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import pathlib
3 |
4 | from plotly.io import templates
5 |
6 | from webviz_config import WebvizConfigTheme
7 |
8 | default_theme = WebvizConfigTheme(theme_name="default")
9 |
10 | default_theme.assets = glob.glob(
11 | str(pathlib.Path(__file__).resolve().parent / "default_assets" / "*")
12 | )
13 | default_theme.assets.append(
14 | str(
15 | pathlib.Path(__file__).resolve().parent.parent
16 | / "_docs"
17 | / "static"
18 | / "webviz-logo.svg"
19 | )
20 | )
21 |
22 | default_theme.plotly_theme = templates["plotly"].to_plotly_json()
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is.
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/webviz_config/__init__.py:
--------------------------------------------------------------------------------
1 | from importlib.metadata import version, PackageNotFoundError
2 |
3 | from ._theme_class import WebvizConfigTheme
4 | from ._webviz_settings_class import WebvizSettings
5 | from ._localhost_token import LocalhostToken
6 | from ._is_reload_process import is_reload_process
7 | from ._plugin_abc import WebvizPluginABC, EncodedFile, ZipFileMember
8 | from ._shared_settings_subscriptions import SHARED_SETTINGS_SUBSCRIPTIONS
9 | from .webviz_instance_info import WEBVIZ_INSTANCE_INFO
10 | from ._oauth2 import Oauth2
11 |
12 | try:
13 | __version__ = version("webviz-config")
14 | except PackageNotFoundError:
15 | # package is not installed
16 | pass
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **How to reproduce**
13 | Steps to reproduce the behavior:
14 | 1. Go to '...'
15 | 2. Click on '....'
16 | 3. Scroll down to '....'
17 | 4. See error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/webviz_config/templates/webviz-doc.js.jinja2:
--------------------------------------------------------------------------------
1 | window.$docsify = {
2 | logo: "./webviz-logo.svg",
3 | homepage: "INTRODUCTION.md",
4 | name: "Webviz",
5 | loadSidebar: "sidebar.md",
6 | subMaxLevel: 4,
7 | copyCode: {
8 | buttonText : "Copy",
9 | },
10 | tabs: {
11 | sync: false,
12 | theme: "material",
13 | tabHeadings: false
14 | },
15 | search: {
16 | paths: {{ paths | tojson }},
17 | depth: 6,
18 | hideOtherSidebarContent: true,
19 | maxAge: 30e3 // default cache maxage one day (8.64e7 ms) - old cached content can be confusing if user changes installed webviz plugins
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/basic_example_wlf.yaml:
--------------------------------------------------------------------------------
1 | # This file demonstrates the most basic usage of webviz in a FMU setting
2 | # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML).
3 |
4 | title: Reek Webviz Demonstration
5 |
6 | options:
7 | menu:
8 | show_logo: True
9 | bar_position: left
10 | drawer_position: left
11 | initially_pinned: True
12 |
13 | layout:
14 | - section: Section
15 | content:
16 | - page: Front page
17 | icon: home
18 | content:
19 | - ExampleWlfPlugin:
20 | title: Example Plugin
21 | contact_person:
22 | email: kari.nordmann@equinor.com
23 | name: Kari Nordmann
24 | phone: "000000000"
25 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_assets.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from dash import html
4 |
5 | from .. import WebvizPluginABC
6 | from ..webviz_assets import WEBVIZ_ASSETS
7 |
8 |
9 | class ExampleAssets(WebvizPluginABC):
10 | def __init__(self, picture_path: Path, css_path: Path = None, js_path: Path = None):
11 | super().__init__()
12 |
13 | self.asset_url = WEBVIZ_ASSETS.add(picture_path)
14 |
15 | if css_path is not None:
16 | WEBVIZ_ASSETS.add(css_path)
17 |
18 | if js_path is not None:
19 | WEBVIZ_ASSETS.add(js_path)
20 |
21 | @property
22 | def layout(self) -> html.Img:
23 | return html.Img(src=self.asset_url)
24 |
--------------------------------------------------------------------------------
/webviz_config/static/assets/webviz_config.css:
--------------------------------------------------------------------------------
1 | ._banner_image {
2 |
3 | display: flex;
4 | justify-content: center;
5 | font-size: 300%;
6 |
7 | text-shadow: 0.05em 0.05em 0 rgba(0, 0, 0, 0.7);
8 |
9 | width: 100%;
10 |
11 | margin-bottom: 25px;
12 |
13 | background-size: cover;
14 | background-position: center;
15 |
16 | }
17 |
18 | ._markdown_image {
19 |
20 | display: block;
21 | margin: auto;
22 | max-width: 90%;
23 | max-height: 90vw;
24 | margin-top: 16px;
25 | margin-bottom: 16px;
26 |
27 | }
28 |
29 | ._markdown_image_caption {
30 |
31 | display: block;
32 | text-align: center;
33 | margin-top: 16px;
34 | margin-bottom: 32px;
35 | }
36 |
--------------------------------------------------------------------------------
/webviz_config/utils/_dash_component_utils.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 |
4 | def calculate_slider_step(
5 | min_value: float, max_value: float, steps: int = 100
6 | ) -> float:
7 | """Calculates a step value for use in e.g. dcc.RangeSlider() component
8 | that will always be rounded.
9 |
10 | The number of steps will be atleast the number
11 | of input steps, but might not be precisely the same due to use of the floor function.
12 |
13 | This function is necessary since there is currently no precision control in the underlying
14 | React component (https://github.com/react-component/slider/issues/275).
15 |
16 | """
17 |
18 | return 10 ** math.floor(math.log10((max_value - min_value) / steps))
19 |
--------------------------------------------------------------------------------
/tests/test_schema.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import pytest
3 | import yaml
4 | import jsonschema
5 |
6 | from webviz_config._docs._create_schema import create_schema
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "config_file_path",
11 | [
12 | (pathlib.Path("examples") / "basic_example.yaml"),
13 | (pathlib.Path("examples") / "basic_example_advanced_menu.yaml"),
14 | ],
15 | )
16 | def test_schema(config_file_path: pathlib.Path):
17 | """Tests both that the generated schema is valid,
18 | and that the input configuration is valid according to the schema.
19 | """
20 |
21 | config = yaml.safe_load(config_file_path.read_text())
22 | jsonschema.validate(instance=config, schema=create_schema())
23 |
--------------------------------------------------------------------------------
/webviz_config/webviz_factory.py:
--------------------------------------------------------------------------------
1 | class WebvizFactory:
2 | """Base class for all factories that want to register themselves in
3 | WebvizFactoryRegistry.
4 |
5 | Note that this functionality is provisional/experimental, and will not necessarily
6 | see a deprecation phase and "may deviate from the usual version semantics."
7 | """
8 |
9 | def cleanup_resources_after_plugin_init(self) -> None:
10 | """Will be called after all plugins have been initialized to allow the factory
11 | to do clean-up of any temporary resources allocated during the initialization
12 | phase. The base implementation does nothing, override this function to
13 | perform factory-specific cleanup.
14 | """
15 |
--------------------------------------------------------------------------------
/tests/test_plugin_metadata.py:
--------------------------------------------------------------------------------
1 | import webviz_config
2 | from webviz_config.plugins import PLUGIN_PROJECT_METADATA
3 |
4 |
5 | def test_webviz_config_metadata():
6 |
7 | metadata = PLUGIN_PROJECT_METADATA["webviz-config"]
8 |
9 | assert metadata["dist_version"] == webviz_config.__version__
10 | assert metadata["documentation_url"] == "https://equinor.github.io/webviz-config"
11 | assert metadata["download_url"] == "https://pypi.org/project/webviz-config"
12 | assert metadata["source_url"] == "https://github.com/equinor/webviz-config"
13 | assert metadata["tracker_url"] == "https://github.com/equinor/webviz-config/issues"
14 |
15 | assert "dash" in metadata["dependencies"] # dash is a direct dependency
16 | assert "flask" in metadata["dependencies"] # flask is an indirect dependency
17 |
--------------------------------------------------------------------------------
/webviz_config/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | """These are the basic Webviz configuration plugins, distributed through
2 | the utility itself.
3 | """
4 |
5 | import abc
6 | from importlib.metadata import distributions
7 |
8 | from ._utils import load_webviz_plugins_with_metadata, PluginProjectMetaData
9 |
10 |
11 | (
12 | PLUGIN_METADATA,
13 | PLUGIN_PROJECT_METADATA,
14 | plugin_entrypoints,
15 | ) = load_webviz_plugins_with_metadata(distributions())
16 |
17 | __all__ = list(plugin_entrypoints.keys())
18 |
19 |
20 | def __getattr__(name: str) -> abc.ABC:
21 | """Lazy load plugins, i.e. only import/load when a given plugin is requested."""
22 |
23 | if name in __all__:
24 | return plugin_entrypoints[name].load()
25 | raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
26 |
--------------------------------------------------------------------------------
/webviz_config/common_cache.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | import flask_caching
4 |
5 |
6 | class Cache(flask_caching.Cache):
7 | def __init__(
8 | self,
9 | ) -> None:
10 |
11 | super().__init__(config={"CACHE_TYPE": "simple", "DEFAULT_CACHE_TIMEOUT": 3600})
12 |
13 | @property
14 | def TIMEOUT(self) -> int: # pylint: disable=invalid-name
15 | warnings.warn(
16 | "Default cache timeout is now initialized directly, and the TIMEOUT "
17 | "attribute is therefore deprecated, and will be removed. The timeout "
18 | "argument can still be used as input to @CACHE.memoize() with an input "
19 | "if you want to overwride the 3600 (seconds) webviz default.",
20 | DeprecationWarning,
21 | )
22 | return 3600
23 |
24 |
25 | CACHE = Cache()
26 |
--------------------------------------------------------------------------------
/webviz_config/themes/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from importlib.metadata import EntryPoint, entry_points
3 |
4 | from .. import WebvizConfigTheme
5 | from ._default_theme import default_theme
6 |
7 | installed_themes = {default_theme.theme_name: default_theme}
8 |
9 | __all__ = ["installed_themes"]
10 |
11 |
12 | def process_entry_point(entry_point: EntryPoint):
13 | theme = entry_point.load()
14 |
15 | globals()[entry_point.name] = theme
16 | __all__.append(entry_point.name)
17 |
18 | if isinstance(theme, WebvizConfigTheme):
19 | installed_themes[theme.theme_name] = theme
20 |
21 |
22 | if sys.version_info < (3, 10, 0):
23 | eps = entry_points().get("webviz_config_themes", [])
24 | else:
25 | eps = entry_points().select(group="webviz_config_themes")
26 |
27 | for ep in eps:
28 | process_entry_point(ep)
29 |
--------------------------------------------------------------------------------
/tests/test_syntax_highlighter.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from unittest import mock
3 |
4 | import dash
5 |
6 | from webviz_config.common_cache import CACHE
7 | from webviz_config.generic_plugins import _syntax_highlighter
8 |
9 |
10 | def test_syntax_highlighter(dash_duo):
11 | app = dash.Dash(__name__)
12 | app.config.suppress_callback_exceptions = True
13 | CACHE.init_app(app.server)
14 | code_file = Path("./tests/data/basic_example.yaml")
15 | with mock.patch(
16 | "webviz_config.generic_plugins._syntax_highlighter.get_path"
17 | ) as mock_path:
18 | mock_path.return_value = code_file
19 | page = _syntax_highlighter.SyntaxHighlighter(app, code_file)
20 | app.layout = page.layout
21 | dash_duo.start_server(app)
22 | assert not dash_duo.get_logs(), "browser console should contain no error"
23 |
--------------------------------------------------------------------------------
/tests/test_example_wlf_plugin.py:
--------------------------------------------------------------------------------
1 | from webviz_config.testing import WebvizComposite
2 | from webviz_config.generic_plugins._example_wlf_plugin import ExampleWlfPlugin
3 |
4 |
5 | def test_example_wlf_plugin(
6 | _webviz_duo: WebvizComposite,
7 | ) -> None:
8 | plugin = ExampleWlfPlugin(title="hello")
9 |
10 | _webviz_duo.start_server(plugin)
11 |
12 | _webviz_duo.toggle_webviz_settings_drawer()
13 |
14 | _webviz_duo.toggle_webviz_settings_group(
15 | plugin.view("plot-view").settings_group_unique_id("plot-settings")
16 | )
17 |
18 | component_id = _webviz_duo.view_settings_group_unique_component_id(
19 | view_id="plot-view",
20 | settings_group_id="plot-settings",
21 | component_unique_id="coordinates-selector",
22 | )
23 |
24 | _webviz_duo.wait_for_contains_text(component_id, "x - y")
25 | assert not _webviz_duo.get_logs()
26 |
--------------------------------------------------------------------------------
/webviz_config/testing/_plugin.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Generator
2 |
3 | import pytest
4 |
5 | from webviz_config.testing import WebvizComposite
6 |
7 |
8 | @pytest.fixture
9 | def _webviz_duo(request: Any, dash_thread_server: Any, tmpdir: Any) -> Generator:
10 | with WebvizComposite(
11 | dash_thread_server,
12 | browser=request.config.getoption("webdriver"),
13 | remote=request.config.getoption("remote"),
14 | remote_url=request.config.getoption("remote_url"),
15 | headless=request.config.getoption("headless"),
16 | options=request.config.hook.pytest_setup_options(),
17 | download_path=tmpdir.mkdir("download").strpath,
18 | percy_assets_root=request.config.getoption("percy_assets"),
19 | percy_finalize=request.config.getoption("nopercyfinalize"),
20 | pause=request.config.getoption("pause"),
21 | ) as duo:
22 | yield duo
23 |
--------------------------------------------------------------------------------
/tests/test_example_plugin.py:
--------------------------------------------------------------------------------
1 | import dash
2 |
3 | from webviz_config.common_cache import CACHE
4 | from webviz_config.generic_plugins import _example_plugin
5 |
6 |
7 | def test_example_plugin(dash_duo):
8 |
9 | app = dash.Dash(__name__)
10 | app.config.suppress_callback_exceptions = True
11 | CACHE.init_app(app.server)
12 | title = "Example"
13 | page = _example_plugin.ExamplePlugin(app, title)
14 | app.layout = page.layout
15 | dash_duo.start_server(app)
16 | btn = dash_duo.find_element("#" + page.uuid("submit-button"))
17 | assert btn.text == "Submit"
18 | text = dash_duo.find_element("#" + page.uuid("output-state"))
19 | assert text.text == "Button has been pressed 0 times."
20 | btn.click()
21 | dash_duo.wait_for_contains_text(
22 | "#" + page.uuid("output-state"), "Button has been pressed 1 times", timeout=2
23 | )
24 | assert text.text == "Button has been pressed 1 times."
25 |
--------------------------------------------------------------------------------
/tests/test_portable.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import subprocess # nosec
3 |
4 |
5 | def test_portable(dash_duo, tmp_path):
6 | # Build a portable webviz from config file
7 | appdir = tmp_path / "app"
8 | subprocess.call( # nosec
9 | ["webviz", "build", "basic_example.yaml", "--portable", appdir], cwd="examples"
10 | )
11 |
12 | # Import generated app
13 | sys.path.append(str(appdir))
14 | from webviz_app import app # pylint: disable=import-error, import-outside-toplevel
15 |
16 | # Start and test app
17 | dash_duo.start_server(app)
18 | for page in [
19 | "markdown-example",
20 | "table-example",
21 | "pdf-example",
22 | "syntax-highlighting-example",
23 | "plot-a-table",
24 | "pivot-table",
25 | ]:
26 | dash_duo.wait_for_element(f".Menu__Page[href='/{page}']").click()
27 | assert not dash_duo.get_logs(), "browser console should contain no error"
28 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | If you discover a security vulnerability in this project, please follow these steps to responsibly disclose it:
2 |
3 | 1. **Do not** create a public GitHub issue for the vulnerability.
4 |
5 | 2. Follow our guideline for Responsible Disclosure Policy at https://www.equinor.com/about-us/csirt to report the issue
6 |
7 | The following information will help us triage your report more quickly:
8 |
9 | - Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
10 | - Full paths of source file(s) related to the manifestation of the issue
11 | - The location of the affected source code (tag/branch/commit or direct URL)
12 | - Any special configuration required to reproduce the issue
13 | - Step-by-step instructions to reproduce the issue
14 | - Proof-of-concept or exploit code (if possible)
15 | - Impact of the issue, including how an attacker might exploit the issue
16 | - We prefer all communications to be in English.
17 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_shared_view_elements/_text_view_element.py:
--------------------------------------------------------------------------------
1 | from typing import Type, Union
2 | from enum import Enum
3 |
4 | from dash.development.base_component import Component
5 | from dash import html
6 |
7 | from webviz_config.webviz_plugin_subclasses import ViewElementABC
8 |
9 |
10 | class TextViewElement(ViewElementABC):
11 | class Ids(str, Enum):
12 | TEXT = "text"
13 |
14 | def __init__(self) -> None:
15 | super().__init__()
16 |
17 | def inner_layout(self) -> Union[str, Type[Component]]:
18 | return html.Div(
19 | id=self.register_component_unique_id(TextViewElement.Ids.TEXT),
20 | children=[
21 | html.H1("Hello"),
22 | """
23 | This is an example plugin.
24 | Please have a look how views and settings are working in this new environment =).
25 | """,
26 | ],
27 | )
28 |
--------------------------------------------------------------------------------
/webviz_config/_webviz_settings_class.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from typing import Dict, Any, Mapping
3 |
4 | from ._theme_class import WebvizConfigTheme
5 |
6 |
7 | class WebvizSettings:
8 | """This class contains global Webviz settings that will be made available
9 | to all plugins through the special argument named webviz_settings.
10 | """
11 |
12 | def __init__(self, shared_settings: Dict[str, Any], theme: WebvizConfigTheme):
13 | if not isinstance(shared_settings, dict):
14 | raise TypeError("shared_settings must be of type dict")
15 |
16 | if not isinstance(theme, WebvizConfigTheme):
17 | raise TypeError("theme must be of type WebvizConfigTheme")
18 |
19 | self._shared_settings = shared_settings
20 | self._theme = theme
21 |
22 | @property
23 | def shared_settings(self) -> Mapping[str, Any]:
24 | return copy.deepcopy(self._shared_settings)
25 |
26 | @property
27 | def theme(self) -> WebvizConfigTheme:
28 | return copy.deepcopy(self._theme)
29 |
--------------------------------------------------------------------------------
/webviz_config/utils/_deprecate_webviz_settings_attribute_in_dash_app.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 | import warnings
3 |
4 | import dash
5 |
6 |
7 | def _get_deprecated_webviz_settings(self: Any) -> dict:
8 | warnings.warn(
9 | "Accessing webviz_settings through the Dash application object has been deprecated, "
10 | "see https://github.com/equinor/webviz-config/pull/368",
11 | DeprecationWarning,
12 | stacklevel=2,
13 | )
14 | # pylint: disable=protected-access
15 | return self._deprecated_webviz_settings
16 |
17 |
18 | def deprecate_webviz_settings_attribute_in_dash_app() -> None:
19 | """Helper that monkey patches dash.Dash application class so that access to
20 | the webviz_settings via the Dash application instance attribute is reported
21 | as being deprecated.
22 | """
23 | dash.Dash.webviz_settings = property(
24 | _get_deprecated_webviz_settings,
25 | None,
26 | None,
27 | "Property to mark webviz_settings access as deprecated",
28 | )
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webviz-config",
3 | "description": "Documentation system for webviz-config plugins",
4 | "scripts": {
5 | "postinstall": "cd ./node_modules; copyfiles --flat ./docsify/lib/docsify.min.js ./docsify/lib/plugins/search.min.js ./docsify/lib/themes/vue.css ./docsify-tabs/dist/docsify-tabs.min.js ./prismjs/components/prism-bash.min.js ./prismjs/components/prism-python.min.js ./prismjs/components/prism-yaml.min.js ./docsify-copy-code/dist/docsify-copy-code.min.js ./katex/dist/katex.min.css ./docsify-katex/dist/docsify-katex.js ../INTRODUCTION.md ../webviz_config/_docs/static/; copyfiles --flat ./katex/dist/fonts/*.woff2 ../webviz_config/_docs/static/fonts; cd .."
6 | },
7 | "author": "Equinor",
8 | "license": "MIT",
9 | "dependencies": {
10 | "docsify": "^4.12.0",
11 | "docsify-copy-code": "^2.1.1",
12 | "docsify-katex": "^1.4.3",
13 | "docsify-tabs": "^1.4.4",
14 | "katex": "^0.12.0",
15 | "prismjs": "^1.27.0"
16 | },
17 | "devDependencies": {
18 | "copyfiles": "^2.4.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_tour.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from dash import html
4 |
5 | from .. import WebvizPluginABC
6 |
7 |
8 | class ExampleTour(WebvizPluginABC):
9 | @property
10 | def tour_steps(self) -> List[dict]:
11 | return [
12 | {"id": self.uuid("blue_text"), "content": "This is the first step"},
13 | {"id": self.uuid("red_text"), "content": "This is the second step"},
14 | ]
15 |
16 | @property
17 | def layout(self) -> html.Div:
18 | return html.Div(
19 | children=[
20 | html.Span(
21 | "Here is some blue text to explain... ",
22 | id=self.uuid("blue_text"),
23 | style={"color": "blue"},
24 | ),
25 | html.Span(
26 | " ...and here is some red text that also needs an explanation.",
27 | id=self.uuid("red_text"),
28 | style={"color": "red"},
29 | ),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_plugin.py:
--------------------------------------------------------------------------------
1 | from dash import html, Dash, Input, Output
2 |
3 | from .. import WebvizPluginABC
4 |
5 |
6 | class ExamplePlugin(WebvizPluginABC):
7 | def __init__(self, app: Dash, title: str):
8 | super().__init__()
9 |
10 | self.title = title
11 |
12 | self.set_callbacks(app)
13 |
14 | @property
15 | def layout(self) -> html.Div:
16 | return html.Div(
17 | [
18 | html.H1(self.title),
19 | html.Button(
20 | id=self.uuid("submit-button"), n_clicks=0, children="Submit"
21 | ),
22 | html.Div(id=self.uuid("output-state")),
23 | ]
24 | )
25 |
26 | def set_callbacks(self, app: Dash) -> None:
27 | @app.callback(
28 | Output(self.uuid("output-state"), "children"),
29 | [Input(self.uuid("submit-button"), "n_clicks")],
30 | )
31 | def _update_output(n_clicks: int) -> str:
32 | return f"Button has been pressed {n_clicks} times."
33 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_data_download.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from dash import html, Dash
4 |
5 | from .. import WebvizPluginABC, EncodedFile
6 |
7 |
8 | class ExampleDataDownload(WebvizPluginABC):
9 | def __init__(self, app: Dash, title: str):
10 | super().__init__()
11 |
12 | self.title = title
13 | self.set_callbacks(app)
14 |
15 | @property
16 | def layout(self) -> html.H1:
17 | return html.H1(self.title)
18 |
19 | def set_callbacks(self, app: Dash) -> None:
20 | @app.callback(self.plugin_data_output, self.plugin_data_requested)
21 | def _user_download_data(data_requested: bool) -> Optional[EncodedFile]:
22 | return (
23 | WebvizPluginABC.plugin_compressed_data(
24 | filename="webviz-data.zip",
25 | content=[
26 | {"filename": "some_file.txt", "content": "Some download data"}
27 | ],
28 | )
29 | if data_requested
30 | else None
31 | )
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Equinor ASA
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.
22 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_embed_pdf.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from dash import html
4 |
5 | from .. import WebvizPluginABC
6 | from ..webviz_assets import WEBVIZ_ASSETS
7 |
8 |
9 | class EmbedPdf(WebvizPluginABC):
10 | """Embeds a given PDF file into the page.
11 |
12 | !> Webviz does not scan your PDF for malicious code. Make sure it comes from a trusted source.
13 | ---
14 |
15 | * **`pdf_file`:** Path to the PDF file to include. Either absolute path or \
16 | relative to the configuration file.
17 | * **`height`:** Height of the PDF object (in percent of viewport height).
18 | * **`width`:** Width of the PDF object (in percent of available space).
19 |
20 | """
21 |
22 | def __init__(self, pdf_file: Path, height: int = 80, width: int = 100):
23 |
24 | super().__init__()
25 |
26 | self.pdf_url = WEBVIZ_ASSETS.add(pdf_file)
27 | self.height = height
28 | self.width = width
29 |
30 | @property
31 | def layout(self) -> html.Embed:
32 |
33 | style = {"height": f"{self.height}vh", "width": f"{self.width}%"}
34 |
35 | return html.Embed(src=self.pdf_url, style=style, type="application/pdf")
36 |
--------------------------------------------------------------------------------
/webviz_config/_docs/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Loading Webviz documentation...
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/webviz_config/utils/_silence_flask_startup.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import flask
4 |
5 |
6 | def silence_flask_startup() -> None:
7 | # pylint: disable=line-too-long
8 | """Calling this function monkey patches the function flask.cli.show_server_banner
9 | (https://github.com/pallets/flask/blob/a3f07829ca03bf312b12b3732e917498299fa82d/src/flask/cli.py#L657-L683)
10 | which by default outputs something like:
11 |
12 | * Serving Flask app "webviz_app" (lazy loading)
13 | * Environment: production
14 | WARNING: This is a development server. Do not use it in a production deployment.
15 | Use a production WSGI server instead.
16 | * Debug mode: off
17 |
18 | This warning is confusing to new users of flask and webviz-config, which thinks
19 | something is wrong (even though having development/debug mode turned off, and
20 | limit availability to localhost, is best practice wrt. security).
21 |
22 | After calling this function the exact lines above are not shown
23 | (all other information/output from the flask instance is untouched).
24 | """
25 |
26 | def silent_function(*_args: Any, **_kwargs: Any) -> None:
27 | pass
28 |
29 | flask.cli.show_server_banner = silent_function
30 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_syntax_highlighter.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List
3 |
4 | from dash import dcc
5 |
6 | from .. import WebvizPluginABC
7 | from ..webviz_store import webvizstore
8 |
9 |
10 | class SyntaxHighlighter(WebvizPluginABC):
11 | """Adds support for syntax highlighting of code. Language is automatically detected.
12 |
13 | ---
14 |
15 | * **`filename`:** Path to a file containing the code to highlight.
16 | * **`dark_theme`:** If `True`, the code is shown with a dark theme. Default is \
17 | `False`, giving a light theme.
18 | """
19 |
20 | def __init__(self, filename: Path, dark_theme: bool = False):
21 |
22 | super().__init__()
23 |
24 | self.filename = filename
25 | self.config = {"theme": "dark"} if dark_theme else {"theme": "light"}
26 |
27 | def add_webvizstore(self) -> List[tuple]:
28 | return [(get_path, [{"path": self.filename}])]
29 |
30 | @property
31 | def layout(self) -> dcc.Markdown:
32 | return dcc.Markdown(
33 | f"```\n{get_path(self.filename).read_text()}\n```",
34 | highlight_config=self.config,
35 | )
36 |
37 |
38 | @webvizstore
39 | def get_path(path: Path) -> Path:
40 | return path
41 |
--------------------------------------------------------------------------------
/webviz_config/static/README.md:
--------------------------------------------------------------------------------
1 | This webviz instance has been automatically created from configuration file.
2 |
3 | ## Run locally
4 |
5 | You can run it locally with:
6 |
7 | cd THISFOLDER
8 | python ./webviz_app.py
9 |
10 | ## Upload and build into Azure Container registry
11 |
12 | If you want to upload it to e.g. Azure Container Registry, you can do e.g.
13 |
14 | cd THISFOLDER
15 | az acr build --registry $ACR_NAME --image $IMAGE_NAME .
16 |
17 | assuming you have set the environment variables $ACR_NAME and $IMAGE_NAME.
18 |
19 | ## Private plugin projects
20 |
21 | Note that if you have included plugins from private GitHub projects in your
22 | application, you will need to provide a build time environment variable to Docker e.g. like:
23 |
24 | docker build . --build-arg GITHUB_DEPLOY_KEYS=COMMA_SEPARATED_LIST_OF_DEPLOY_KEYS.
25 |
26 | To read more on GitHub deploy keys, see https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys.
27 | In order to add the multi-line deploy keys, you should `base64` encode the deploy key
28 | before giving it as the `GITHUB_DEPLOY_KEYS` variable. Multiple keys (in case you have
29 | multiple private repositiories as dependencies) can be joined together with a comma
30 | separator (,) before `base64` encoding.
31 |
--------------------------------------------------------------------------------
/tests/data/basic_example.yaml:
--------------------------------------------------------------------------------
1 | # This file demonstrates the most basic usage of webviz in a FMU setting
2 | # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML).
3 |
4 | title: Reek Webviz Demonstration
5 |
6 | pages:
7 |
8 | - title: Front page
9 | content:
10 | - BannerImage:
11 | image: ./example_banner.png
12 | title: My banner image
13 | - Webviz created from configuration file.
14 | - Some other text, potentially with strange letters like Åre, Smørbukk Sør.
15 |
16 | - title: Markdown example
17 | content:
18 | - Markdown:
19 | markdown_file: ./example-markdown.md
20 |
21 | - title: PDF example
22 | content:
23 | - EmbedPdf:
24 | pdf_file: ./example.pdf
25 |
26 | - title: Syntax highlighting example
27 | content:
28 | - SyntaxHighlighter:
29 | filename: ./basic_example.yaml
30 | dark_theme: true
31 |
32 | - title: Plot a table
33 | content:
34 | - TablePlotter:
35 | csv_file: ./example_data.csv
36 |
37 | - title: Plot a table (locked)
38 | content:
39 | - TablePlotter:
40 | csv_file: ./example_data.csv
41 | lock: true
42 | plot_options:
43 | x: Well
44 | y: Initial reservoir pressure (bar)
45 | size: Average permeability (D)
46 | facet_col: Segment
47 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_portable.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import pandas as pd
4 |
5 | from .. import WebvizPluginABC
6 | from ..webviz_store import webvizstore
7 | from ..common_cache import CACHE
8 |
9 |
10 | class ExamplePortable(WebvizPluginABC):
11 | def __init__(self, some_number: int):
12 | super().__init__()
13 |
14 | self.some_number = some_number
15 | self.some_string = "a"
16 |
17 | def add_webvizstore(self) -> List[tuple]:
18 | return [
19 | (
20 | input_data_function,
21 | [{"some_string": self.some_string, "some_number": self.some_number}],
22 | )
23 | ]
24 |
25 | @property
26 | def layout(self) -> str:
27 | return str(input_data_function(self.some_string, some_number=self.some_number))
28 |
29 |
30 | @CACHE.memoize()
31 | @webvizstore
32 | def input_data_function(
33 | some_string: str, some_number: int, some_bool: bool = True
34 | ) -> pd.DataFrame:
35 | print("This time I'm actually doing the calculation...")
36 | return pd.DataFrame(
37 | data={
38 | "col1": [some_number, some_number * 2],
39 | "col2": [some_number * 3, some_number * 4],
40 | "col3": [some_string, some_string + "b"],
41 | "col4": [some_bool, not some_bool],
42 | }
43 | )
44 |
--------------------------------------------------------------------------------
/webviz_config/webviz_plugin_subclasses/_layout_base_abc.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, List, Optional, Union
2 | import abc
3 |
4 | from ._layout_unique_id import LayoutUniqueId
5 |
6 |
7 | class LayoutBaseABC(abc.ABC):
8 | def __init__(self) -> None:
9 | self._unique_id = LayoutUniqueId()
10 | self._plugin_register_id_func: Optional[
11 | Callable[[Union[str, List[str]]], None]
12 | ] = None
13 | self._plugin_get_store_unique_id_func: Optional[Callable[[str], str]] = None
14 |
15 | def _set_plugin_register_id_func(
16 | self, func: Callable[[Union[str, List[str]]], None]
17 | ) -> None:
18 | self._plugin_register_id_func = func
19 |
20 | def _set_unique_id(self, parent_unique_id: LayoutUniqueId) -> None:
21 | self._unique_id.adopt(parent_unique_id)
22 |
23 | if self._plugin_register_id_func:
24 | self._plugin_register_id_func(str(self._unique_id))
25 |
26 | def get_unique_id(self) -> LayoutUniqueId:
27 | return self._unique_id
28 |
29 | def _set_plugin_get_store_unique_id_func(self, func: Callable[[str], str]) -> None:
30 | self._plugin_get_store_unique_id_func = func
31 |
32 | def get_store_unique_id(self, store_id: str) -> str:
33 | if self._plugin_get_store_unique_id_func:
34 | return self._plugin_get_store_unique_id_func(store_id)
35 |
36 | raise NotImplementedError("This element has not been added to a plugin yet.")
37 |
--------------------------------------------------------------------------------
/webviz_config/utils/_available_port.py:
--------------------------------------------------------------------------------
1 | import os
2 | import socket
3 | from typing import Optional
4 |
5 |
6 | def get_available_port(preferred_port: Optional[int] = None) -> int:
7 | """Finds an available port for use in webviz on localhost. If a reload process,
8 | it will reuse the same port as found in the parent process by using an inherited
9 | environment variable.
10 |
11 | If preferred_port is given, ports in the range [preferred_port, preferred_port + 20)
12 | will be tried first, before an OS provided random port is used as fallback.
13 | """
14 |
15 | def is_available(port: int) -> bool:
16 | with socket.socket() as sock:
17 | try:
18 | sock.bind(("localhost", port))
19 | return True
20 | except OSError:
21 | return False
22 |
23 | if os.environ.get("WEBVIZ_PORT") is None:
24 | port = None
25 |
26 | if preferred_port is not None:
27 | for port_to_test in range(preferred_port, preferred_port + 20):
28 | if is_available(port_to_test):
29 | port = port_to_test
30 | break
31 |
32 | if port is None:
33 | with socket.socket() as sock:
34 | sock.bind(("localhost", 0))
35 | port = sock.getsockname()[1]
36 |
37 | os.environ["WEBVIZ_PORT"] = str(port)
38 | return port
39 |
40 | return int(os.environ.get("WEBVIZ_PORT")) # type: ignore[arg-type]
41 |
--------------------------------------------------------------------------------
/webviz_config/templates/feedback.md.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ### 🐞 Bug report:
6 |
7 |
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **How to reproduce**
13 | Steps to reproduce the behavior.
14 |
15 | **Expected behavior**
16 | A clear and concise description of what you expected to happen.
17 |
18 | **Screenshots**
19 | If applicable, add screenshots to help explain your problem.
20 |
21 | ---
22 |
23 | ### 💡 Feature request:
24 |
25 |
26 |
27 | **Is your feature request related to a problem? Please describe**
28 | A clear and concise description of what the problem is.
29 |
30 | **Describe the solution you'd like**
31 | A clear and concise description of what you want to happen.
32 |
33 | **Describe alternatives you've considered**
34 | A clear and concise description of any alternative solutions or features you've considered.
35 |
36 | ---
37 |
38 | ### 🔖 Version information
39 |
40 | * **Plugin name:** `{{plugin_name}}`
41 | * **Plugin project version:** `{{dist_name}}=={{dist_version}}`
42 | * **Plugin project dependencies:**
43 | ```{% for dependency, version in dependencies.items() %}
44 | {{dependency}}=={{version}}
45 | {%- endfor %}
46 | ```
47 |
--------------------------------------------------------------------------------
/tests/unit_tests/test_webviz_settings.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | import pytest
4 |
5 | from webviz_config import WebvizConfigTheme, WebvizSettings
6 |
7 |
8 | def test_construction_and_basic_access() -> None:
9 | # pylint: disable=unidiomatic-typecheck
10 | the_shared_settings = {"somenumber": 10, "somestring": "abc"}
11 | the_theme = WebvizConfigTheme("dummyThemeName")
12 | settings_obj = WebvizSettings(the_shared_settings, the_theme)
13 |
14 | copy_of_shared_settings = settings_obj.shared_settings
15 | assert copy_of_shared_settings is not the_shared_settings
16 | assert type(copy_of_shared_settings) == type(the_shared_settings)
17 | assert copy_of_shared_settings == the_shared_settings
18 | the_shared_settings["somestring"] = "MODIFIED"
19 | assert copy_of_shared_settings != the_shared_settings
20 |
21 | copy_of_theme = settings_obj.theme
22 | assert copy_of_theme is not the_theme
23 | assert type(copy_of_theme) == type(the_theme)
24 | assert copy_of_theme.__dict__ == the_theme.__dict__
25 | the_theme.theme_name = "MODIFIED"
26 | assert copy_of_theme.__dict__ != the_theme.__dict__
27 |
28 |
29 | def test_construction_with_invalid_types() -> None:
30 | with pytest.raises(TypeError):
31 | theme = WebvizConfigTheme("dummyThemeName")
32 | _settings_obj = WebvizSettings(cast(dict, None), theme)
33 |
34 | with pytest.raises(TypeError):
35 | shared_settings = {"somenumber": 10, "somestring": "abc"}
36 | _settings_obj = WebvizSettings(shared_settings, cast(WebvizConfigTheme, None))
37 |
--------------------------------------------------------------------------------
/tests/test_data_table.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | import pandas as pd
4 | import dash
5 |
6 | from webviz_config.common_cache import CACHE
7 | from webviz_config.generic_plugins import _data_table
8 |
9 | # mocked functions
10 | GET_DATA = "webviz_config.generic_plugins._data_table.get_data"
11 |
12 |
13 | def test_data_table(dash_duo):
14 | app = dash.Dash(__name__)
15 | app.config.suppress_callback_exceptions = True
16 | CACHE.init_app(app.server)
17 | code_file = "./tests/data/example_data.csv"
18 | with mock.patch(GET_DATA) as mock_path:
19 | mock_path.return_value = pd.read_csv(code_file)
20 | page = _data_table.DataTable(app, code_file)
21 | app.layout = page.layout
22 | dash_duo.start_server(app)
23 | assert not dash_duo.get_logs(), "browser console should contain no error"
24 |
25 |
26 | def test_data_table_with_settings(dash_duo):
27 | app = dash.Dash(__name__)
28 | app.css.config.serve_locally = True
29 | app.scripts.config.serve_locally = True
30 | app.config.suppress_callback_exceptions = True
31 | CACHE.init_app(app.server)
32 | code_file = "./tests/data/example_data.csv"
33 | with mock.patch(GET_DATA) as mock_path:
34 | mock_path.return_value = pd.read_csv(code_file)
35 | page = _data_table.DataTable(
36 | app, csv_file=code_file, sorting=False, filtering=False, pagination=False
37 | )
38 | app.layout = page.layout
39 | dash_duo.start_server(app)
40 | assert not dash_duo.get_logs(), "browser console should contain no error"
41 |
--------------------------------------------------------------------------------
/webviz_config/_user_preferences.py:
--------------------------------------------------------------------------------
1 | import json
2 | import webbrowser
3 | from typing import Optional
4 |
5 | from ._user_data_dir import user_data_dir
6 | from .themes import installed_themes
7 |
8 | USER_SETTINGS_FILE = user_data_dir() / "user_settings.json"
9 |
10 |
11 | def set_user_preferences(
12 | theme: Optional[str] = None, browser: Optional[str] = None
13 | ) -> None:
14 |
15 | preferences = (
16 | json.loads(USER_SETTINGS_FILE.read_text())
17 | if USER_SETTINGS_FILE.is_file()
18 | else {}
19 | )
20 |
21 | new_preferences = {}
22 |
23 | if theme is not None:
24 | if theme not in installed_themes:
25 | raise ValueError(
26 | f"Theme {theme} is not one of the installed themes ({', '.join(installed_themes)})"
27 | )
28 | new_preferences["theme"] = theme
29 |
30 | if browser is not None:
31 | try:
32 | webbrowser.get(using=browser)
33 | except webbrowser.Error as exc:
34 | raise ValueError(
35 | f"Could not find an installed browser with the name {browser}."
36 | ) from exc
37 |
38 | new_preferences["browser"] = browser
39 |
40 | if new_preferences:
41 | preferences.update(new_preferences)
42 | USER_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
43 | USER_SETTINGS_FILE.write_text(json.dumps(preferences))
44 |
45 |
46 | def get_user_preference(setting: str) -> Optional[str]:
47 | return (
48 | json.loads(USER_SETTINGS_FILE.read_text()).get(setting)
49 | if USER_SETTINGS_FILE.is_file()
50 | else None
51 | )
52 |
--------------------------------------------------------------------------------
/webviz_config/_dockerize/_pip_git_url.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | from typing import Optional
3 |
4 | import requests
5 |
6 |
7 | def pip_git_url(
8 | version: str, source_url: str, git_pointer: Optional[str] = None
9 | ) -> str:
10 | """All webviz-config plugin projects are encouraged to be open source
11 | and on PyPI. Those that are not are encouraged to be on a git solution,
12 | and use quite standard setuptools_scm to give a runtime available version
13 | to the Python package.
14 |
15 | setuptool_scm enables us to get information regarding which hash/tag of the
16 | git repository to download + install in the Docker image, as installation from a
17 | clean repository state should give one of the following versions:
18 |
19 | no distance to tag:
20 | {tag}
21 | distance:
22 | {next_version}.dev{distance}+g{revision hash}
23 |
24 | If git_pointer argument is not None, this git ponter/reference is used instead
25 | of derived commit hash/tag from version.
26 | """
27 |
28 | source_url = source_url.rstrip("/")
29 |
30 | if git_pointer is None:
31 | git_pointer = version.split("+g")[1] if "+" in version else version
32 |
33 | if requests.get(source_url).status_code != 200:
34 | warnings.warn(
35 | f"Could not find {source_url}, assuming private repository. Changing to "
36 | "SSH URL. If building Docker image you will need to provide a deploy key."
37 | )
38 | for string in ["https://github.com/", "https://www.github.com/"]:
39 | source_url = source_url.replace(string, "ssh://git@github.com/")
40 |
41 | return f"git+{source_url}@{git_pointer}"
42 |
--------------------------------------------------------------------------------
/tests/test_docker_setup.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | import pytest
4 |
5 | from webviz_config._dockerize._create_docker_setup import get_python_requirements
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "distributions, requirements",
10 | [
11 | (
12 | {
13 | "webviz-config": {
14 | "download_url": "https://pypi.org/project/webviz-config",
15 | "source_url": None,
16 | "dist_version": "0.3.0",
17 | }
18 | },
19 | ["webviz-config==0.3.0"],
20 | ),
21 | (
22 | {
23 | "webviz-config": {
24 | "download_url": None,
25 | "source_url": None,
26 | "dist_version": "0.3.0",
27 | }
28 | },
29 | [],
30 | ),
31 | (
32 | {
33 | "webviz-config": {
34 | "download_url": None,
35 | "source_url": "https://github.com/equinor/webviz-config",
36 | "dist_version": "0.3.0",
37 | }
38 | },
39 | ["git+https://github.com/equinor/webviz-config@0.3.0"],
40 | ),
41 | ],
42 | )
43 | def test_derived_requirements(distributions, requirements):
44 | with warnings.catch_warnings(record=True) as record:
45 | warnings.simplefilter("always")
46 | assert set(requirements).issubset(get_python_requirements(distributions))
47 | assert len(record) == len(
48 | [
49 | metadata
50 | for metadata in distributions.values()
51 | if metadata["download_url"] is None and metadata["source_url"] is None
52 | ]
53 | )
54 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/radix_configuration.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Dict
3 |
4 | from . import interactive_terminal
5 | from . import radix_cli
6 |
7 |
8 | def radix_configuration() -> Dict[str, str]:
9 |
10 | interactive_terminal.terminal_title("Radix cluster")
11 | radix_context = interactive_terminal.user_input_from_list(
12 | "WEBVIZ_RADIX_CONTEXT", "Radix context", ["production", "playground"]
13 | )
14 | radix_subdomain = "radix.equinor.com"
15 | if radix_context == "playground":
16 | radix_subdomain = "playground." + radix_subdomain
17 |
18 | interactive_terminal.terminal_title("Radix AD group")
19 | radix_ad_group = interactive_terminal.user_input_from_stdin(
20 | "WEBVIZ_RADIX_AD_GROUP",
21 | "AD group object ID",
22 | )
23 |
24 | interactive_terminal.terminal_title("Radix application name")
25 | radix_application_name = interactive_terminal.user_input_optional_reuse(
26 | "WEBVIZ_RADIX_APPLICATION",
27 | "Radix application",
28 | functools.partial(radix_cli.application_exists, context=radix_context),
29 | reuse_allowed=False,
30 | )
31 |
32 | interactive_terminal.terminal_title("Configuration item")
33 | radix_configuration_item = interactive_terminal.user_input_from_stdin(
34 | "WEBVIZ_RADIX_CONFIGURATION_ITEM",
35 | "Configuration item",
36 | )
37 |
38 | return {
39 | "context": radix_context,
40 | "application_name": radix_application_name,
41 | "configuration_item": radix_configuration_item,
42 | "app_url": f"https://{radix_application_name}.app.{radix_subdomain}",
43 | "webhook_receiver_url": f"https://webhook.{radix_subdomain}/events/github",
44 | "ad_group": radix_ad_group,
45 | }
46 |
--------------------------------------------------------------------------------
/webviz_config/_docs/open_docs.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import pathlib
3 | import tempfile
4 | import argparse
5 | import logging
6 |
7 | import flask
8 |
9 | import webviz_config.utils
10 | from ._build_docs import build_docs
11 |
12 |
13 | def _start_doc_app(build_directory: pathlib.Path) -> None:
14 |
15 | app = flask.Flask(__name__, static_folder=str(build_directory), static_url_path="")
16 | app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
17 |
18 | @app.route("/")
19 | def _index() -> str:
20 | return (build_directory / "index.html").read_text()
21 |
22 | webviz_config.utils.silence_flask_startup()
23 | logging.getLogger("werkzeug").setLevel(logging.WARNING)
24 |
25 | port = webviz_config.utils.get_available_port(preferred_port=5050)
26 | token = webviz_config.LocalhostToken(app, port).one_time_token
27 | webviz_config.utils.LocalhostOpenBrowser(port, token)
28 |
29 | app.run(
30 | host="localhost",
31 | port=port,
32 | debug=False,
33 | )
34 |
35 |
36 | def open_docs(args: argparse.Namespace) -> None:
37 |
38 | if args.portable is None:
39 | build_directory = pathlib.Path(tempfile.mkdtemp())
40 | else:
41 | build_directory = args.portable.resolve()
42 | if build_directory.exists():
43 | if not args.force:
44 | raise ValueError(
45 | f"{build_directory} already exists. Either add --force or change output folder."
46 | )
47 | shutil.rmtree(build_directory)
48 | build_directory.mkdir(parents=True)
49 |
50 | try:
51 | build_docs(build_directory)
52 | if not args.skip_open:
53 | _start_doc_app(build_directory)
54 | finally:
55 | if args.portable is None:
56 | shutil.rmtree(build_directory)
57 |
--------------------------------------------------------------------------------
/webviz_config/_write_script.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import getpass
3 | import datetime
4 | import pathlib
5 | import argparse
6 | from typing import Dict, Tuple
7 |
8 | import yaml
9 | import jinja2
10 |
11 | from ._config_parser import ConfigParser
12 |
13 |
14 | def write_script(
15 | args: argparse.Namespace,
16 | build_directory: pathlib.Path,
17 | template_filename: str,
18 | output_filename: str,
19 | ) -> Tuple[set, Dict[str, dict]]:
20 | """Writes rendered script to build directory. Also returns information regarding
21 | assets and which plugins that are incluced in the user provided configuration file.
22 | """
23 |
24 | config_parser = ConfigParser(args.yaml_file)
25 | configuration = config_parser.configuration
26 |
27 | configuration.update(
28 | {
29 | "author": getpass.getuser(),
30 | "config_folder": f"Path('{(args.yaml_file.resolve().parent.as_posix())}')",
31 | "current_date": datetime.date.today().strftime("%Y-%m-%d"),
32 | "debug": args.debug,
33 | "loglevel": args.loglevel,
34 | "portable": args.portable is not None,
35 | "shared_settings": config_parser.shared_settings,
36 | "sys_executable": sys.executable,
37 | "theme_name": args.theme,
38 | }
39 | )
40 |
41 | if args.logconfig is not None:
42 | configuration["logging_config_dict"] = yaml.safe_load(
43 | args.logconfig.read_text()
44 | )
45 |
46 | template_environment = jinja2.Environment( # nosec
47 | loader=jinja2.PackageLoader("webviz_config", "templates"),
48 | undefined=jinja2.StrictUndefined,
49 | autoescape=False,
50 | )
51 |
52 | template = template_environment.get_template(template_filename)
53 |
54 | (build_directory / output_filename).write_text(template.render(configuration))
55 |
56 | return config_parser.assets, config_parser.plugin_metadata
57 |
--------------------------------------------------------------------------------
/webviz_config/_docs/static/README.md:
--------------------------------------------------------------------------------
1 | # Webviz introduction
2 |
3 | ## Usage {docsify-ignore}
4 |
5 | Assuming you have a configuration file `your_config.yml`,
6 | there are two main usages of `webviz`:
7 |
8 | ```bash
9 | webviz build your_config.yml
10 | ```
11 | and
12 | ```bash
13 | webviz build your_config.yml --portable ./some_output_folder
14 | python ./some_output_folder/webviz_app.py
15 | ```
16 |
17 | The latter is useful when one or more plugins included in the configuration need to do
18 | some time-consuming data aggregation on their own, before presenting it to the user.
19 | The time-consuming part will then be done in the `build` step, and you can run your
20 | created application as many time as you want afterwards, with as little waiting
21 | time as possible).
22 |
23 | The `--portable` way also has the benefit of creating a :whale: Docker setup for your
24 | application - ready to be deployed to e.g. a cloud provider.
25 |
26 | ### Fundamental configuration {docsify-ignore}
27 |
28 | A configuration consists of some mandatory properties, e.g. app title,
29 | and one or more pages. A page has a title and some content.
30 | Each page can contain as many plugins as you want.
31 |
32 | Plugins represent predefined content, which can take one or more arguments.
33 | Lists and descriptions of installed plugins can be found on the other subpages.
34 |
35 | Content which is not plugins is interpreted as text paragraphs.
36 |
37 | A simple example configuration:
38 | ```yaml
39 | # This is a webviz configuration file example.
40 | # The configuration files use the YAML standard (https://en.wikipedia.org/wiki/YAML).
41 |
42 | title: Reek Webviz Demonstration
43 |
44 | pages:
45 |
46 | - title: Front page
47 | content:
48 | - BannerImage:
49 | image: ./example_banner.png
50 | title: My banner image
51 | - Webviz created from a configuration file.
52 |
53 | - title: Markdown example
54 | content:
55 | - Markdown:
56 | markdown_file: ./example-markdown.md
57 | ```
58 |
--------------------------------------------------------------------------------
/webviz_config/templates/plugin_docs.md.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 | #### {{ plugin["name"] }}
4 | {% if plugin["deprecated"] %}
5 |
6 | :warning: {{ plugin["deprecation_text_short"] }}
7 |
8 | {{ plugin["deprecation_text_long"] }}
9 |
10 | {% elif plugin["has_deprecated_arguments"] %}
11 | > :warning: At least one argument has a deprecation warning.
12 | {% endif %}
13 |
14 |
15 | {% if plugin["description"] is not none %}
16 |
17 |
18 |
19 | {{ plugin["description"] }}
20 |
21 | {% endif %}
22 |
23 |
24 |
25 | {% for arg, arg_info in plugin["arg_info"].items() %}
26 | {% if arg_info["deprecated"] %}>:warning: **`{{ arg }}`:** {% if arg_info["deprecation_message"] == "" %}Certain values for the argument have been deprecated and might soon not be accepted anymore. See function below for details.{% else %}{{ arg_info["deprecation_message"] }}{% endif %}
27 | {% endif %}
28 | {% endfor %}
29 | {% if plugin["has_deprecated_arguments"] %}
30 | ---
31 | {% endif %}
32 | {% if plugin["argument_description"] is not none %}
33 | {{ plugin["argument_description"] }}
34 | {% endif %}
35 | {% if plugin["deprecation_check_code"] != "" %}
36 | Function checking for deprecations:
37 | ```python
38 | {{ plugin["deprecation_check_code"] }}
39 | ```
40 | ---
41 | {% endif %}
42 | ---
43 | How to use in YAML config file:
44 | ```yaml
45 | - {{ plugin["name"] }}:
46 | {%- for arg, arg_info in plugin["arg_info"].items() %}
47 | {{ arg }}: {{ arg_info["default"] | tojson if "default" in arg_info else "" }} # {{ "Deprecated" if arg_info["deprecated"] else "Required" if arg_info["required"] else "Optional" }}{{ ", type " + arg_info["typehint_string"] | string if "typehint_string" in arg_info }}.
48 | {%- endfor %}
49 | ```
50 |
51 | {% if plugin["data_input"] is not none %}
52 |
53 |
54 |
55 | {{ plugin["data_input"] }}
56 |
57 | {% endif %}
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/examples/example-markdown.md:
--------------------------------------------------------------------------------
1 | # This is a big title
2 |
3 | ## This is a somewhat smaller title
4 |
5 | ### This is a subsubtitle
6 |
7 | Hi from a Markdown text file containing Norwegian letters (æ ø å), some
8 | **bold** letters, _italic_ letters. _You can also **combine** them._
9 |
10 | You can also add (potential multi-line) equations as illustrated here with the Maxwell equations,
11 | $$
12 | \begin{aligned}
13 | \nabla \cdot \mathbf{E} &= \frac {\rho} {\varepsilon_0} \\\\
14 | \nabla \cdot \mathbf{B} &= 0 \\\\
15 | \nabla \times \mathbf{E} &= -\frac{\partial \mathbf{B}} {\partial t} \\\\
16 | \nabla \times \mathbf{B} &= \mu_0\left(\mathbf{J} + \varepsilon_0 \frac{\partial \mathbf{E}} {\partial t} \right)
17 | \end{aligned}
18 | $$
19 | You can also add inline math where you e.g. describe the different parameters that goes into
20 | the equations, like $\varepsilon_0$ being permittivity of free space.
21 |
22 | ---
23 |
24 | Horizontal line splitting two paragraphs.
25 |
26 | #### An unordered list
27 |
28 | * Item 1
29 | * Item 2
30 | * Item 2a
31 | * Item 2b
32 |
33 | #### An automatically ordered list
34 |
35 | 1. Item 1
36 | 1. Item 2
37 | 1. Item 2a
38 | 1. Item 2b
39 |
40 | #### An image with a caption
41 |
42 | 
43 |
44 | #### Quote
45 |
46 | > Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
47 | > tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
48 | > quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
49 |
50 | #### Collapsible text
51 |
52 |
53 | This is the title of some detailed information
54 | Here is some more information, which can be extended/collapsed on demand.
55 |
56 |
57 | #### An example table
58 |
59 | First Header | Second Header
60 | ------------ | -------------
61 | Content Cell | Content Cell
62 | Content Cell | Content Cell
63 |
64 | #### Some code
65 |
66 | ```python
67 | def some_function(input: int) -> int:
68 | return 42 * input
69 | ```
--------------------------------------------------------------------------------
/webviz_config/_shared_settings_subscriptions.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import inspect
3 | import pathlib
4 | from typing import Callable, List, Dict
5 |
6 |
7 | class SharedSettingsSubscriptions:
8 | """The user can configure common settings, shared between different cointainers,
9 | under a key called `shared_settings` in the configuration file. Since it originates
10 | from a native yaml file, the content is dictionaries/strings/ints/floats/dates.
11 |
12 | Third-party plugin packages might want to check early if the `shared_settings`
13 | they use are reasonable, and/or do some transformations on them.
14 | """
15 |
16 | def __init__(self) -> None:
17 | self._subscriptions: List[Dict] = []
18 |
19 | def subscribe(self, key: str) -> Callable:
20 | """This is the decorator, which third-party plugin packages will use."""
21 |
22 | def register(function: Callable) -> Callable:
23 | self._subscriptions.append({"key": key, "function": function})
24 | return function
25 |
26 | return register
27 |
28 | def transformed_settings(
29 | self, shared_settings: Dict, config_folder: str, portable: bool
30 | ) -> Dict:
31 | """Called from the app template, which returns the `shared_settings`
32 | after all third-party package subscriptions have done their
33 | (optional) transfomrations.
34 | """
35 | shared_settings = copy.deepcopy(shared_settings)
36 |
37 | for subscription in self._subscriptions:
38 | key, function = subscription["key"], subscription["function"]
39 | shared_settings[key] = function(
40 | *[
41 | pathlib.Path(config_folder)
42 | if arg == "config_folder"
43 | else portable
44 | if arg == "portable"
45 | else shared_settings.get(arg)
46 | for arg in inspect.getfullargspec(function).args
47 | ]
48 | )
49 |
50 | return shared_settings
51 |
52 |
53 | SHARED_SETTINGS_SUBSCRIPTIONS = SharedSettingsSubscriptions()
54 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_shared_settings/_shared_settings.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from dash.development.base_component import Component
4 | from dash import html
5 |
6 | import webviz_core_components as wcc
7 |
8 | from webviz_config.utils import StrEnum
9 | from webviz_config.webviz_plugin_subclasses import SettingsGroupABC
10 |
11 | from .._views._plot import Kindness
12 |
13 |
14 | class SharedSettingsGroup(SettingsGroupABC):
15 | class Ids(StrEnum):
16 | KINDNESS_SELECTOR = "kindness-selector"
17 | POWER_SELECTOR = "power-selector"
18 |
19 | def __init__(self) -> None:
20 | super().__init__("Shared settings")
21 |
22 | def layout(self) -> List[Component]:
23 | return [
24 | html.Div(
25 | children=[
26 | wcc.Label("Kindness"),
27 | wcc.RadioItems(
28 | id=self.register_component_unique_id(
29 | SharedSettingsGroup.Ids.KINDNESS_SELECTOR
30 | ),
31 | options=[
32 | {
33 | "label": a.value,
34 | "value": a.value,
35 | }
36 | for a in Kindness
37 | ],
38 | value="friendly",
39 | ),
40 | wcc.Label("Power"),
41 | wcc.RadioItems(
42 | id=self.register_component_unique_id(
43 | SharedSettingsGroup.Ids.POWER_SELECTOR
44 | ),
45 | options=[
46 | {
47 | "label": "2",
48 | "value": 2,
49 | },
50 | {
51 | "label": "3",
52 | "value": 3,
53 | },
54 | ],
55 | value=2,
56 | ),
57 | ]
58 | )
59 | ]
60 |
--------------------------------------------------------------------------------
/webviz_config/_deprecation_store.py:
--------------------------------------------------------------------------------
1 | from typing import Any, List, Optional, Dict, Callable, Union
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class DeprecatedPlugin:
7 | class_reference: Any
8 | short_message: str
9 | long_message: str
10 |
11 |
12 | @dataclass(frozen=True)
13 | class DeprecatedArgument:
14 | method_reference: Any
15 | method_name: str
16 | argument_name: str
17 | argument_value: str
18 | short_message: str
19 | long_message: str
20 |
21 |
22 | @dataclass(frozen=True)
23 | class DeprecatedArgumentCheck:
24 | method_reference: Any
25 | method_name: str
26 | argument_names: List[str]
27 | callback: Callable
28 | callback_code: str
29 |
30 |
31 | class DeprecationStore:
32 | def __init__(self) -> None:
33 | self.stored_plugin_deprecations: Dict[Any, DeprecatedPlugin] = {}
34 | self.stored_plugin_argument_deprecations: List[
35 | Union[DeprecatedArgument, DeprecatedArgumentCheck]
36 | ] = []
37 |
38 | def register_deprecated_plugin(self, deprecated_plugin: DeprecatedPlugin) -> None:
39 | """This function is automatically called by the decorator
40 | @deprecated_plugin, registering the plugin it decorates.
41 | """
42 | self.stored_plugin_deprecations[
43 | deprecated_plugin.class_reference
44 | ] = deprecated_plugin
45 |
46 | def register_deprecated_plugin_argument(
47 | self,
48 | deprecated_plugin_argument: Union[DeprecatedArgument, DeprecatedArgumentCheck],
49 | ) -> None:
50 | """This function is automatically called by the decorator
51 | @deprecated_plugin_arguments, registering the __init__ function it decorates.
52 | """
53 | self.stored_plugin_argument_deprecations.append(deprecated_plugin_argument)
54 |
55 | def get_stored_plugin_deprecation(self, plugin: Any) -> Optional[DeprecatedPlugin]:
56 | return self.stored_plugin_deprecations.get(plugin)
57 |
58 | def get_stored_plugin_argument_deprecations(
59 | self, method: Callable
60 | ) -> List[Union[DeprecatedArgument, DeprecatedArgumentCheck]]:
61 | return [
62 | stored
63 | for stored in self.stored_plugin_argument_deprecations
64 | if stored.method_reference == method
65 | ]
66 |
67 |
68 | DEPRECATION_STORE = DeprecationStore()
69 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_pivot_table.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from pathlib import Path
3 | from typing import List, Optional
4 |
5 | import pandas as pd
6 | from dash import Dash
7 | import dash_pivottable
8 |
9 | from .. import WebvizPluginABC, EncodedFile
10 | from ..webviz_store import webvizstore
11 | from ..common_cache import CACHE
12 |
13 |
14 | class PivotTable(WebvizPluginABC):
15 | """Adds a pivot table to the webviz instance, using tabular data from a \
16 | provided csv file.
17 |
18 | ---
19 |
20 | * **`csv_file`:** Path to the csv file containing the tabular data. Either absolute \
21 | path or relative to the configuration file.
22 | * **`options`:** Additional options for the plot. See [dash-pivottable documentation]\
23 | (https://github.com/plotly/dash-pivottable#references) for all possible options.
24 | """
25 |
26 | def __init__(self, app: Dash, csv_file: Path, options: dict = None):
27 |
28 | super().__init__()
29 |
30 | self.csv_file = csv_file
31 | self.options = options if options is not None else {}
32 |
33 | self.set_callbacks(app)
34 |
35 | def add_webvizstore(self) -> List[tuple]:
36 | return [(get_data, [{"csv_file": self.csv_file}])]
37 |
38 | @property
39 | def layout(self) -> dash_pivottable.PivotTable:
40 | return generate_table(get_data(self.csv_file), **self.options)
41 |
42 | def set_callbacks(self, app: Dash) -> None:
43 | @app.callback(self.plugin_data_output, self.plugin_data_requested)
44 | def _user_download_data(data_requested: Optional[int]) -> Optional[EncodedFile]:
45 | return (
46 | {
47 | "filename": "pivot-table.csv",
48 | "content": base64.b64encode(
49 | get_data(self.csv_file).to_csv(index=False).encode()
50 | ).decode("ascii"),
51 | "mime_type": "text/csv",
52 | }
53 | if data_requested
54 | else None
55 | )
56 |
57 |
58 | def generate_table(dframe: pd.DataFrame, **options: str) -> dash_pivottable.PivotTable:
59 | return dash_pivottable.PivotTable(
60 | data=[dframe.columns.to_list()] + dframe.values.tolist(), **options
61 | )
62 |
63 |
64 | @CACHE.memoize()
65 | @webvizstore
66 | def get_data(csv_file: Path) -> pd.DataFrame:
67 | return pd.read_csv(csv_file)
68 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_banner_image.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Dict
3 |
4 | from dash import html
5 |
6 | from .. import WebvizPluginABC
7 | from ..webviz_assets import WEBVIZ_ASSETS
8 |
9 |
10 | class BannerImage(WebvizPluginABC):
11 | """Adds a full width banner image, with an optional overlayed title.
12 | Useful on e.g. the front page for introducing a field or project.
13 |
14 | ---
15 |
16 | * **`image`:** Path to the picture you want to add. \
17 | Either absolute path or relative to the configuration file.
18 | * **`title`:** Title which will be overlayed over the banner image.
19 | * **`color`:** Color to be used for the font.
20 | * **`shadow`:** Set to `False` if you do not want text shadow for the title.
21 | * **`height`:** Height of the banner image (in pixels).
22 | * **`title_position`:** Position of title (either `center`, `top` or `bottom`).
23 | """
24 |
25 | CSS_TITLE_POSITIONS: Dict[str, str] = {
26 | "top": "start",
27 | "center": "center",
28 | "bottom": "end",
29 | }
30 |
31 | def __init__(
32 | self,
33 | image: Path,
34 | title: str = "",
35 | color: str = "white",
36 | shadow: bool = True,
37 | height: int = 300,
38 | title_position: str = "center",
39 | ):
40 |
41 | super().__init__()
42 |
43 | self.image = image
44 | self.title = title
45 | self.color = color
46 | self.shadow = shadow
47 | self.height = height
48 |
49 | try:
50 | self.css_title_position = BannerImage.CSS_TITLE_POSITIONS[title_position]
51 | except KeyError as exc:
52 | raise ValueError(
53 | f"{title_position} not a valid position for banner image title. "
54 | f"Valid options: {', '.join(BannerImage.CSS_TITLE_POSITIONS.keys())}"
55 | ) from exc
56 |
57 | self.image_url = WEBVIZ_ASSETS.add(image)
58 |
59 | @property
60 | def layout(self) -> html.Div:
61 |
62 | style = {
63 | "color": self.color,
64 | "backgroundImage": f"url({self.image_url})",
65 | "height": f"{self.height}px",
66 | "align-items": self.css_title_position,
67 | }
68 |
69 | if self.shadow:
70 | style["textShadow"] = "0.05em 0.05em 0"
71 |
72 | if self.color == "white":
73 | style["textShadow"] += " rgba(0, 0, 0, 0.7)"
74 | else:
75 | style["textShadow"] += " rgba(255, 255, 255, 0.7)"
76 |
77 | return html.Div(self.title, className="_banner_image", style=style)
78 |
--------------------------------------------------------------------------------
/webviz_config/deprecation_decorators.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Union, Callable, Tuple, cast, Optional, List, Type
2 | import inspect
3 |
4 | from ._plugin_abc import WebvizPluginABC
5 | from . import _deprecation_store as _ds
6 |
7 |
8 | def deprecated_plugin(
9 | deprecation_info: str = "",
10 | ) -> Callable[[Type[WebvizPluginABC]], Type[WebvizPluginABC]]:
11 | def wrapper(
12 | original_plugin: Type[WebvizPluginABC],
13 | ) -> Type[WebvizPluginABC]:
14 |
15 | _ds.DEPRECATION_STORE.register_deprecated_plugin(
16 | _ds.DeprecatedPlugin(
17 | original_plugin,
18 | f"Plugin '{original_plugin.__name__}' has been deprecated.",
19 | deprecation_info,
20 | )
21 | )
22 |
23 | return original_plugin
24 |
25 | return wrapper
26 |
27 |
28 | def deprecated_plugin_arguments(
29 | check: Union[Dict[str, str], Callable[..., Optional[Tuple[str, str]]]]
30 | ) -> Callable:
31 | def decorator(original_init_method: Callable) -> Callable:
32 | original_method_args = inspect.getfullargspec(original_init_method).args
33 |
34 | if callable(check):
35 | check_args = inspect.getfullargspec(check).args
36 | verified_args: List[str] = []
37 | for check_arg in check_args:
38 | for original_arg in original_method_args:
39 | if check_arg == original_arg:
40 | verified_args.append(check_arg)
41 |
42 | _ds.DEPRECATION_STORE.register_deprecated_plugin_argument(
43 | _ds.DeprecatedArgumentCheck(
44 | original_init_method,
45 | original_init_method.__name__,
46 | verified_args,
47 | check,
48 | inspect.getsource(check),
49 | )
50 | )
51 |
52 | elif isinstance(check, dict):
53 | for original_arg in original_method_args:
54 | if original_arg in check.keys():
55 | short_message = cast(Tuple[str, str], check[original_arg])[0]
56 | long_message = cast(Tuple[str, str], check[original_arg])[1]
57 | _ds.DEPRECATION_STORE.register_deprecated_plugin_argument(
58 | _ds.DeprecatedArgument(
59 | original_init_method,
60 | original_init_method.__name__,
61 | original_arg,
62 | "",
63 | short_message,
64 | long_message,
65 | )
66 | )
67 | return original_init_method
68 |
69 | return decorator
70 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # How we work - Open, Collaborative, Courageous, Caring
2 |
3 | In Equinor, [how we deliver is as important as what we
4 | deliver](https://www.equinor.com/en/careers/our-culture.html).
5 |
6 | As a values-based organization, our code of conduct for open source projects is
7 | simply a reflection of our values and how we live up to these as teams as well
8 | as individuals.
9 |
10 | This set the expectations for how we collaborate in our open source projects,
11 | and applies to all community members and participants in any Equinor maintained
12 | project.
13 |
14 | In addition to any definition or interpretation of the open source code of
15 | conduct, the company wide [Equinor code of conduct](https://www.equinor.com/content/dam/statoil/documents/ethics/equinor-code-of-conduct.pdf)
16 | always applies to all employees and hired contractors.
17 |
18 | ## Handling issues within the communities
19 |
20 | We expect all to have a low threshold for raising issues, or in general discuss
21 | how we live up to our
22 | [values](https://www.equinor.com/en/about-us.html#our-values). Equally, we also
23 | encourage all community members to appreciate when concerns are raised, and make
24 | its best effort in solving them.
25 |
26 | As well as responsibility for what is delivered the project maintainers are also
27 | responsible creating an environment for proper handling of issues raised within
28 | the communities.
29 |
30 | ## Call out for assistance
31 |
32 | For any problem not directly resolvable within the community, we encourage you
33 | to call out for assistance. Getting an outsiders perspective on the topic might
34 | be just what is needed for you to proceed.
35 |
36 | Send an e-mail to [opensource@equinor.com](mailto:opensource@equinor.com) and
37 | invite for a discussion. The email will be handled by a team within the Equinor organization.
38 |
39 | Your request will be kept confidential from the team or community in question,
40 | unless you chose to disclose it yourself.
41 |
42 | ## Ethics helpline
43 |
44 | In Equinor, we want you to speak up whenever you see unethical behaviour that
45 | conflicts with our values or threatens our reputation.
46 |
47 | To underline this, we continuously encourage and remind our employees and any
48 | external third parties interacting with us to raise concerns or report any
49 | suspected or potential breaches of law or company policies.
50 |
51 | For any questions or issues that one suspect falls outside the scope of
52 | behaviour regulated, and handled within the open source code of conduct, or you
53 | wish to place an anonymous, confidential report we encourage to use the
54 | [Equinor Ethics helpline](https://secure.ethicspoint.eu/domain/media/en/gui/102166/index.html).
55 |
56 | This helpline is hosted by a third party helpline provider.
57 |
--------------------------------------------------------------------------------
/webviz_config/webviz_plugin_subclasses/_settings_group_abc.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, List, Optional, Type, Union
2 | import abc
3 |
4 | from dash.development.base_component import Component
5 | import webviz_core_components as wcc
6 |
7 | from ._layout_base_abc import LayoutBaseABC
8 | from ._layout_unique_id import LayoutUniqueId
9 |
10 |
11 | class SettingsGroupABC(LayoutBaseABC):
12 | def __init__(self, title: str) -> None:
13 | super().__init__()
14 |
15 | self.title = title
16 | self._layout_created: bool = False
17 | self._visible_in_views: List[str] = []
18 | self._not_visible_in_views: List[str] = []
19 |
20 | def _set_visible_in_views(self, visible_in_views: List[str]) -> None:
21 | self._visible_in_views = visible_in_views
22 |
23 | def _set_not_visible_in_views(self, not_visible_in_views: List[str]) -> None:
24 | self._not_visible_in_views = not_visible_in_views
25 |
26 | def _set_plugin_register_id_func(
27 | self, func: Callable[[Union[str, List[str]]], None]
28 | ) -> None:
29 | self._plugin_register_id_func = func
30 |
31 | def register_component_unique_id(self, component_name: str) -> str:
32 | uuid = self.component_unique_id(component_name).to_string()
33 | if self._plugin_register_id_func and not self._layout_created:
34 | self._plugin_register_id_func(uuid)
35 |
36 | return uuid
37 |
38 | def component_unique_id(self, component_name: str) -> LayoutUniqueId:
39 | component_uuid = LayoutUniqueId(other=self.get_unique_id())
40 | component_uuid.set_component_id(component_name)
41 | return component_uuid
42 |
43 | @abc.abstractmethod
44 | def layout(self) -> Union[List[Component], Type[Component]]:
45 | raise NotImplementedError
46 |
47 | def _wrapped_layout(
48 | self,
49 | view_id: Optional[str] = "",
50 | plugin_id: Optional[str] = "",
51 | always_open: bool = False,
52 | ) -> Type[Component]:
53 | layout = self.layout()
54 | wrapped_layout = wcc.WebvizSettingsGroup(
55 | id=str(self.get_unique_id()),
56 | title=self.title,
57 | viewId=view_id,
58 | pluginId=plugin_id,
59 | visibleInViews=self._visible_in_views
60 | if len(self._visible_in_views) > 0
61 | else None,
62 | notVisibleInViews=self._not_visible_in_views
63 | if len(self._not_visible_in_views) > 0
64 | else None,
65 | alwaysOpen=always_open,
66 | children=layout if isinstance(layout, list) else [layout],
67 | )
68 | self._layout_created = True
69 | return wrapped_layout
70 |
71 | def set_callbacks(self) -> None:
72 | pass
73 |
--------------------------------------------------------------------------------
/webviz_config/static/assets/webviz_layout.css:
--------------------------------------------------------------------------------
1 | .styledLogo {
2 | border: none;
3 | background: transparent;
4 | outline: none;
5 | cursor: pointer;
6 | text-align: left;
7 | box-sizing: none;
8 | padding: 20px 30px;
9 | }
10 |
11 | .styledButton {
12 | border: none;
13 | background: transparent;
14 | outline: none;
15 | cursor: pointer;
16 | transition: background-color, color 200ms;
17 | text-align: left;
18 | box-sizing: none;
19 | font-size: 16px;
20 | font-family: var(--menuLinkFont);
21 | font-weight: var(--menuLinkFontWeight);
22 | font-style: var(--menuLinkFontStyle);
23 | color: var(--menuLinkColor);
24 | text-decoration: none;
25 | padding: 20px 30px;
26 | border-bottom: 1px solid white;
27 | }
28 |
29 | .styledButton:hover {
30 | color: var(--menuLinkHoverColor);
31 | background-color: var(--menuLinkBackgroundHover);
32 | }
33 |
34 | .selectedButton--selected {
35 | color: var(--menuLinkColorSelected);
36 | background-color: var(--menuLinkBackgroundSelected);
37 | }
38 |
39 | .layoutWrapper {
40 | display: flex;
41 | flex-direction: row;
42 | width: 100%;
43 | }
44 | .sideWrapper {
45 | display: flex;
46 | flex: 1;
47 | flex-direction: column;
48 | overflow-x: hidden;
49 | overflow-y: scroll;
50 | height: 98vh;
51 | min-width: 300px;
52 | scrollbar-width: thin;
53 | scrollbar-color: var(--menuBackground) var(--menuBackground); /* thumb and track color */
54 | }
55 |
56 | .pageContent {
57 | display: flex;
58 | flex: 8;
59 | flex-direction: column;
60 | height: 100%;
61 | width: 100%;
62 | padding: 15px;
63 | }
64 |
65 |
66 | @media (min-width: 800px) {
67 | .sideWrapper {
68 | width: 300px;
69 | }
70 | }
71 |
72 | .sideWrapper:hover {
73 | scrollbar-color: var(--menuLinkColor) var(--menuBackground); /* thumb and track color */
74 | }
75 |
76 | .sideWrapper::-webkit-scrollbar
77 | {
78 | width: 3px;
79 | background-color:var(--menuBackground);
80 | }
81 |
82 | .sideWrapper::-webkit-scrollbar-track
83 | {
84 | -webkit-box-shadow: inset 0 0 3px var(--menuBackground);
85 | background-color: var(--menuBackground);
86 | }
87 |
88 | .sideWrapper:hover::-webkit-scrollbar-track
89 | {
90 | -webkit-box-shadow: inset 0 0 3px var(--menuLinkBackgroundHover);
91 | background-color: var(--menuBackground);
92 | }
93 |
94 | .sideWrapper::-webkit-scrollbar-thumb
95 | {
96 | background-color: var(--menuBackground);
97 | }
98 |
99 | .sideWrapper:hover::-webkit-scrollbar-thumb
100 | {
101 | background-color: var(--menuLinkColor);
102 | }
103 |
104 | #logo {
105 | padding: 50px;
106 | border: none;
107 | max-width: 100%;
108 | height: auto;
109 | }
110 |
--------------------------------------------------------------------------------
/tests/test_plugin_init.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | from unittest import mock
3 | import importlib
4 | import importlib.metadata
5 |
6 | import webviz_config.plugins._utils
7 |
8 |
9 | class DistMock:
10 | # pylint: disable=too-few-public-methods
11 | def __init__(self, entry_points, name):
12 | self.metadata = {"name": name}
13 |
14 | self.entry_points = entry_points
15 | self.version = "123"
16 |
17 |
18 | plugin_entrypoint_mock1 = mock.Mock()
19 | plugin_entrypoint_mock1.group = "webviz_config_plugins"
20 | plugin_entrypoint_mock1.name = "SomePlugin1"
21 |
22 | plugin_entrypoint_mock2 = mock.Mock()
23 | plugin_entrypoint_mock2.group = "webviz_config_plugins"
24 | plugin_entrypoint_mock2.name = "SomePlugin2"
25 |
26 | dist_mock1 = DistMock([plugin_entrypoint_mock1], "dist_mock1")
27 | dist_mock2 = DistMock([plugin_entrypoint_mock1], "dist_mock2")
28 | dist_mock3 = DistMock([plugin_entrypoint_mock2], "dist_mock3")
29 |
30 |
31 | def test_no_warning(monkeypatch):
32 | # pylint: disable=protected-access
33 | monkeypatch.setattr(importlib.metadata, "requires", lambda x: [])
34 | importlib.reload(webviz_config.plugins._utils)
35 |
36 | with warnings.catch_warnings(record=True) as warn:
37 | (
38 | metadata,
39 | _,
40 | plugin_entrypoints,
41 | ) = webviz_config.plugins._utils.load_webviz_plugins_with_metadata(
42 | [dist_mock1, dist_mock3]
43 | )
44 | assert len(warn) == 0, "Too many warnings"
45 |
46 | assert len(metadata) == 2, "Wrong number of items in metadata"
47 | assert "SomePlugin1" in plugin_entrypoints
48 | assert "SomePlugin2" in plugin_entrypoints
49 |
50 |
51 | def test_warning_multiple(monkeypatch):
52 | # pylint: disable=protected-access
53 | monkeypatch.setattr(importlib.metadata, "requires", lambda x: [])
54 | importlib.reload(webviz_config.plugins._utils)
55 |
56 | with warnings.catch_warnings(record=True) as warn:
57 | (
58 | metadata,
59 | _,
60 | plugin_entrypoints,
61 | ) = webviz_config.plugins._utils.load_webviz_plugins_with_metadata(
62 | [dist_mock1, dist_mock2]
63 | )
64 |
65 | assert len(warn) == 1
66 | assert issubclass(warn[-1].category, RuntimeWarning)
67 | print(warn[-1].message)
68 | assert str(warn[-1].message) == (
69 | "Multiple versions of plugin with name SomePlugin1. "
70 | "Already loaded from project dist_mock1. "
71 | "Overwriting using plugin with from project dist_mock2"
72 | )
73 |
74 | assert len(metadata) == 1, "Wrong number of items in metadata"
75 | assert metadata["SomePlugin1"]["dist_name"] == "dist_mock2", "Wrong dist name"
76 | assert "SomePlugin1" in plugin_entrypoints
77 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_data_table.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from pathlib import Path
3 | from typing import List, Optional
4 |
5 | import pandas as pd
6 | from dash import dash_table, Dash
7 |
8 | from .. import WebvizPluginABC, EncodedFile
9 | from ..webviz_store import webvizstore
10 | from ..common_cache import CACHE
11 |
12 |
13 | class DataTable(WebvizPluginABC):
14 | """Adds a table to the webviz instance, using tabular data from a provided csv file.
15 | If feature is requested, the data could also come from a database.
16 |
17 | ---
18 |
19 | * **`csv_file`:** Path to the csv file containing the tabular data. Either absolute \
20 | path or relative to the configuration file.
21 | * **`sorting`:** If `True`, the table can be sorted interactively based \
22 | on data in the individual columns.
23 | * **`filtering`:** If `True`, the table can be filtered based on values in the \
24 | individual columns.
25 | * **`pagination`:** If `True`, only a subset of the table is displayed at once. \
26 | Different subsets can be viewed from 'previous/next' buttons
27 | """
28 |
29 | def __init__(
30 | self,
31 | app: Dash,
32 | csv_file: Path,
33 | sorting: bool = True,
34 | filtering: bool = True,
35 | pagination: bool = True,
36 | ):
37 |
38 | super().__init__()
39 |
40 | self.csv_file = csv_file
41 | self.df = get_data(self.csv_file)
42 | self.sorting = sorting
43 | self.filtering = filtering
44 | self.pagination = pagination
45 |
46 | self.set_callbacks(app)
47 |
48 | def add_webvizstore(self) -> List[tuple]:
49 | return [(get_data, [{"csv_file": self.csv_file}])]
50 |
51 | @property
52 | def layout(self) -> dash_table.DataTable:
53 | return dash_table.DataTable(
54 | columns=[{"name": i, "id": i} for i in self.df.columns],
55 | data=self.df.to_dict("records"),
56 | sort_action="native" if self.sorting else "none",
57 | filter_action="native" if self.filtering else "none",
58 | page_action="native" if self.pagination else "none",
59 | )
60 |
61 | def set_callbacks(self, app: Dash) -> None:
62 | @app.callback(self.plugin_data_output, self.plugin_data_requested)
63 | def _user_download_data(data_requested: Optional[int]) -> Optional[EncodedFile]:
64 | return (
65 | {
66 | "filename": "data-table.csv",
67 | "content": base64.b64encode(
68 | get_data(self.csv_file).to_csv(index=False).encode()
69 | ).decode("ascii"),
70 | "mime_type": "text/csv",
71 | }
72 | if data_requested
73 | else None
74 | )
75 |
76 |
77 | @CACHE.memoize()
78 | @webvizstore
79 | def get_data(csv_file: Path) -> pd.DataFrame:
80 | return pd.read_csv(csv_file)
81 |
--------------------------------------------------------------------------------
/webviz_config/templates/Dockerfile.jinja2:
--------------------------------------------------------------------------------
1 | ######################
2 | # Installation image #
3 | ######################
4 |
5 | FROM python:{{python_version_major}}.{{python_version_minor}}-slim AS builder
6 |
7 | # Install git and ssh, in order to install Python
8 | # packages not available from PyPI.
9 | RUN apt-get update && \
10 | apt-get install -y git {{ "ssh" if ssh_required else "" }}
11 |
12 | # Changing to non-root user early
13 | RUN useradd --create-home appuser
14 | WORKDIR /home/appuser
15 | USER appuser
16 |
17 | # Set environment variables
18 | ENV PATH="${PATH}:/home/appuser/.local/bin"{% if ssh_required %} \
19 | GIT_SSH_COMMAND="ssh $(cat ssh_identity_files) -o IdentitiesOnly=yes -o UserKnownHostsFile=./temp-known-host" \
20 | KNOWN_GITHUB_FINGERPRINT="SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"
21 |
22 | # Read GitHub deploy key environment variable, and move to
23 | # temporary files which the ssh command can read from.
24 | ARG GITHUB_DEPLOY_KEYS
25 | RUN python -c "\
26 | import base64, os, stat; from pathlib import Path; \
27 | keys = base64.b64decode(os.environ['GITHUB_DEPLOY_KEYS']).decode('utf-8').split(','); \
28 | paths = [f'github_deploy_key_{i}' for i, _ in enumerate(keys)]; \
29 | [Path(path).write_text(key) for path, key in zip(paths, keys)]; \
30 | [Path(path).chmod(stat.S_IRUSR) for path in paths]; \
31 | Path('ssh_identity_files').write_text('-i ' + ' -i '.join(paths))"
32 |
33 | # Add github.com ssh as known host, and verify that fingerprint equals known value
34 | RUN FINGERPRINT=$(ssh-keyscan -t rsa github.com | tee ./temp-known-host | ssh-keygen -lf -) && \
35 | if [ -n "${FINGERPRINT##*$KNOWN_GITHUB_FINGERPRINT*}" ] ;then \
36 | echo "ERROR: GitHub SSH fingerprint does not match known fingerprint!"; \
37 | exit 1; \
38 | fi
39 | {% endif %}
40 |
41 | # Install relevant Python packages
42 | COPY --chown=appuser requirements.txt requirements.txt
43 | RUN pip install -r requirements.txt
44 |
45 | ###############
46 | # Final image #
47 | ###############
48 |
49 | FROM python:{{python_version_major}}.{{python_version_minor}}-slim
50 |
51 | # Changing to non-root user early
52 | RUN useradd --create-home --uid 1234 appuser
53 | USER 1234
54 | WORKDIR /home/appuser
55 |
56 | # Set environment variables
57 | ENV PATH="${PATH}:/home/appuser/.local/bin" \
58 | PYTHONFAULTHANDLER=1
59 |
60 | # Copy over appuser installed Python packages
61 | COPY --chown=appuser --from=builder /home/appuser/.local /home/appuser/.local
62 |
63 | # Copy over the created Webviz application
64 | COPY --chown=appuser . dash_app
65 |
66 | EXPOSE 5000
67 |
68 | # Define startup command of container image
69 | CMD gunicorn \
70 | --access-logfile "-" \
71 | --bind 0.0.0.0:5000 \
72 | --keep-alive 120 \
73 | --preload \
74 | --workers 2 \
75 | --worker-class gthread \
76 | --worker-tmp-dir /dev/shm \
77 | --threads 2 \
78 | --timeout 100000 \
79 | "dash_app.webviz_app:server"
80 |
--------------------------------------------------------------------------------
/examples/basic_example.yaml:
--------------------------------------------------------------------------------
1 | # This file demonstrates the most basic usage of webviz in a FMU setting
2 | # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML).
3 |
4 | title: Reek Webviz Demonstration
5 |
6 | options:
7 | menu:
8 | initially_pinned: True
9 | plotly_theme:
10 | yaxis:
11 | showgrid: True
12 | gridcolor: lightgrey
13 |
14 | layout:
15 |
16 | - page: Front page
17 | content:
18 | - BannerImage:
19 | image: ./example_banner.png
20 | title: My banner image
21 | - Webviz created from configuration file.
22 | - Some other text, potentially with strange letters like Åre, Smørbukk Sør.
23 | - And even math like $e^{\pi i} + 1 = 0$.
24 |
25 | - page: Markdown example
26 | content:
27 | - Markdown:
28 | markdown_file: ./example-markdown.md
29 |
30 | - page: Table example
31 | content:
32 | - DataTable:
33 | csv_file: ./example_data.csv
34 |
35 | - page: PDF example
36 | content:
37 | - EmbedPdf:
38 | pdf_file: ./example.pdf
39 |
40 | - page: Syntax highlighting example
41 | content:
42 | - SyntaxHighlighter:
43 | filename: ./basic_example.yaml
44 |
45 | - page: Plot a table
46 | content:
47 | - TablePlotter:
48 | csv_file: ./example_data.csv
49 | # Everything below are examples of optional settings
50 | filter_cols:
51 | - Well
52 | - Segment
53 | plot_options:
54 | type: bar
55 | facet_col: Well
56 | color: Segment
57 | barmode: group
58 | filter_defaults:
59 | Well:
60 | - A-1H
61 | - A-2H
62 | - C-1H
63 | column_color_discrete_maps:
64 | # Supports css color codes, rgb and hex code.
65 | # Note that hex code needs quotes '' to not be read as a comment
66 | Segment:
67 | A: '#ff0000'
68 | B: rgb(0,255,0)
69 | C: blue
70 | contact_person:
71 | name: Ola Nordmann
72 | phone: +47 12345678
73 | email: some@email.com
74 |
75 | - page: Plot a table (locked)
76 | content:
77 | - TablePlotter:
78 | csv_file: ./example_data.csv
79 | lock: true
80 | plot_options:
81 | x: Well
82 | y: Initial reservoir pressure (bar)
83 | size: Average permeability (D)
84 | filter_cols:
85 | - Well
86 | contact_person:
87 | name: Kari Nordmann
88 | phone: 12345678
89 | email: someother@email.com
90 |
91 | - page: Pivot Table
92 | content:
93 | - PivotTable:
94 | csv_file: ./example_data.csv
95 | options:
96 | cols:
97 | - Well
98 | rows:
99 | - Segment
100 | vals:
101 | - Average permeability (D)
102 | aggregatorName: Average
103 | rendererName: Table Heatmap
104 |
--------------------------------------------------------------------------------
/webviz_config/templates/radixconfig.yaml.jinja2:
--------------------------------------------------------------------------------
1 | apiVersion: radix.equinor.com/v1
2 | kind: RadixApplication
3 | metadata:
4 | name: {{ application_name }}
5 | spec:
6 | environments:
7 | - name: prod
8 | build:
9 | from: main
10 | components:
11 | - name: auth
12 | image: quay.io/oauth2-proxy/oauth2-proxy:v7.1.3
13 | ports:
14 | - name: http
15 | port: 8000
16 | publicPort: http
17 | secrets:
18 | - OAUTH2_PROXY_CLIENT_SECRET
19 | - OAUTH2_PROXY_COOKIE_SECRET
20 | - OAUTH2_PROXY_REDIRECT_URL # Redirect URL a "secret" so it can be configured per cluster, but it's not sensitive information.
21 | environmentConfig:
22 | - environment: prod
23 | variables:
24 | OAUTH2_PROXY_SCOPE: openid offline_access profile {{ app_registration_id }}/user_impersonation email
25 | OAUTH2_PROXY_CLIENT_ID: {{ app_registration_id }}
26 | OAUTH2_PROXY_COOKIE_REFRESH: "60m" # How often should the token be refreshed. Default for Azure AD is currently 60m
27 | OAUTH2_PROXY_EMAIL_DOMAINS: "*" # Any email address registered in AD should be accepted
28 | OAUTH2_PROXY_HTTP_ADDRESS: "http://:8000" # The port oauth2_proxy listens on
29 | OAUTH2_PROXY_OIDC_ISSUER_URL: "https://login.microsoftonline.com/{{ tenant_id }}/v2.0"
30 | OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true" # Pass the access token upstream (to the proxy component)
31 | OAUTH2_PROXY_PASS_BASIC_AUTH: "false" # Disable unused default
32 | OAUTH2_PROXY_PASS_USER_HEADERS: "false" # Disable unused default
33 | OAUTH2_PROXY_PROVIDER: "oidc" # The "azure" provider doesn't seem to work properly
34 | OAUTH2_PROXY_REDIS_CONNECTION_URL: "redis://auth-state:6379" # Where to store session info (the auth-state component)
35 | OAUTH2_PROXY_SESSION_STORE_TYPE: "redis" # We're using Redis for storing session info instead of cookies (cookies would get too big)
36 | OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true" # We don't want a "click to login" page; just issue a redirect
37 | OAUTH2_PROXY_UPSTREAMS: "http://main:5000" # Where authenticated requests are routed to: the web application
38 |
39 | - name: auth-state
40 | dockerfileName: auth-state.Dockerfile
41 | ports:
42 | - name: redis
43 | port: 6379
44 |
45 | - name: main
46 | src: "."
47 | ports:
48 | - name: http
49 | port: 5000
50 | environmentConfig:
51 | - environment: prod
52 | resources:
53 | requests:
54 | memory: "400Mi"
55 | cpu: "100m"
56 | limits:
57 | memory: "8G"
58 | cpu: "1000m"
59 | volumeMounts:
60 | - type: azure-blob
61 | name: appstorage
62 | storage: {{ azure_storage_container_name }}
63 | path: /home/appuser/dash_app/resources
64 | uid: 1234
65 | dnsAppAlias:
66 | environment: prod
67 | component: auth
68 |
--------------------------------------------------------------------------------
/tests/test_docstring.py:
--------------------------------------------------------------------------------
1 | from webviz_config._docs._build_docs import _split_docstring
2 |
3 |
4 | def test_split_docstring():
5 | tests = [
6 | (
7 | "This is a test with only description.",
8 | ["This is a test with only description."],
9 | ),
10 | (
11 | (
12 | " This is a test with only description, "
13 | "but which has leading and trailing spaces. "
14 | ),
15 | [
16 | (
17 | "This is a test with only description, "
18 | "but which has leading and trailing spaces."
19 | )
20 | ],
21 | ),
22 | (
23 | "Test with some newlines\n\n \n and a 4 space indent",
24 | ["Test with some newlines\n\n\nand a 4 space indent"],
25 | ),
26 | (
27 | "Test with a \n 4 space and a \n 2 space indent\n",
28 | ["Test with a \n 4 space and a \n2 space indent"],
29 | ),
30 | (
31 | "Test with a \n 4 space and a \n 2 space indent\n",
32 | ["Test with a \n 4 space and a \n2 space indent"],
33 | ),
34 | (
35 | (
36 | " This is a test with description, arguments and data input."
37 | "\n\n ---\n\n The docstring is indented by 4 spaces:\n "
38 | "* The argument list has a sub list indented by 8 spaces\n like this."
39 | "\n ---\n The data input section is not very interesting."
40 | ),
41 | [
42 | "This is a test with description, arguments and data input.\n",
43 | (
44 | "\nThe docstring is indented by 4 spaces:\n"
45 | "* The argument list has a sub list indented by 8 spaces\n like this."
46 | ),
47 | "The data input section is not very interesting.",
48 | ],
49 | ),
50 | (
51 | (
52 | "\tThis is a test with description, arguments and data input,"
53 | " where indents are made with tabs and not spaces-"
54 | "\n\n\t---\n\n\tThe docstring is indented by 1 tab:\n"
55 | "\t* The argument list has a sub list indented by 2 tabs\n\t\tlike this."
56 | "\n\t---\n\tThe data input section is not very interesting."
57 | ),
58 | [
59 | (
60 | "This is a test with description, arguments and data input,"
61 | " where indents are made with tabs and not spaces-\n"
62 | ),
63 | (
64 | "\nThe docstring is indented by 1 tab:\n"
65 | "* The argument list has a sub list indented by 2 tabs\n\tlike this."
66 | ),
67 | "The data input section is not very interesting.",
68 | ],
69 | ),
70 | (
71 | "This is a test with only description, which includes a linebreak.\n",
72 | ["This is a test with only description, which includes a linebreak."],
73 | ),
74 | ]
75 | for test in tests:
76 | assert _split_docstring(test[0]) == test[1]
77 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/azure_configuration.py:
--------------------------------------------------------------------------------
1 | import functools
2 | from typing import Dict
3 |
4 | from . import interactive_terminal
5 | from . import azure_cli
6 |
7 |
8 | def azure_configuration() -> Dict[str, str]:
9 | # Select Azure subscription
10 | interactive_terminal.terminal_title("Azure subscription")
11 | subscription = interactive_terminal.user_input_from_list(
12 | env_var="WEBVIZ_AZURE_SUBSCRIPTION",
13 | noun="subscription",
14 | choices=azure_cli.subscriptions(),
15 | )
16 |
17 | # Select resource group
18 | interactive_terminal.terminal_title("Azure resource group")
19 | resource_group = interactive_terminal.user_input_from_list(
20 | env_var="WEBVIZ_AZURE_RESOURCE_GROUP",
21 | noun="resource group",
22 | choices=azure_cli.resource_groups(subscription),
23 | )
24 |
25 | # Choose app registration
26 | interactive_terminal.terminal_title("Azure app registration")
27 | display_name = interactive_terminal.user_input_optional_reuse(
28 | env_var="WEBVIZ_AZURE_APP_REGISTRATION_DISPLAY_NAME",
29 | noun="app registration",
30 | exists_function=azure_cli.existing_app_registration,
31 | )
32 |
33 | # Choose Azure storage account
34 | storage_account_exists = functools.partial(
35 | azure_cli.storage_account_exists,
36 | subscription=subscription,
37 | resource_group=resource_group,
38 | )
39 |
40 | interactive_terminal.terminal_title("Azure storage account")
41 | storage_account_name = interactive_terminal.user_input_optional_reuse(
42 | env_var="WEBVIZ_AZURE_STORAGE_ACCOUNT",
43 | noun="storage account",
44 | exists_function=storage_account_exists,
45 | regex="^[a-z0-9]{3,24}$",
46 | )
47 |
48 | if not storage_account_exists(storage_account_name):
49 | azure_cli.create_storage_account(
50 | subscription, resource_group, storage_account_name
51 | )
52 | print("✓ Created storage account.")
53 |
54 | # Choose Azure storage container
55 | storage_container_exists = functools.partial(
56 | azure_cli.storage_container_exists,
57 | account_name=storage_account_name,
58 | subscription=subscription,
59 | resource_group=resource_group,
60 | )
61 |
62 | interactive_terminal.terminal_title("Azure storage container")
63 |
64 | storage_container_name = interactive_terminal.user_input_optional_reuse(
65 | env_var="WEBVIZ_AZURE_STORAGE_CONTAINER",
66 | noun="storage container",
67 | exists_function=storage_container_exists,
68 | )
69 |
70 | if not storage_container_exists(storage_container_name):
71 | azure_cli.create_storage_container(
72 | subscription=subscription,
73 | resource_group=resource_group,
74 | storage_account=storage_account_name,
75 | container=storage_container_name,
76 | )
77 | print(f"✓ Created storage container '{storage_container_name}'.")
78 |
79 | return {
80 | "subscription": subscription,
81 | "resource_group": resource_group,
82 | "storage_account_name": storage_account_name,
83 | "storage_account_key_secret": azure_cli.get_storage_account_access_key(
84 | subscription=subscription,
85 | resource_group=resource_group,
86 | account_name=storage_account_name,
87 | ),
88 | "storage_container_name": storage_container_name,
89 | "display_name": display_name,
90 | }
91 |
--------------------------------------------------------------------------------
/examples/basic_example_advanced_menu.yaml:
--------------------------------------------------------------------------------
1 | # This file demonstrates the most basic usage of webviz in a FMU setting
2 | # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML).
3 |
4 | title: Reek Webviz Demonstration
5 |
6 | options:
7 | menu:
8 | show_logo: True
9 | bar_position: left
10 | drawer_position: left
11 | initially_pinned: True
12 |
13 | layout:
14 | - section: Section
15 | content:
16 | - page: Front page
17 | icon: home
18 | content:
19 | - BannerImage:
20 | image: ./example_banner.png
21 | title: My banner image
22 | - Webviz created from configuration file.
23 | - Some other text, potentially with strange letters like Åre, Smørbukk Sør.
24 |
25 | - group: Other
26 | icon: label
27 | content:
28 | - page: Markdown example
29 | content:
30 | - Markdown:
31 | markdown_file: ./example-markdown.md
32 |
33 | - page: PDF example
34 | content:
35 | - EmbedPdf:
36 | pdf_file: ./example.pdf
37 |
38 | - page: Syntax highlighting example
39 | content:
40 | - SyntaxHighlighter:
41 | filename: ./basic_example.yaml
42 |
43 | - group: Tables
44 | icon: table_chart
45 | content:
46 | - page: Table example
47 | content:
48 | - DataTable:
49 | csv_file: ./example_data.csv
50 |
51 | - page: Plot a table
52 | content:
53 | - TablePlotter:
54 | csv_file: ./example_data.csv
55 | # Everything below are examples of optional settings
56 | filter_cols:
57 | - Well
58 | - Segment
59 | - Average permeability (D)
60 | plot_options:
61 | type: bar
62 | facet_col: Well
63 | color: Segment
64 | barmode: group
65 | filter_defaults:
66 | Well:
67 | - A-1H
68 | - A-2H
69 | - C-1H
70 | column_color_discrete_maps:
71 | # Supports css color codes, rgb and hex code.
72 | # Note that hex code needs quotes '' to not be read as a comment
73 | Segment:
74 | A: '#ff0000'
75 | B: rgb(0,255,0)
76 | C: blue
77 | contact_person:
78 | name: Ola Nordmann
79 | phone: +47 12345678
80 | email: some@email.com
81 |
82 | - page: Plot a table (locked)
83 | content:
84 | - TablePlotter:
85 | csv_file: ./example_data.csv
86 | lock: true
87 | plot_options:
88 | x: Well
89 | y: Initial reservoir pressure (bar)
90 | size: Average permeability (D)
91 | facet_col: Segment
92 | contact_person:
93 | name: Kari Nordmann
94 | phone: 12345678
95 | email: someother@email.com
96 |
97 | - page: Pivot Table
98 | content:
99 | - PivotTable:
100 | csv_file: ./example_data.csv
101 | options:
102 | cols:
103 | - Well
104 | rows:
105 | - Segment
106 | vals:
107 | - Average permeability (D)
108 | aggregatorName: Average
109 | rendererName: Table Heatmap
110 |
--------------------------------------------------------------------------------
/webviz_config/templates/copy_data_template.py.jinja2:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import logging
5 | import logging.config
6 | import datetime
7 | from pathlib import Path
8 |
9 | import dash
10 |
11 | import webviz_config
12 | import webviz_config.plugins
13 | from webviz_config.themes import installed_themes
14 | from webviz_config.webviz_store import WEBVIZ_STORAGE
15 | from webviz_config.webviz_assets import WEBVIZ_ASSETS
16 | from webviz_config.common_cache import CACHE
17 | from webviz_config.webviz_instance_info import WebvizRunMode, WEBVIZ_INSTANCE_INFO
18 | from webviz_config.webviz_factory_registry import WEBVIZ_FACTORY_REGISTRY
19 | from webviz_config.utils import deprecate_webviz_settings_attribute_in_dash_app
20 |
21 | logging.basicConfig(level=logging.{{ loglevel }})
22 |
23 | theme = webviz_config.WebvizConfigTheme("{{ theme_name }}")
24 | theme.from_json((Path(__file__).resolve().parent / "theme_settings.json").read_text())
25 | theme.plotly_theme_layout_update({{ options.plotly_theme }})
26 |
27 | dash._dash_renderer._set_react_version("18.2.0")
28 |
29 | app = dash.Dash()
30 | app.config.suppress_callback_exceptions = True
31 |
32 | # Trigger import of plugins used, such that SHARED_SETTINGS_SUBSCRIPTIONS
33 | # is populated before setting webviz_settings
34 | {% for page in pageContents %}
35 | {% for content in page.content %}
36 | {% if content is not string %}
37 | webviz_config.plugins.{{ content._call_signature[0].split('(')[0]}}
38 | {% endif %}
39 | {% endfor %}
40 | {% endfor %}
41 |
42 | # Create the common webviz_setting object that will get passed as an
43 | # argument to all plugins that request it.
44 | webviz_settings: webviz_config.WebvizSettings = webviz_config.WebvizSettings(
45 | shared_settings=webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
46 | {{ shared_settings }}, {{ config_folder }}, {{ False }}
47 | ),
48 | theme=theme,
49 | )
50 |
51 | # Previously, webviz_settings was piggybacked onto the Dash application object.
52 | # For a period of time, keep it but mark access to the webviz_settings attribute
53 | # on the Dash application object as deprecated.
54 | deprecate_webviz_settings_attribute_in_dash_app()
55 | app._deprecated_webviz_settings = {
56 | "shared_settings" : webviz_settings.shared_settings,
57 | "theme" : webviz_settings.theme
58 | }
59 |
60 | {% if logging_config_dict is defined %}
61 | # Apply a logging config dict as specified via the --logconfig command line argument.
62 | logging.config.dictConfig({{ logging_config_dict }})
63 | {% endif %}
64 |
65 | CACHE.init_app(app.server)
66 |
67 | storage_folder = Path(__file__).resolve().parent / "resources" / "webviz_storage"
68 |
69 | WEBVIZ_STORAGE.storage_folder = storage_folder
70 |
71 | WEBVIZ_INSTANCE_INFO.initialize(
72 | dash_app=app,
73 | run_mode=WebvizRunMode.BUILDING_PORTABLE,
74 | theme=theme,
75 | storage_folder=storage_folder
76 | )
77 |
78 | WEBVIZ_FACTORY_REGISTRY.initialize({{ internal_factory_settings if internal_factory_settings is defined else None }})
79 |
80 | plugins = []
81 |
82 | {% for page in pageContents %}
83 | {% for content in page.content %}
84 | {% if content is not string %}
85 | plugins.append(webviz_config.plugins.{{ content._call_signature[0] }})
86 | {% endif %}
87 | {% endfor %}
88 | {% endfor %}
89 |
90 | WEBVIZ_FACTORY_REGISTRY.cleanup_resources_after_plugin_init()
91 |
92 | for plugin in plugins:
93 | if hasattr(plugin, "add_webvizstore"):
94 | WEBVIZ_STORAGE.register_function_arguments(plugin.add_webvizstore())
95 |
96 | WEBVIZ_ASSETS.make_portable(Path(__file__).resolve().parent / "resources" / "assets")
97 |
98 | WEBVIZ_STORAGE.build_store()
99 |
--------------------------------------------------------------------------------
/webviz_config/_localhost_token.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 |
4 | import flask
5 |
6 | from ._is_reload_process import is_reload_process
7 | from ._oauth2 import Oauth2
8 |
9 |
10 | class LocalhostToken:
11 | """Uses a method similar to jupyter notebook. This method is only used during
12 | interactive usage on localhost, and the workflow is as follows:
13 |
14 | - During the flask app building, a one-time-token (ott) and a cookie_token
15 | is generated.
16 | - When the app is ready, the user needs to "login" using this
17 | one-time-token in the url (http://localhost:{port}?ott={token})
18 | - If ott is valid - a cookie with a separate token is set, and the
19 | one-time-token is discarded. The cookie is then used for subsequent
20 | requests.
21 |
22 | If the user fails both providing a valid one-time-token and a valid cookie
23 | token, all localhost requests gets a 401.
24 |
25 | If the app is in non-portable mode, the one-time-token and
26 | cookie token are reused on app reload (in order to ensure live reload
27 | works without requiring new login).
28 |
29 | The port is used as a postfix on the cookie name in order to make sure that
30 | two different localhost applications running simultaneously do not interfere.
31 | """
32 |
33 | def __init__(self, app: flask.app.Flask, port: int, oauth2: Oauth2 = None):
34 | self._app = app
35 | self._port = port
36 | self._oauth2 = oauth2
37 |
38 | if not is_reload_process():
39 | # One time token (per run) user has to provide
40 | # when visiting the localhost app the first time.
41 | self._ott = os.environ["WEBVIZ_OTT"] = LocalhostToken.generate_token()
42 |
43 | # This is the cookie token set in the users browser after
44 | # successfully providing the one time token
45 | self._cookie_token = os.environ[
46 | "WEBVIZ_COOKIE_TOKEN"
47 | ] = LocalhostToken.generate_token()
48 |
49 | else:
50 | self._ott = os.environ["WEBVIZ_OTT"]
51 | self._cookie_token = os.environ["WEBVIZ_COOKIE_TOKEN"]
52 |
53 | self._ott_validated = False
54 | self.set_request_decorators()
55 |
56 | @staticmethod
57 | def generate_token() -> str:
58 | return secrets.token_urlsafe(nbytes=64)
59 |
60 | @property
61 | def one_time_token(self) -> str:
62 | return self._ott
63 |
64 | def set_request_decorators(self) -> None:
65 | # pylint: disable=inconsistent-return-statements
66 | @self._app.before_request
67 | def _check_for_ott_or_cookie(): # type: ignore[no-untyped-def]
68 |
69 | if not self._ott_validated and self._ott == flask.request.args.get("ott"):
70 | self._ott_validated = True
71 | flask.g.set_cookie_token = True
72 | return flask.redirect(flask.request.base_url)
73 |
74 | if self._cookie_token == flask.request.cookies.get(
75 | f"cookie_token_{self._port}"
76 | ):
77 | self._ott_validated = True
78 |
79 | if self._oauth2:
80 | return self._oauth2.check_access_token()
81 |
82 | else:
83 | flask.abort(401)
84 |
85 | @self._app.after_request
86 | def _set_cookie_token_in_response(
87 | response: flask.wrappers.Response,
88 | ) -> flask.wrappers.Response:
89 | if flask.g.get("set_cookie_token", False):
90 | response.set_cookie(
91 | key=f"cookie_token_{self._port}", value=self._cookie_token
92 | )
93 | return response
94 |
--------------------------------------------------------------------------------
/webviz_config/webviz_instance_info.py:
--------------------------------------------------------------------------------
1 | import copy
2 | from enum import Enum
3 | from pathlib import Path
4 | from typing import Optional
5 |
6 | from dash import Dash
7 |
8 | from ._theme_class import WebvizConfigTheme
9 |
10 |
11 | class WebvizRunMode(Enum):
12 | NON_PORTABLE = 1
13 | PORTABLE = 2
14 | BUILDING_PORTABLE = 3
15 |
16 |
17 | class WebvizInstanceInfo:
18 | """Contains global information regarding the running webviz app instance, exposed
19 | globally through WEBVIZ_INSTANCE_INFO.
20 |
21 | Note that this class utilizes a two-stage initialization approach which renders it
22 | useless until the initialize() method has been called. It is assumed that
23 | initialization will be done early during application execution, typically as part
24 | of the webviz_app.py / jinja2 template.
25 | """
26 |
27 | __slots__ = (
28 | "_is_initialized",
29 | "_dash_app",
30 | "_run_mode",
31 | "_theme",
32 | "_storage_folder",
33 | )
34 |
35 | def __init__(self) -> None:
36 | self._is_initialized: bool = False
37 | self._dash_app: Optional[Dash] = None
38 | self._run_mode: Optional[WebvizRunMode] = None
39 | self._theme: Optional[WebvizConfigTheme] = None
40 | self._storage_folder: Optional[Path] = None
41 |
42 | def initialize(
43 | self,
44 | dash_app: Dash,
45 | run_mode: WebvizRunMode,
46 | theme: WebvizConfigTheme,
47 | storage_folder: Path,
48 | ) -> None:
49 | """This function is responsible for the actual initialization of the object instance.
50 | None of the access methods in this class are valid until initialize() has been called.
51 | This function will be called as part of the webviz_app.py / jinja2 template.
52 | """
53 | if self._is_initialized:
54 | raise RuntimeError("Registry already initialized")
55 |
56 | if not isinstance(dash_app, Dash):
57 | raise TypeError("dash_app must be of type Dash")
58 | self._dash_app = dash_app
59 |
60 | if not isinstance(run_mode, WebvizRunMode):
61 | raise TypeError("run_mode must be of type WebvizRunMode")
62 | self._run_mode = run_mode
63 |
64 | if not isinstance(theme, WebvizConfigTheme):
65 | raise TypeError("theme must be of type WebvizConfigTheme")
66 | self._theme = theme
67 |
68 | if not isinstance(storage_folder, Path):
69 | raise TypeError("storage_folder must be of type Path")
70 | self._storage_folder = storage_folder
71 |
72 | self._is_initialized = True
73 |
74 | @property
75 | def dash_app(self) -> Dash:
76 | if not self._is_initialized or self._dash_app is None:
77 | raise RuntimeError("WebvizInstanceInfo is not yet initialized")
78 |
79 | return self._dash_app
80 |
81 | @property
82 | def run_mode(self) -> WebvizRunMode:
83 | if not self._is_initialized or self._run_mode is None:
84 | raise RuntimeError("WebvizInstanceInfo is not yet initialized")
85 |
86 | return self._run_mode
87 |
88 | @property
89 | def theme(self) -> WebvizConfigTheme:
90 | if not self._is_initialized or self._theme is None:
91 | raise RuntimeError("WebvizInstanceInfo is not yet initialized")
92 |
93 | # The theme class is mutable, so return a copy
94 | return copy.deepcopy(self._theme)
95 |
96 | @property
97 | def storage_folder(self) -> Path:
98 | if not self._is_initialized or self._storage_folder is None:
99 | raise RuntimeError("WebvizInstanceInfo is not yet initialized")
100 |
101 | return self._storage_folder
102 |
103 |
104 | WEBVIZ_INSTANCE_INFO = WebvizInstanceInfo()
105 |
--------------------------------------------------------------------------------
/webviz_config/webviz_factory_registry.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict, Optional, Type, TypeVar
2 | import warnings
3 |
4 | from .webviz_factory import WebvizFactory
5 | from .webviz_instance_info import WebvizInstanceInfo, WEBVIZ_INSTANCE_INFO
6 |
7 | T = TypeVar("T", bound=WebvizFactory)
8 |
9 |
10 | class WebvizFactoryRegistry:
11 | """Global registry for factories that allows the actual factory instances
12 | to be shared between plugins. Also facilitates storage of optional factory
13 | settings that are read from the YAML config file for later consumption when
14 | an actual factory gets created and added to the registry.
15 | The registry is exposed globally through WEBVIZ_FACTORY_REGISTRY below.
16 | This is also the reason for the two-stage initialization approach. Note that
17 | the registry instance is useless until the initialize() method has been called.
18 |
19 | Note that this functionality is provisional/experimental, and will not necessarily
20 | see a deprecation phase and "may deviate from the usual version semantics."
21 | """
22 |
23 | def __init__(self) -> None:
24 | self._is_initialized: bool = False
25 | self._factory_settings_dict: Dict[str, Any] = {}
26 | self._factories: Dict[Type, WebvizFactory] = {}
27 |
28 | def initialize(
29 | self,
30 | factory_settings_dict: Optional[Dict[str, Any]],
31 | ) -> None:
32 | """Does the actual initialization of the object instance.
33 | This function will be called as part of the webviz_app.py / jinja2 template.
34 | """
35 | if self._is_initialized:
36 | raise RuntimeError("Registry already initialized")
37 |
38 | if factory_settings_dict:
39 | if not isinstance(factory_settings_dict, dict):
40 | raise TypeError("factory_settings_dict must be of type dict")
41 | self._factory_settings_dict = factory_settings_dict
42 |
43 | self._is_initialized = True
44 |
45 | def set_factory(self, factory_class: Type[T], factory_obj: T) -> None:
46 | if not self._is_initialized:
47 | raise RuntimeError("Illegal access, factory registry is not initialized")
48 |
49 | if not isinstance(factory_obj, factory_class):
50 | raise TypeError("The type of the factory does not match factory_class")
51 |
52 | self._factories[factory_class] = factory_obj
53 |
54 | def get_factory(self, factory_class: Type[T]) -> Optional[T]:
55 | if not self._is_initialized:
56 | raise RuntimeError("Illegal access, factory registry is not initialized")
57 |
58 | if not factory_class in self._factories:
59 | return None
60 |
61 | factory_obj = self._factories[factory_class]
62 | if not isinstance(factory_obj, factory_class):
63 | raise TypeError("The stored factory object has wrong type")
64 |
65 | return factory_obj
66 |
67 | def cleanup_resources_after_plugin_init(self) -> None:
68 | if not self._is_initialized:
69 | raise RuntimeError("Illegal access, factory registry is not initialized")
70 |
71 | for factory in self._factories.values():
72 | factory.cleanup_resources_after_plugin_init()
73 |
74 | @property
75 | def all_factory_settings(self) -> Dict[str, Any]:
76 | if not self._is_initialized:
77 | raise RuntimeError("Illegal access, factory registry is not initialized")
78 |
79 | return self._factory_settings_dict
80 |
81 | @property
82 | def app_instance_info(self) -> WebvizInstanceInfo:
83 | warnings.warn(
84 | "Accessing WebvizInstanceInfo through WebvizFactoryRegistry has been deprecated, "
85 | "please use global WEBVIZ_INSTANCE_INFO instead",
86 | DeprecationWarning,
87 | )
88 |
89 | return WEBVIZ_INSTANCE_INFO
90 |
91 |
92 | WEBVIZ_FACTORY_REGISTRY = WebvizFactoryRegistry()
93 |
--------------------------------------------------------------------------------
/tests/unit_tests/test_webviz_instance_info.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import cast
3 |
4 | import pytest
5 | from dash import Dash
6 |
7 | from webviz_config import WebvizConfigTheme
8 | from webviz_config.webviz_instance_info import WebvizInstanceInfo, WebvizRunMode
9 |
10 |
11 | def test_construction_and_basic_access() -> None:
12 | my_dash_app = Dash("dummyAppName")
13 | my_run_mode = WebvizRunMode.NON_PORTABLE
14 | my_theme = WebvizConfigTheme("dummyThemeName")
15 | my_storage_folder = Path("dummyPath")
16 |
17 | instance_info = WebvizInstanceInfo()
18 | instance_info.initialize(
19 | dash_app=my_dash_app,
20 | run_mode=my_run_mode,
21 | theme=my_theme,
22 | storage_folder=my_storage_folder,
23 | )
24 |
25 | assert instance_info.dash_app is my_dash_app
26 | assert instance_info.run_mode is WebvizRunMode.NON_PORTABLE
27 | assert instance_info.theme.__dict__ == my_theme.__dict__
28 | assert instance_info.storage_folder == Path("dummyPath")
29 |
30 |
31 | def test_that_construction_with_invalid_types_throw() -> None:
32 | dash_app = Dash("dummyAppName")
33 | run_mode = WebvizRunMode.NON_PORTABLE
34 | theme = WebvizConfigTheme("dummyThemeName")
35 | storage_folder = Path("dummyPath")
36 |
37 | with pytest.raises(TypeError):
38 | WebvizInstanceInfo().initialize(
39 | dash_app=cast(Dash, None),
40 | run_mode=run_mode,
41 | theme=theme,
42 | storage_folder=storage_folder,
43 | )
44 | with pytest.raises(TypeError):
45 | WebvizInstanceInfo().initialize(
46 | dash_app=dash_app,
47 | run_mode=cast(WebvizRunMode, None),
48 | theme=theme,
49 | storage_folder=storage_folder,
50 | )
51 | with pytest.raises(TypeError):
52 | WebvizInstanceInfo().initialize(
53 | dash_app=dash_app,
54 | run_mode=run_mode,
55 | theme=cast(WebvizConfigTheme, None),
56 | storage_folder=storage_folder,
57 | )
58 | with pytest.raises(TypeError):
59 | WebvizInstanceInfo().initialize(
60 | dash_app=dash_app,
61 | run_mode=run_mode,
62 | theme=theme,
63 | storage_folder=cast(Path, None),
64 | )
65 |
66 |
67 | def test_immutability() -> None:
68 | my_dash_app = Dash("dummyAppName")
69 | my_run_mode = WebvizRunMode.NON_PORTABLE
70 | my_theme = WebvizConfigTheme("dummyThemeName")
71 | my_storage_folder = Path("dummyPath")
72 |
73 | instance_info = WebvizInstanceInfo()
74 | instance_info.initialize(
75 | dash_app=my_dash_app,
76 | run_mode=my_run_mode,
77 | theme=my_theme,
78 | storage_folder=my_storage_folder,
79 | )
80 |
81 | # Ony allowed to initialize once
82 | with pytest.raises(RuntimeError):
83 | instance_info.initialize(
84 | dash_app=my_dash_app,
85 | run_mode=my_run_mode,
86 | theme=my_theme,
87 | storage_folder=my_storage_folder,
88 | )
89 |
90 | # This is ok and necessary since we want to share the actual dash app
91 | assert instance_info.dash_app is my_dash_app
92 |
93 | # This two are also ok since integer enums and Path themselves is immutable
94 | assert instance_info.run_mode is my_run_mode
95 | assert instance_info.storage_folder is my_storage_folder
96 |
97 | returned_theme = instance_info.theme
98 | assert returned_theme is not my_theme
99 | assert isinstance(returned_theme, WebvizConfigTheme)
100 | assert returned_theme.__dict__ == my_theme.__dict__
101 | my_theme.theme_name = "MODIFIED"
102 | assert returned_theme.__dict__ != my_theme.__dict__
103 |
104 | with pytest.raises(AttributeError):
105 | # pylint: disable=assigning-non-slot
106 | instance_info.some_new_attribute = "myAttributeValue" # type: ignore[attr-defined]
107 |
--------------------------------------------------------------------------------
/tests/test_table_plotter.py:
--------------------------------------------------------------------------------
1 | import time
2 | from pathlib import Path
3 |
4 | import dash
5 | from dash.testing.composite import DashComposite
6 |
7 | from webviz_config import WebvizSettings
8 | from webviz_config.common_cache import CACHE
9 | from webviz_config.themes import default_theme
10 | from webviz_config.generic_plugins import _table_plotter
11 |
12 |
13 | def test_table_plotter(dash_duo: DashComposite) -> None:
14 |
15 | app = dash.Dash(__name__)
16 | app.config.suppress_callback_exceptions = True
17 | CACHE.init_app(app.server)
18 | webviz_settings = WebvizSettings({}, default_theme)
19 | csv_file = Path("./tests/data/example_data.csv")
20 | page = _table_plotter.TablePlotter(app, webviz_settings, csv_file)
21 | app.layout = page.layout
22 | dash_duo.start_server(app)
23 |
24 | # Wait for the app to render(there is probably a better way...)
25 | time.sleep(5)
26 |
27 | # Checking that no plot options are defined
28 | assert not page.plot_options
29 | # Check that filter is not active
30 | assert not page.use_filter
31 |
32 | # Checking that the correct plot type is initialized
33 | plot_dd = dash_duo.find_element("#" + page.uuid("plottype"))
34 | assert plot_dd.text == "scatter"
35 |
36 | # Checking that only the relevant options are shown
37 | for plot_option in page.plot_args.keys():
38 | plot_option_dd = dash_duo.find_element("#" + page.uuid(f"div-{plot_option}"))
39 | if plot_option not in page.plots["scatter"]:
40 | assert plot_option_dd.get_attribute("style") == "display: none;"
41 |
42 | # Checking that options are initialized correctly
43 | for option in ["x", "y"]:
44 | plot_option_dd = dash_duo.find_element("#" + page.uuid(f"dropdown-{option}"))
45 | assert plot_option_dd.text == "Well"
46 |
47 |
48 | def test_table_plotter_filter(dash_duo: DashComposite) -> None:
49 |
50 | app = dash.Dash(__name__)
51 | app.config.suppress_callback_exceptions = True
52 | CACHE.init_app(app.server)
53 | webviz_settings = WebvizSettings({}, default_theme)
54 | csv_file = Path("./tests/data/example_data.csv")
55 | page = _table_plotter.TablePlotter(
56 | app, webviz_settings, csv_file, filter_cols=["Well"]
57 | )
58 | app.layout = page.layout
59 | dash_duo.start_server(app)
60 |
61 | # Wait for the app to render(there is probably a better way...)
62 | time.sleep(5)
63 |
64 | # Checking that no plot options are defined
65 | assert not page.plot_options
66 | # Check that filter is active
67 | assert page.use_filter
68 | assert page.filter_cols == ["Well"]
69 |
70 | # Checking that the correct plot type is initialized
71 | plot_dd = dash_duo.find_element("#" + page.uuid("plottype"))
72 | assert plot_dd.text == "scatter"
73 |
74 | # Checking that options are initialized correctly
75 | for option in ["x", "y"]:
76 | plot_option_dd = dash_duo.find_element("#" + page.uuid(f"dropdown-{option}"))
77 | assert plot_option_dd.text == "Well"
78 |
79 |
80 | def test_initialized_table_plotter(dash_duo: DashComposite) -> None:
81 |
82 | app = dash.Dash(__name__)
83 | app.css.config.serve_locally = True
84 | app.scripts.config.serve_locally = True
85 | app.config.suppress_callback_exceptions = True
86 | CACHE.init_app(app.server)
87 | webviz_settings = WebvizSettings({}, default_theme)
88 | csv_file = Path("./tests/data/example_data.csv")
89 | plot_options = dict(
90 | x="Well",
91 | y="Initial reservoir pressure (bar)",
92 | size="Average permeability (D)",
93 | facet_col="Segment",
94 | )
95 |
96 | page = _table_plotter.TablePlotter(
97 | app, webviz_settings, csv_file, lock=True, plot_options=plot_options
98 | )
99 | app.layout = page.layout
100 | dash_duo.start_server(app)
101 |
102 | # Wait for the app to render(there is probably a better way...)
103 |
104 | # Checking that plot options are defined
105 | assert page.plot_options == plot_options
106 | assert page.lock
107 |
--------------------------------------------------------------------------------
/webviz_config/testing/_composite.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import pathlib
4 |
5 | from dash.testing.composite import Browser
6 | import dash
7 | import webviz_core_components as wcc
8 |
9 | from webviz_config.common_cache import CACHE
10 | from webviz_config.themes import default_theme
11 | from webviz_config.webviz_factory_registry import WEBVIZ_FACTORY_REGISTRY
12 | from webviz_config.webviz_instance_info import WEBVIZ_INSTANCE_INFO, WebvizRunMode
13 | from webviz_config import WebvizPluginABC
14 |
15 | from ._webviz_ids import WebvizIds
16 |
17 |
18 | class WebvizComposite(Browser):
19 | def __init__(self, server: Any, **kwargs: Any) -> None:
20 | super().__init__(**kwargs)
21 | self.app = dash.Dash(__name__)
22 | self.server = server
23 | self.plugin: WebvizPluginABC
24 | self.init_app()
25 |
26 | def init_app(self) -> None:
27 | WEBVIZ_INSTANCE_INFO.initialize(
28 | dash_app=self.app,
29 | run_mode=WebvizRunMode.NON_PORTABLE,
30 | theme=default_theme,
31 | storage_folder=pathlib.Path(__file__).resolve().parent,
32 | )
33 | try:
34 | WEBVIZ_FACTORY_REGISTRY.initialize(None)
35 | except RuntimeError:
36 | pass
37 |
38 | self.app.css.config.serve_locally = True
39 | self.app.scripts.config.serve_locally = True
40 | self.app.config.suppress_callback_exceptions = True
41 | CACHE.init_app(self.app.server)
42 |
43 | def start_server(self, plugin: WebvizPluginABC, **kwargs: Any) -> None:
44 | """Start the local server with app."""
45 |
46 | self.app.layout = dash.html.Div(
47 | className=WebvizIds.LAYOUT_WRAPPER,
48 | children=[
49 | wcc.WebvizContentManager(
50 | id=WebvizIds.CONTENT_MANAGER,
51 | children=[
52 | wcc.WebvizSettingsDrawer(
53 | id=WebvizIds.SETTINGS_DRAWER,
54 | children=plugin.get_all_settings(),
55 | ),
56 | wcc.WebvizPluginsWrapper(
57 | id=WebvizIds.PLUGINS_WRAPPER,
58 | children=plugin.plugin_layout(),
59 | ),
60 | ],
61 | ),
62 | ],
63 | )
64 | self.plugin = plugin
65 | # start server with app and pass Dash arguments
66 | self.server(self.app, **kwargs)
67 |
68 | # set the default server_url, it implicitly call wait_for_page
69 | self.server_url = self.server.url
70 |
71 | def toggle_webviz_settings_drawer(self) -> None:
72 | """Open the plugin settings drawer"""
73 | self.wait_for_element(WebvizIds.SETTINGS_DRAWER_TOGGLE).click()
74 |
75 | def toggle_webviz_settings_group(self, settings_group_id: str) -> None:
76 | """Open the respective settings group in the settings drawer"""
77 | self.wait_for_element(
78 | f"#{settings_group_id} > .WebvizSettingsGroup__Title"
79 | ).click()
80 |
81 | def shared_settings_group_unique_component_id(
82 | self, settings_group_id: str, component_unique_id: str
83 | ) -> str:
84 | """Returns the element id of a component in a shared settings group"""
85 | unique_id = (
86 | self.plugin.shared_settings_group(settings_group_id)
87 | .component_unique_id(component_unique_id)
88 | .to_string()
89 | )
90 | return f"#{unique_id}"
91 |
92 | def view_settings_group_unique_component_id(
93 | self, view_id: str, settings_group_id: str, component_unique_id: str
94 | ) -> str:
95 | """Returns the element id of a component in a view settings group"""
96 | unique_id = (
97 | self.plugin.view(view_id)
98 | .settings_group(settings_group_id)
99 | .component_unique_id(component_unique_id)
100 | .to_string()
101 | )
102 | return f"#{unique_id}"
103 |
--------------------------------------------------------------------------------
/webviz_config/utils/_localhost_open_browser.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import urllib
4 | import warnings
5 | import threading
6 | import webbrowser
7 |
8 | from .._is_reload_process import is_reload_process
9 | from .._user_preferences import get_user_preference
10 | from . import terminal_colors
11 |
12 |
13 | class LocalhostOpenBrowser:
14 | # pylint: disable=too-few-public-methods
15 |
16 | def __init__(self, port: int, token: str):
17 | self._port = port
18 | self._token = token
19 |
20 | if not is_reload_process():
21 | # Only open new browser tab if not a reload process
22 | threading.Thread(target=self._timer).start()
23 |
24 | def _timer(self) -> None:
25 | """Waits until the app is ready, and then opens the page
26 | in the default browser.
27 | """
28 |
29 | timeout = 120 # maximum number of seconds to wait before timeout
30 |
31 | for _ in range(timeout):
32 | if self._app_ready():
33 | self._open_new_tab()
34 | return
35 |
36 | time.sleep(1)
37 |
38 | print(
39 | f"WARNING: Webviz application still not ready after {timeout}s.\n"
40 | "Will not open browser automatically. Your private one-time login link:\n"
41 | f"{self._url(with_token=True)}"
42 | )
43 |
44 | def _url(self, with_token: bool = False) -> str:
45 | return (
46 | f"http://localhost:{self._port}"
47 | + f"{'?ott=' + self._token if with_token else ''}"
48 | )
49 |
50 | @staticmethod
51 | def _get_browser_controller() -> webbrowser.BaseBrowser:
52 |
53 | if get_user_preference("browser") is not None:
54 | try:
55 | return webbrowser.get(using=get_user_preference("browser"))
56 | except webbrowser.Error:
57 | warnings.warn("Could not find the user preferred browser.")
58 |
59 | for browser in ["firefox", "chrome", "chromium-browser"]:
60 | try:
61 | return webbrowser.get(using=browser)
62 | except webbrowser.Error:
63 | pass
64 |
65 | # Return default browser if none of the
66 | # preferred browsers are installed:
67 | return webbrowser.get()
68 |
69 | def _app_ready(self) -> bool:
70 | """Check if the flask instance is ready."""
71 |
72 | no_proxy_env = os.environ.get("NO_PROXY")
73 | os.environ["NO_PROXY"] = "localhost"
74 |
75 | try:
76 | with urllib.request.urlopen(self._url()): # nosec
77 | app_ready = True
78 | except urllib.error.HTTPError:
79 | # The flask instance responded with some HTTP error (likely 401),
80 | # but is otherwise ready to accept connections
81 | app_ready = True
82 | except urllib.error.URLError:
83 | # The flask instance has not started
84 | app_ready = False
85 | except ConnectionResetError:
86 | # The flask instance has started but (correctly) abort
87 | # request due to "401 Unauthorized"
88 | app_ready = True
89 | finally:
90 | os.environ["NO_PROXY"] = no_proxy_env if no_proxy_env else ""
91 |
92 | return app_ready
93 |
94 | def _open_new_tab(self) -> None:
95 | """Open the url (with token) in the default browser."""
96 |
97 | print(
98 | f"{terminal_colors.GREEN}{terminal_colors.BOLD}"
99 | f" Opening the application ({self._url()}) in your browser.\n"
100 | f"{terminal_colors.BLUE}"
101 | " Note that your browser might display security issues.\n"
102 | " See: https://equinor.github.io/webviz-config/#/?id=localhost-hsts\n"
103 | f"{terminal_colors.GREEN}"
104 | " Press CTRL + C in this terminal window to stop the application."
105 | f"{terminal_colors.END}"
106 | )
107 |
108 | LocalhostOpenBrowser._get_browser_controller().open_new_tab(
109 | self._url(with_token=True)
110 | )
111 |
--------------------------------------------------------------------------------
/webviz_config/_dockerize/_create_docker_setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import warnings
4 | from pathlib import Path
5 | from typing import Dict, Set
6 |
7 | import jinja2
8 | import requests
9 |
10 | from ..plugins import PLUGIN_PROJECT_METADATA
11 | from ._pip_git_url import pip_git_url
12 |
13 |
14 | PYPI_URL_ROOT = "https://pypi.org"
15 |
16 |
17 | def create_docker_setup(
18 | build_directory: Path, plugin_metadata: Dict[str, dict]
19 | ) -> None:
20 | """Creates a Docker setup in build_directory. The input dictionary plugin_metadata
21 | is a dictionary of plugin metadata only for the plugins used in the generated
22 | application. This is used in order to automatically include the necessary
23 | plugin projects as Python requirements in the generated Docker setup.
24 | """
25 |
26 | template_environment = jinja2.Environment( # nosec
27 | loader=jinja2.PackageLoader("webviz_config", "templates"),
28 | undefined=jinja2.StrictUndefined,
29 | autoescape=False,
30 | )
31 | template = template_environment.get_template("Dockerfile.jinja2")
32 |
33 | distributions = {
34 | metadata["dist_name"]: PLUGIN_PROJECT_METADATA[metadata["dist_name"]]
35 | for metadata in plugin_metadata.values()
36 | }
37 |
38 | # Regardless of a standard webviz-config plugin is included in user's
39 | # configuration file, we still need to install the plugin framework webviz-config:
40 | distributions["webviz-config"] = PLUGIN_PROJECT_METADATA["webviz-config"]
41 |
42 | requirements = get_python_requirements(distributions)
43 | requirements.add("gunicorn")
44 |
45 | (build_directory / "requirements.txt").write_text(
46 | "\n".join(sorted(list(requirements)))
47 | )
48 |
49 | (build_directory / "Dockerfile").write_text(
50 | template.render(
51 | {
52 | "python_version_major": sys.version_info.major,
53 | "python_version_minor": sys.version_info.minor,
54 | "ssh_required": any("ssh://" in req for req in requirements),
55 | }
56 | )
57 | )
58 |
59 |
60 | def get_python_requirements(distributions: dict) -> Set[str]:
61 |
62 | requirements = set()
63 |
64 | for dist_name, dist in distributions.items():
65 |
66 | requirements.update(
67 | [
68 | f"{dep}=={version}"
69 | for dep, version in PLUGIN_PROJECT_METADATA[dist_name][
70 | "dependencies"
71 | ].items()
72 | if dep not in distributions
73 | ]
74 | )
75 |
76 | if dist["download_url"] is None and dist["source_url"] is None:
77 | warnings.warn(
78 | f"Plugin distribution {dist_name} has no download/source URL specified. "
79 | "Will therefore not automatically become part of built Docker image."
80 | )
81 | continue
82 |
83 | if dist["download_url"] is not None and dist["download_url"].startswith(
84 | PYPI_URL_ROOT
85 | ):
86 | pypi_data = requests.get(f"{PYPI_URL_ROOT}/pypi/{dist_name}/json").json()
87 | if dist["dist_version"] in pypi_data["releases"]:
88 | requirements.add(f"{dist_name}=={dist['dist_version']}")
89 | continue
90 |
91 | if dist["source_url"] is None:
92 | raise RuntimeError(
93 | f"Could not find version {dist['dist_version']} of {dist_name} on PyPI. "
94 | "Falling back to git source code is not possible since "
95 | "project_urls['Source'] is not defined in setup.py."
96 | )
97 |
98 | requirements.add(
99 | pip_git_url(
100 | dist["dist_version"],
101 | source_url=os.environ.get(
102 | "SOURCE_URL_" + dist_name.upper().replace("-", "_"),
103 | dist["source_url"],
104 | ),
105 | git_pointer=os.environ.get(
106 | "GIT_POINTER_" + dist_name.upper().replace("-", "_")
107 | ),
108 | )
109 | )
110 |
111 | return requirements
112 |
--------------------------------------------------------------------------------
/.github/workflows/webviz-config.yml:
--------------------------------------------------------------------------------
1 | name: webviz-config
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | release:
11 | types:
12 | - published
13 | schedule:
14 | # Run CI daily and check that tests are working with latest dependencies
15 | - cron: "0 0 * * *"
16 |
17 | jobs:
18 | webviz-config:
19 | runs-on: ubuntu-latest
20 | env:
21 | PYTHONWARNINGS: default # We want to see e.g. DeprecationWarnings
22 | strategy:
23 | matrix:
24 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
25 |
26 | steps:
27 | - name: 📖 Checkout commit locally
28 | uses: actions/checkout@v4
29 |
30 | - name: Set setuptools_scm version
31 | if: github.event_name == 'release'
32 | # Need to instruct setuptools_scm to use the GitHub provided tag, despite local git changes (due to build step)
33 | run: echo "SETUPTOOLS_SCM_PRETEND_VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
34 |
35 | - name: 🐍 Set up Python ${{ matrix.python-version }}
36 | uses: actions/setup-python@v5
37 | with:
38 | python-version: ${{ matrix.python-version }}
39 |
40 | - name: 📦 Install npm dependencies
41 | run: |
42 | npm ci --ignore-scripts
43 | npm run postinstall
44 |
45 | - name: 📦 Install webviz-config with dependencies
46 | run: |
47 | pip install --upgrade pip
48 | pip install .
49 | pip install --pre --upgrade webviz-core-components # Testing against our latest release (including pre-releases)
50 |
51 | - name: 📦 Install extra deployment dependencies
52 | if: matrix.python-version != '3.10' # Check tests pass also without optional dependencies
53 | run: |
54 | pip install .[deployment]
55 |
56 | - name: 📦 Install test dependencies
57 | run: |
58 | pip install .[tests]
59 | wget https://chromedriver.storage.googleapis.com/$(wget https://chromedriver.storage.googleapis.com/LATEST_RELEASE -q -O -)/chromedriver_linux64.zip
60 | unzip chromedriver_linux64.zip
61 |
62 | - name: 🧾 List all installed packages
63 | run: pip freeze
64 |
65 | - name: 🕵️ Check code style & linting
66 | if: matrix.python-version == '3.8'
67 | run: |
68 | black --check webviz_config tests setup.py
69 | pylint webviz_config tests setup.py
70 | bandit -r -c ./bandit.yml webviz_config tests setup.py
71 | # mypy --package webviz_config --ignore-missing-imports --disallow-untyped-defs --show-error-codes
72 |
73 | - name: 🤖 Run tests
74 | run: |
75 | webviz preferences --theme default
76 | pytest ./tests --headless --forked --ignore ./tests/test_example_wlf_plugin.py
77 | webviz docs --portable ./docs_build --skip-open
78 | webviz schema
79 |
80 | - name: 🐳 Build Docker example image
81 | run: |
82 | export GIT_POINTER_WEBVIZ_CONFIG=$GITHUB_REF
83 | webviz build ./examples/basic_example.yaml --portable ./some_portable_app
84 | pushd ./some_portable_app
85 | docker build .
86 | rm -rf ./some_portable_app
87 |
88 | - name: 🚢 Build and deploy Python package
89 | if: github.event_name == 'release' && matrix.python-version == '3.8'
90 | env:
91 | TWINE_USERNAME: __token__
92 | TWINE_PASSWORD: ${{ secrets.pypi_webviz_token }}
93 | run: |
94 | python -m pip install --upgrade setuptools wheel twine
95 | python setup.py sdist bdist_wheel
96 | twine upload dist/*
97 |
98 | - name: 📚 Update GitHub pages
99 | if: github.event_name == 'release' && matrix.python-version == '3.8'
100 | run: |
101 | cp -R ./docs_build ../docs_build
102 |
103 | git config --local user.email "webviz-github-action"
104 | git config --local user.name "webviz-github-action"
105 | git fetch origin gh-pages
106 | git checkout --track origin/gh-pages
107 | git clean -f -f -d -x
108 | git rm -r *
109 |
110 | cp -R ../docs_build/* .
111 |
112 | git add .
113 |
114 | if git diff-index --quiet HEAD; then
115 | echo "No changes in documentation. Skip documentation deploy."
116 | else
117 | git commit -m "Update Github Pages"
118 | git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" gh-pages
119 | fi
120 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/interactive_terminal.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from typing import List, Callable, Optional
4 |
5 | from webviz_config.utils import terminal_colors
6 |
7 |
8 | def terminal_title(title: str) -> None:
9 | print("\n" + terminal_colors.BOLD + title + terminal_colors.END)
10 | print("=" * len(title) + "\n")
11 |
12 |
13 | def terminal_yes_no(question: str) -> bool:
14 | while True:
15 | reply = input(question + " (y/n): ").lower().strip()
16 | if reply.startswith("y"):
17 | return True
18 | if reply.startswith("n"):
19 | return False
20 |
21 |
22 | def terminal_list_selector(options: List[str], display_string: str) -> str:
23 |
24 | for i, subscription in enumerate(options):
25 | print(f"{i+1}) {subscription}")
26 | print()
27 |
28 | selected_value = None
29 | while selected_value not in options:
30 | try:
31 | index = int(input(f"Choose {display_string} index to use: "))
32 | return_value = selected_value = options[index - 1]
33 | except (IndexError, ValueError):
34 | print(f"Select an integer in range 1-{len(options)}.")
35 |
36 | print(
37 | "\n"
38 | + terminal_colors.BOLD
39 | + f"Selected {display_string}: {selected_value}"
40 | + terminal_colors.END
41 | )
42 | return return_value
43 |
44 |
45 | def user_input_from_stdin(env_var: str, noun: str, regex: str = None) -> str:
46 | if env_var in os.environ:
47 | value = os.environ[env_var]
48 | print(f"Using {noun} '{value}' (from environment variable {env_var}).")
49 | if regex is not None and not re.fullmatch(regex, value):
50 | raise RuntimeError("Input not on expected format.")
51 | return value
52 |
53 | print(f"Tip: Default value can be set through env. variable {env_var}\n")
54 | while True:
55 | value = input(f"{noun.capitalize()}: ").strip()
56 | if regex is None or re.fullmatch(regex, value):
57 | return value
58 | print("Input not on expected format.")
59 |
60 |
61 | def user_input_from_list(env_var: str, noun: str, choices: List[str]) -> str:
62 | if env_var in os.environ:
63 | value = os.environ[env_var]
64 | print(f"Using {noun} '{value}' (from environment variable {env_var}).")
65 | if value not in choices:
66 | raise ValueError(
67 | f"Value '{value}' not among available choices ({', '.join(choices)})"
68 | )
69 | return value
70 |
71 | print(f"Tip: Default value can be set through env. variable {env_var}\n")
72 | return terminal_list_selector(choices, noun)
73 |
74 |
75 | def user_input_optional_reuse(
76 | env_var: str,
77 | noun: str,
78 | exists_function: Callable,
79 | reuse_allowed: bool = True,
80 | regex: Optional[str] = None,
81 | ) -> str:
82 | if env_var in os.environ:
83 | value = os.environ[env_var]
84 | print(f"Using {noun} '{value}' (from environment variable {env_var}).")
85 | if not reuse_allowed and exists_function(value):
86 | raise RuntimeError(f"There already exists an {noun} with name '{value}'.")
87 | if regex is not None and not re.match(regex, value):
88 | raise RuntimeError(
89 | f"Value {value} does not satisfy regular expression {regex}"
90 | )
91 | return value
92 |
93 | print(f"Tip: Default value can be set through env. variable {env_var}\n")
94 |
95 | while True:
96 | value = input(f"{noun.capitalize()}: ")
97 |
98 | if regex is not None and not re.match(regex, value):
99 | print(f"Value {value} does not satisfy regular expression {regex}")
100 | continue
101 |
102 | check = exists_function(value)
103 |
104 | if isinstance(check, tuple):
105 | exists, message = check
106 | else:
107 | exists = check
108 | message = None
109 |
110 | if exists:
111 | if not reuse_allowed:
112 | print(
113 | f"There already exists an {noun} with name '{value}'"
114 | if message is None
115 | else message
116 | )
117 | continue
118 | print(f"Found existing {noun} with value '{value}'")
119 | reuse = terminal_yes_no(f"Do you want to reuse the existing {noun}")
120 | if reuse:
121 | return value
122 | continue
123 |
124 | print(f"{noun.capitalize()} name '{value}' is available.")
125 | return value
126 |
--------------------------------------------------------------------------------
/tests/unit_tests/test_webviz_factory_registry.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import pytest
4 |
5 | from webviz_config.webviz_factory import WebvizFactory
6 | from webviz_config.webviz_factory_registry import WebvizFactoryRegistry
7 |
8 |
9 | # pylint: disable=no-self-use
10 | class FactoryA(WebvizFactory):
11 | def create_obj_a(self) -> str:
12 | return "A"
13 |
14 |
15 | class FactoryB(WebvizFactory):
16 | def create_obj_b(self) -> str:
17 | return "B"
18 |
19 |
20 | class FactoryBSub(FactoryB):
21 | def create_obj_b_sub(self) -> str:
22 | return "B_sub"
23 |
24 |
25 | # pylint: disable=too-few-public-methods
26 | class UnrelatedFactory:
27 | pass
28 |
29 |
30 | def create_initialized_registry() -> WebvizFactoryRegistry:
31 | registry = WebvizFactoryRegistry()
32 | registry.initialize({"MyFactory": "someValue"})
33 | return registry
34 |
35 |
36 | def test_uninitialized_access() -> None:
37 | registry = WebvizFactoryRegistry()
38 |
39 | with pytest.raises(RuntimeError):
40 | _settings = registry.all_factory_settings
41 |
42 |
43 | def test_initialization_and_basic_access() -> None:
44 | registry = create_initialized_registry()
45 |
46 | settings = registry.all_factory_settings
47 | assert "MyFactory" in settings
48 |
49 | factory = registry.get_factory(FactoryA)
50 | assert factory is None
51 |
52 |
53 | def test_multiple_initializations() -> None:
54 | registry = WebvizFactoryRegistry()
55 | registry.initialize({"MyFactory": "someValue"})
56 |
57 | with pytest.raises(RuntimeError):
58 | registry.initialize({"MyFactory": "someValue"})
59 |
60 |
61 | def test_set_get_factory() -> None:
62 | registry = create_initialized_registry()
63 |
64 | factory_a = FactoryA()
65 | registry.set_factory(FactoryA, factory_a)
66 |
67 | factory_b = FactoryB()
68 | registry.set_factory(FactoryB, factory_b)
69 |
70 | f_a: Optional[FactoryA] = registry.get_factory(FactoryA)
71 | assert f_a is factory_a
72 |
73 | f_b: Optional[FactoryB] = registry.get_factory(FactoryB)
74 | assert f_b is factory_b
75 |
76 |
77 | def test_set_get_derived_factory() -> None:
78 | registry = create_initialized_registry()
79 |
80 | factory_b = FactoryB()
81 | factory_b_sub = FactoryBSub()
82 |
83 | registry.set_factory(FactoryB, factory_b)
84 | registry.set_factory(FactoryBSub, factory_b_sub)
85 | assert registry.get_factory(FactoryB) is factory_b
86 | assert registry.get_factory(FactoryBSub) is factory_b_sub
87 |
88 | # This should be fine
89 | registry.set_factory(FactoryB, factory_b_sub)
90 | assert registry.get_factory(FactoryB) is factory_b_sub
91 |
92 | # This should not be legal, but cannot currently figure out how
93 | # to disallow it using type hinting. Currently throws an exception
94 | with pytest.raises(TypeError):
95 | registry.set_factory(FactoryBSub, factory_b)
96 |
97 |
98 | def test_set_mismatched_factory_types() -> None:
99 | registry = create_initialized_registry()
100 |
101 | factory_a: FactoryA = FactoryA()
102 | factory_b: FactoryB = FactoryB()
103 | factory_b_sub: FactoryBSub = FactoryBSub()
104 |
105 | # These four are legal
106 | registry.set_factory(FactoryA, factory_a)
107 | registry.set_factory(FactoryB, factory_b)
108 | registry.set_factory(FactoryB, factory_b_sub)
109 | registry.set_factory(FactoryBSub, factory_b_sub)
110 |
111 | # These are rightly caught as errors by type checking since they don't derive from WebvizFactory
112 | # registry.set_factory(UnrelatedFactory, UnrelatedFactory())
113 | # registry.set_factory(FactoryA, UnrelatedFactory())
114 | # registry.set_factory(FactoryA, 123)
115 |
116 | # The following setter calls are illegal, but can't figure out how
117 | # to get type hinting to disallow them
118 | # For now, we rely on exceptions
119 | with pytest.raises(TypeError):
120 | registry.set_factory(FactoryA, factory_b)
121 | with pytest.raises(TypeError):
122 | registry.set_factory(FactoryBSub, factory_b)
123 |
124 |
125 | def test_get_mismatched_factory_types() -> None:
126 | registry = create_initialized_registry()
127 |
128 | _f_a: Optional[FactoryA] = None
129 | _f_b: Optional[FactoryB] = None
130 | _f_b_sub: Optional[FactoryBSub] = None
131 |
132 | # These four are legal and do indeed pass wrt type checking
133 | _f_a = registry.get_factory(FactoryA)
134 | _f_b = registry.get_factory(FactoryB)
135 | _f_b = registry.get_factory(FactoryBSub)
136 | _f_b_sub = registry.get_factory(FactoryBSub)
137 |
138 | # These are rightly caught as errors by type checking, good
139 | # _f_b = registry.get_factory(FactoryA)
140 | # _f_b_sub = registry.get_factory(FactoryB)
141 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/radix_cli.py:
--------------------------------------------------------------------------------
1 | import time
2 | import shutil
3 | import pathlib
4 | import subprocess
5 |
6 |
7 | def binary_available() -> bool:
8 | """Returns True if the Radix CLI binary (rx) is
9 | availabile in $PATH, otherwise returns False.
10 | """
11 | return shutil.which("rx") is not None
12 |
13 |
14 | def _trigger_token_aquisition(update_only: bool = False) -> None:
15 | # There exist no pure login command in rx as of April 2021
16 | # See https://github.com/equinor/radix-cli/issues/20.
17 | # pylint: disable=subprocess-run-check
18 | subprocess.run(
19 | [
20 | "rx",
21 | "get",
22 | "application",
23 | "--application",
24 | "dummy_name",
25 | "--context",
26 | "dummy_context",
27 | ],
28 | timeout=15 if update_only else None,
29 | capture_output=update_only,
30 | )
31 |
32 |
33 | def logged_in() -> bool:
34 |
35 | _trigger_token_aquisition(update_only=True)
36 |
37 | config = pathlib.Path.home() / ".radix" / "config"
38 | return config.is_file()
39 |
40 |
41 | def log_in() -> None:
42 | _trigger_token_aquisition()
43 |
44 |
45 | def application_exists(application_name: str, context: str) -> bool:
46 | result = subprocess.run(
47 | [
48 | "rx",
49 | "get",
50 | "application",
51 | "--application",
52 | application_name,
53 | "--context",
54 | context,
55 | ],
56 | capture_output=True,
57 | check=False,
58 | )
59 | return not result.stderr
60 |
61 |
62 | def create_application(
63 | application_name: str,
64 | configuration_item: str,
65 | repository_url: str,
66 | shared_secret: str,
67 | context: str,
68 | ad_group: str,
69 | ) -> str:
70 | result = subprocess.run(
71 | [
72 | "rx",
73 | "create",
74 | "application",
75 | "--application",
76 | application_name,
77 | "--config-branch",
78 | "main",
79 | "--configuration-item",
80 | configuration_item,
81 | "--repository",
82 | repository_url,
83 | "--shared-secret",
84 | shared_secret,
85 | "--context",
86 | context,
87 | "--ad-groups",
88 | ad_group,
89 | ],
90 | capture_output=True,
91 | check=True,
92 | )
93 |
94 | stderr = result.stderr.decode()
95 | if not stderr.startswith("ssh-rsa"):
96 | pass # https://github.com/equinor/radix-cli/issues/48
97 | # error_message = stderr
98 | # if "registerApplicationBadRequest" in stderr:
99 | # error_message += (
100 | # f"Is {repository_url} being used by another Radix application?"
101 | # )
102 | # raise RuntimeError(error_message)
103 |
104 | return stderr
105 |
106 |
107 | def build_and_deploy_application(application_name: str, context: str) -> None:
108 | subprocess.run(
109 | [
110 | "rx",
111 | "create",
112 | "job",
113 | "build-deploy",
114 | "--application",
115 | application_name,
116 | "--branch",
117 | "main",
118 | "--context",
119 | context,
120 | ],
121 | check=True,
122 | )
123 |
124 |
125 | def set_radix_secret(
126 | application_name: str,
127 | environment: str,
128 | component: str,
129 | key: str,
130 | value: str,
131 | context: str,
132 | ) -> None:
133 |
134 | max_radix_build_time = 60 * 60
135 | sleep_per_attempt = 10
136 |
137 | for _ in range(max_radix_build_time // sleep_per_attempt):
138 | try:
139 | subprocess.run(
140 | [
141 | "rx",
142 | "set",
143 | "environment-secret",
144 | "--await-reconcile",
145 | "--application",
146 | application_name,
147 | "--environment",
148 | environment,
149 | "--component",
150 | component,
151 | "--secret",
152 | key,
153 | "--value",
154 | value,
155 | "--context",
156 | context,
157 | ],
158 | capture_output=True,
159 | check=True,
160 | )
161 | return
162 | except subprocess.CalledProcessError:
163 | time.sleep(sleep_per_attempt)
164 |
165 | raise RuntimeError("Failed setting Radix secret. Is Radix still building?")
166 |
--------------------------------------------------------------------------------
/webviz_config/plugins/_utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | import warnings
3 | from typing import Dict, Iterable, Optional, Tuple, TypedDict
4 |
5 | from importlib.metadata import requires, version, PackageNotFoundError, EntryPoint
6 |
7 |
8 | class PluginProjectMetaData(TypedDict):
9 | dist_version: str
10 | dependencies: Dict[str, str]
11 | documentation_url: Optional[str]
12 | download_url: Optional[str]
13 | source_url: Optional[str]
14 | tracker_url: Optional[str]
15 |
16 |
17 | def _plugin_dist_dependencies(plugin_dist_name: str) -> Dict[str, str]:
18 | """Returns overview of all dependencies (indirect + direct) of a given
19 | plugin project installed in the current environment.
20 |
21 | Key is package name of dependency, value is (installed) version string.
22 | """
23 |
24 | untraversed_dependencies = set([plugin_dist_name])
25 | requirements = {}
26 |
27 | while untraversed_dependencies:
28 | sub_dependencies = requires(untraversed_dependencies.pop())
29 |
30 | if sub_dependencies is None:
31 | continue
32 |
33 | for sub_dependency in sub_dependencies:
34 | split = re.split(r"[;<>~=()]", sub_dependency, 1)
35 | package_name = split[0].strip().replace("_", "-").lower()
36 |
37 | if package_name not in requirements:
38 | # Only include package in dependency list
39 | # if it is not an "extra" dependency...
40 | if len(split) == 1 or "extra" not in split[1]:
41 | try:
42 | # ...and if it is actually installed (there are dependencies
43 | # in setup.py that e.g. are not installed on certain Python
44 | # versions and operating system combinations).
45 | requirements[package_name] = version(package_name)
46 | untraversed_dependencies.add(package_name)
47 | except PackageNotFoundError:
48 | pass
49 |
50 | return {k: requirements[k] for k in sorted(requirements)}
51 |
52 |
53 | def load_webviz_plugins_with_metadata(
54 | distributions: Iterable,
55 | ) -> Tuple[Dict[str, dict], Dict[str, PluginProjectMetaData], Dict[str, EntryPoint]]:
56 | """Finds entry points corresponding to webviz-config plugins,
57 | and returns them as a dictionary (key is plugin name string,
58 | value is reference to entrypoint).
59 |
60 | Also returns a dictionary of plugin metadata.
61 | """
62 |
63 | plugin_project_metadata: Dict[str, PluginProjectMetaData] = {}
64 | plugin_metadata: Dict[str, dict] = {}
65 | plugin_entrypoints: Dict[str, EntryPoint] = {}
66 |
67 | for dist in distributions:
68 | for entry_point in dist.entry_points:
69 | if entry_point.group == "webviz_config_plugins":
70 | dist_name = dist.metadata["name"]
71 |
72 | if (
73 | entry_point.name in plugin_metadata
74 | and dist_name not in plugin_project_metadata
75 | ):
76 | warnings.warn(
77 | f"Multiple versions of plugin with name {entry_point.name}. Already "
78 | f"loaded from project {plugin_metadata[entry_point.name]['dist_name']}. "
79 | f"Overwriting using plugin with from project {dist_name}",
80 | RuntimeWarning,
81 | )
82 |
83 | if dist_name not in plugin_project_metadata:
84 | project_urls = {
85 | value.split(",")[0]: value.split(",")[1].strip()
86 | for (key, value) in dist.metadata.items()
87 | if key == "Project-URL"
88 | }
89 |
90 | plugin_project_metadata[dist_name] = PluginProjectMetaData(
91 | {
92 | "dist_version": dist.version,
93 | "dependencies": _plugin_dist_dependencies(dist_name),
94 | "documentation_url": project_urls.get("Documentation"),
95 | "download_url": project_urls.get("Download"),
96 | "source_url": project_urls.get("Source"),
97 | "tracker_url": project_urls.get("Tracker"),
98 | }
99 | )
100 |
101 | plugin_metadata[entry_point.name] = {
102 | "dist_name": dist.metadata["name"],
103 | }
104 |
105 | plugin_entrypoints[entry_point.name] = entry_point
106 |
107 | return (plugin_metadata, plugin_project_metadata, plugin_entrypoints)
108 |
--------------------------------------------------------------------------------
/webviz_config/webviz_plugin_subclasses/_layout_unique_id.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 |
4 | class LayoutUniqueId:
5 | def __init__(
6 | self,
7 | plugin_uuid: Optional[str] = None,
8 | view_id: Optional[str] = None,
9 | view_element_id: Optional[str] = None,
10 | settings_group_id: Optional[str] = None,
11 | component_id: Optional[str] = None,
12 | other: Optional["LayoutUniqueId"] = None,
13 | ) -> None:
14 | self._plugin_uuid = plugin_uuid
15 | self._view_id = view_id
16 | self._settings_group_id = settings_group_id
17 | self._view_element_id = view_element_id
18 | self._component_id = component_id
19 |
20 | if other:
21 | self.adopt(other)
22 |
23 | def get_plugin_uuid(self) -> Optional[str]:
24 | return self._plugin_uuid
25 |
26 | def get_view_unique_id(self) -> str:
27 | ids: List[str] = []
28 | if not self._view_id:
29 | return ""
30 |
31 | if self._plugin_uuid:
32 | ids.append(self._plugin_uuid)
33 | if self._view_id:
34 | ids.append(self._view_id)
35 |
36 | return "-".join(ids)
37 |
38 | def get_view_id(self) -> Optional[str]:
39 | return self._view_id
40 |
41 | def get_view_element_unique_id(self) -> str:
42 | ids: List[str] = []
43 | if self._plugin_uuid:
44 | ids.append(self._plugin_uuid)
45 | if self._view_id:
46 | ids.append(self._view_id)
47 | if self._view_element_id:
48 | ids.append(self._view_element_id)
49 |
50 | return "-".join(ids)
51 |
52 | def get_view_element_id(self) -> Optional[str]:
53 | return self._view_element_id
54 |
55 | def get_component_id(self) -> Optional[str]:
56 | return self._component_id
57 |
58 | def get_settings_group_id(self) -> Optional[str]:
59 | return self._settings_group_id
60 |
61 | def get_settings_group_unique_id(self) -> str:
62 | ids: List[str] = []
63 | if self._plugin_uuid:
64 | ids.append(self._plugin_uuid)
65 | if self._view_id:
66 | ids.append(self._view_id)
67 | if self._view_element_id:
68 | ids.append(self._view_element_id)
69 | if self._settings_group_id:
70 | ids.append(self._settings_group_id)
71 |
72 | return "-".join(ids)
73 |
74 | def set_plugin_uuid(self, plugin_uuid: str) -> None:
75 | self._plugin_uuid = plugin_uuid
76 |
77 | def set_view_id(self, view_id: str) -> None:
78 | self._view_id = view_id
79 |
80 | def set_view_element_id(self, view_element_id: str) -> None:
81 | self._view_element_id = view_element_id
82 |
83 | def set_settings_group_id(self, settings_group_id: str) -> None:
84 | self._settings_group_id = settings_group_id
85 |
86 | def set_component_id(self, component_id: str) -> None:
87 | self._component_id = component_id
88 |
89 | def is_plugin(self) -> bool:
90 | return (
91 | self._plugin_uuid is not None
92 | and self._view_id is None
93 | and self._settings_group_id is None
94 | )
95 |
96 | def is_view(self) -> bool:
97 | return (
98 | self._view_id is not None
99 | and self._view_element_id is None
100 | and self._settings_group_id is None
101 | )
102 |
103 | def is_view_element(self) -> bool:
104 | return self._view_element_id is not None
105 |
106 | def is_settings_group(self) -> bool:
107 | return self._settings_group_id is not None
108 |
109 | def is_component(self) -> bool:
110 | return self._component_id is not None
111 |
112 | def adopt(self, other: "LayoutUniqueId") -> None:
113 | if self._plugin_uuid is None and other.get_plugin_uuid() is not None:
114 | self._plugin_uuid = other.get_plugin_uuid()
115 |
116 | if self._view_id is None and other.get_view_id() is not None:
117 | self._view_id = other.get_view_id()
118 |
119 | if self._view_element_id is None and other.get_view_element_id() is not None:
120 | self._view_element_id = other.get_view_element_id()
121 |
122 | if (
123 | self._settings_group_id is None
124 | and other.get_settings_group_id() is not None
125 | ):
126 | self._settings_group_id = other.get_settings_group_id()
127 |
128 | if self._component_id is None and other.get_component_id() is not None:
129 | self._component_id = other.get_component_id()
130 |
131 | def __str__(self) -> str:
132 | return self.to_string()
133 |
134 | def to_string(self) -> str:
135 | ids: List[str] = []
136 | if self._plugin_uuid:
137 | ids.append(self._plugin_uuid)
138 | if self._view_id:
139 | ids.append(self._view_id)
140 | if self._view_element_id:
141 | ids.append(self._view_element_id)
142 | if self._settings_group_id:
143 | ids.append(self._settings_group_id)
144 | if self._component_id:
145 | ids.append(self._component_id)
146 |
147 | return "-".join(ids)
148 |
--------------------------------------------------------------------------------
/webviz_config/webviz_assets.py:
--------------------------------------------------------------------------------
1 | import re
2 | import shutil
3 | import pathlib
4 | from typing import Optional
5 |
6 | from tqdm import tqdm
7 | from dash import Dash
8 | import flask
9 |
10 |
11 | class WebvizAssets:
12 | """Dash applications by default host static resources from a folder called
13 | ./assets, relative to the root application folder.
14 |
15 | In order to facilitate hot reloading, and fast building of Webviz
16 | applications from the configuration file, this class facilitates handling
17 | of static assets.
18 |
19 | Individual plugins can add assets to a common instance of WebvizAssets
20 | by calling the .add(filename) function. This adds the resource, and
21 | at the same time returns the resource URI which the plugin can use.
22 |
23 | If the webviz instance is in non-portable mode, the Flask/Dash application
24 | is routed to the actual location of the files, making hot reload and
25 | testing fast.
26 |
27 | When creating a portable webviz instance however, the files are copied
28 | over the ./resources/assets folder, and normal Dash usage applies.
29 |
30 | In both portable and non-portable mode, WebvizAssets makes sure there are
31 | no name conflicts (i.e. it supports multiple assets on different paths,
32 | but with same filename) and also assignes URI friendly resource IDs.
33 | """
34 |
35 | def __init__(self) -> None:
36 | self._assets: dict = {}
37 | self._portable = False
38 |
39 | @property
40 | def portable(self) -> bool:
41 | return self._portable
42 |
43 | @portable.setter
44 | def portable(self, portable: bool) -> None:
45 | self._portable = portable
46 |
47 | def _base_folder(self) -> str:
48 | return "assets" if self.portable else "temp"
49 |
50 | def add(self, filename: pathlib.Path) -> str:
51 | """Calling this function makes the filename given as input
52 | available as a hosted asset when the application is running.
53 | The returned string is a URI which the plugin optionally
54 | can use internally (e.g. as "src" in image elements).
55 |
56 | Calling this function with the same input path
57 | multiple times will return the same URI.
58 |
59 | Filenames added to WebvizAssets that ends with .css or .js
60 | are loaded automatically in the browser by Dash,
61 | both in non-portable and portable mode.
62 | """
63 |
64 | path = pathlib.Path(filename)
65 |
66 | if filename not in self._assets.values():
67 | assigned_id = self._generate_id(path.name)
68 | self._assets[assigned_id] = filename
69 | else:
70 | assigned_id = {v: k for k, v in self._assets.items()}[filename]
71 |
72 | return str(pathlib.Path(self._base_folder()) / assigned_id)
73 |
74 | def directly_host_assets(self, app: Dash) -> None:
75 | """In non-portable mode, this function can be called by the
76 | application. It routes the Dash application to the added assets on
77 | disk, making hot reloading and more interactive development of the
78 | application possible.
79 | """
80 |
81 | if self._portable:
82 | raise RuntimeError(
83 | "The function WebvizAssets.directly_host_assets() "
84 | "method is only meaningful in a non-portable settings."
85 | )
86 |
87 | @app.server.route(f"/{self._base_folder()}/")
88 | def _send_file(asset_id: str) -> Optional[flask.wrappers.Response]:
89 | if asset_id in self._assets: # Only serve white listed resources
90 | path = pathlib.Path(self._assets[asset_id])
91 | return flask.send_from_directory(str(path.parent), path.name)
92 | return None
93 |
94 | # Add .css and .js files to auto-loaded Dash assets
95 | for asset_id, asset_path in self._assets.items():
96 | if asset_path.suffix == ".css":
97 | app.config.external_stylesheets.append(
98 | f"./{self._base_folder()}/{asset_id}"
99 | )
100 | elif asset_path.suffix == ".js":
101 | app.config.external_scripts.append(
102 | f"./{self._base_folder()}/{asset_id}"
103 | )
104 |
105 | def make_portable(self, asset_folder: pathlib.Path) -> None:
106 | """Copy over all added assets to the given folder (asset_folder)."""
107 |
108 | for assigned_id, filename in tqdm(
109 | self._assets.items(),
110 | bar_format="{l_bar} {bar} | Copied {n_fmt}/{total_fmt}",
111 | ):
112 | tqdm.write(f"Copying over {filename}")
113 | shutil.copyfile(filename, asset_folder / assigned_id)
114 |
115 | def _generate_id(self, filename: str) -> str:
116 | """From the filename, create a safe resource id not already present"""
117 | asset_id = base_id = re.sub(
118 | "[^-a-z0-9._]+", "", filename.lower().replace(" ", "_")
119 | )
120 |
121 | count = 1
122 | while asset_id in self._assets:
123 | count += 1
124 | asset_id = f"{base_id}{count}"
125 |
126 | return asset_id
127 |
128 |
129 | WEBVIZ_ASSETS = WebvizAssets()
130 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import pathlib
4 |
5 | from setuptools import setup, find_packages
6 |
7 |
8 | def get_long_description() -> str:
9 | """Converts relative repository links to absolute URLs
10 | if GITHUB_REPOSITORY and GITHUB_SHA environment variables exist.
11 | If not, it returns the raw content in README.md.
12 | """
13 |
14 | raw_readme = pathlib.Path("README.md").read_text()
15 |
16 | repository = os.environ.get("GITHUB_REPOSITORY")
17 | sha = os.environ.get("GITHUB_SHA")
18 |
19 | if repository is not None and sha is not None:
20 | full_url = f"https://github.com/{repository}/blob/{sha}/"
21 | return re.sub(r"]\((?!https)", "](" + full_url, raw_readme)
22 | return raw_readme
23 |
24 |
25 | TESTS_REQUIRES = [
26 | "bandit",
27 | "black>=22.12,<23",
28 | "dash[testing]",
29 | "jsonschema",
30 | "mypy",
31 | "pylint<=2.13.9", # Locked due to https://github.com/equinor/webviz-subsurface/issues/1052
32 | "pytest-xdist",
33 | "pytest-forked",
34 | "selenium",
35 | "types-bleach",
36 | "types-markdown",
37 | "types-pyyaml",
38 | "types-requests",
39 | ]
40 |
41 | # pylint: disable=line-too-long
42 | setup(
43 | name="webviz-config",
44 | description="Configuration file support for webviz",
45 | long_description=get_long_description(),
46 | long_description_content_type="text/markdown",
47 | url="https://github.com/equinor/webviz-config",
48 | author="R&T Equinor",
49 | packages=find_packages(exclude=["tests"]),
50 | package_data={
51 | "webviz_config": [
52 | "_docs/static/*",
53 | "_docs/static/fonts/*",
54 | "py.typed",
55 | "static/*",
56 | "static/.dockerignore",
57 | "static/.gitignore",
58 | "static/assets/*",
59 | "templates/*",
60 | "themes/default_assets/*",
61 | ]
62 | },
63 | entry_points={
64 | "console_scripts": ["webviz=webviz_config.command_line:main"],
65 | "webviz_config_plugins": [
66 | "ExampleAssets = webviz_config.generic_plugins._example_assets:ExampleAssets",
67 | "ExampleWlfPlugin = webviz_config.generic_plugins._example_wlf_plugin:ExampleWlfPlugin",
68 | "ExampleDataDownload = webviz_config.generic_plugins._example_data_download:ExampleDataDownload",
69 | "ExamplePlugin = webviz_config.generic_plugins._example_plugin:ExamplePlugin",
70 | "ExamplePortable = webviz_config.generic_plugins._example_portable:ExamplePortable",
71 | "ExampleTour = webviz_config.generic_plugins._example_tour:ExampleTour",
72 | "BannerImage = webviz_config.generic_plugins._banner_image:BannerImage",
73 | "DataTable = webviz_config.generic_plugins._data_table:DataTable",
74 | "EmbedPdf = webviz_config.generic_plugins._embed_pdf:EmbedPdf",
75 | "Markdown = webviz_config.generic_plugins._markdown:Markdown",
76 | "SyntaxHighlighter = webviz_config.generic_plugins._syntax_highlighter:SyntaxHighlighter",
77 | "TablePlotter = webviz_config.generic_plugins._table_plotter:TablePlotter",
78 | "PivotTable = webviz_config.generic_plugins._pivot_table:PivotTable",
79 | ],
80 | "pytest11": ["webviz = webviz_config.testing._plugin"],
81 | },
82 | install_requires=[
83 | "bleach[css]>=5",
84 | "cryptography>=2.4",
85 | "dash>=2.0",
86 | "dash-pivottable>=0.0.2",
87 | "flask>=2.0",
88 | "flask-caching>=1.4",
89 | "flask-talisman>=0.6",
90 | "jinja2>=2.10",
91 | "markdown>=3.0",
92 | "msal>=1.5.0",
93 | "orjson>=3.3",
94 | "pandas>=1.0",
95 | "pyarrow>=0.16",
96 | "pyyaml>=5.1",
97 | "requests>=2.20",
98 | "tqdm>=4.8",
99 | "webviz-core-components>=0.6",
100 | "werkzeug>=2.0",
101 | ],
102 | extras_require={
103 | "tests": TESTS_REQUIRES,
104 | "deployment": [
105 | "aiohttp", # https://github.com/Azure/azure-sdk-for-python/issues/19201
106 | "azure-core",
107 | "azure-identity",
108 | "azure-mgmt-resource",
109 | "azure-mgmt-storage",
110 | "azure-storage-blob",
111 | ],
112 | },
113 | setup_requires=["setuptools_scm~=3.2"],
114 | python_requires="~=3.8",
115 | use_scm_version=True,
116 | zip_safe=False,
117 | project_urls={
118 | "Documentation": "https://equinor.github.io/webviz-config",
119 | "Download": "https://pypi.org/project/webviz-config",
120 | "Source": "https://github.com/equinor/webviz-config",
121 | "Tracker": "https://github.com/equinor/webviz-config/issues",
122 | },
123 | classifiers=[
124 | "Programming Language :: Python :: 3",
125 | "Operating System :: OS Independent",
126 | "Natural Language :: English",
127 | "Environment :: Web Environment",
128 | "Framework :: Dash",
129 | "Framework :: Flask",
130 | "Topic :: Multimedia :: Graphics",
131 | "Topic :: Scientific/Engineering",
132 | "Topic :: Scientific/Engineering :: Visualization",
133 | "License :: OSI Approved :: MIT License",
134 | ],
135 | )
136 |
--------------------------------------------------------------------------------
/webviz_config/generic_plugins/_example_wlf_plugin/_plugin.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from dash import Input
4 |
5 | from webviz_config import WebvizPluginABC
6 | from webviz_config.utils import StrEnum
7 | from ._views._plot import (
8 | PlotView,
9 | PlotViewElement,
10 | PlotViewSettingsGroup,
11 | PlotViewElementSettingsGroup,
12 | )
13 | from ._views._table import TableView, TableViewElement, TableViewSettingsGroup
14 | from ._shared_view_elements import TextViewElement
15 | from ._shared_settings import SharedSettingsGroup
16 |
17 |
18 | class ExampleWlfPlugin(WebvizPluginABC):
19 | class Ids(StrEnum):
20 | PLOT_VIEW = "plot-view"
21 | TABLE_VIEW = "table-view"
22 | SHARED_SETTINGS = "shared-settings"
23 |
24 | def __init__(self, title: str):
25 | super().__init__(stretch=True)
26 |
27 | self.data = [(x, x * x) for x in range(0, 10)]
28 | self.title = title
29 |
30 | self.settings_group = SharedSettingsGroup()
31 | self.add_shared_settings_group(
32 | self.settings_group, ExampleWlfPlugin.Ids.SHARED_SETTINGS
33 | )
34 |
35 | self.add_view(
36 | PlotView(
37 | self.data,
38 | PlotView.Slots(
39 | kindness_selector=Input(
40 | self.settings_group.component_unique_id(
41 | SharedSettingsGroup.Ids.KINDNESS_SELECTOR
42 | ).to_string(),
43 | "value",
44 | ),
45 | power_selector=Input(
46 | self.settings_group.component_unique_id(
47 | SharedSettingsGroup.Ids.POWER_SELECTOR
48 | ).to_string(),
49 | "value",
50 | ),
51 | ),
52 | ),
53 | ExampleWlfPlugin.Ids.PLOT_VIEW,
54 | )
55 | self.add_view(
56 | TableView(
57 | self.data,
58 | TableView.Slots(
59 | kindness_selector=Input(
60 | self.settings_group.component_unique_id(
61 | SharedSettingsGroup.Ids.KINDNESS_SELECTOR
62 | ).to_string(),
63 | "value",
64 | ),
65 | power_selector=Input(
66 | self.settings_group.component_unique_id(
67 | SharedSettingsGroup.Ids.POWER_SELECTOR
68 | ).to_string(),
69 | "value",
70 | ),
71 | ),
72 | ),
73 | ExampleWlfPlugin.Ids.TABLE_VIEW,
74 | )
75 |
76 | @property
77 | def tour_steps(self) -> List[dict]:
78 | return [
79 | {
80 | "id": self.view(ExampleWlfPlugin.Ids.PLOT_VIEW)
81 | .view_element(PlotView.Ids.TEXT)
82 | .component_unique_id(TextViewElement.Ids.TEXT),
83 | "content": "Greetings from your example plugin.",
84 | },
85 | {
86 | "id": self.settings_group.component_unique_id("kindness-selector"),
87 | "content": "You can change here if this shall be friendly or not.",
88 | },
89 | {
90 | "id": self.view(ExampleWlfPlugin.Ids.PLOT_VIEW)
91 | .view_element(PlotView.Ids.PLOT)
92 | .component_unique_id(PlotViewElement.Ids.GRAPH),
93 | "content": "Over here you see a plot that shows x² or x³.",
94 | },
95 | {
96 | "id": self.settings_group.component_unique_id(
97 | SharedSettingsGroup.Ids.POWER_SELECTOR
98 | ),
99 | "content": "You can change here which exponent you prefer.",
100 | },
101 | {
102 | "id": self.view(ExampleWlfPlugin.Ids.PLOT_VIEW)
103 | .settings_group(PlotView.Ids.PLOT_SETTINGS)
104 | .component_unique_id(PlotViewSettingsGroup.Ids.COORDINATES_SELECTOR),
105 | "content": "...and here you can swap the axes.",
106 | },
107 | {
108 | "id": self.view(ExampleWlfPlugin.Ids.PLOT_VIEW)
109 | .view_element(PlotView.Ids.PLOT)
110 | .settings_group(PlotViewElement.Ids.PLOT_SETTINGS)
111 | .component_unique_id(PlotViewElementSettingsGroup.Ids.COLOR_SELECTOR),
112 | "content": "You can change here which color you prefer.",
113 | },
114 | {
115 | "id": self.view(ExampleWlfPlugin.Ids.TABLE_VIEW)
116 | .view_element(TableView.Ids.TABLE)
117 | .component_unique_id(TableViewElement.Ids.TABLE),
118 | "content": "There is also a table visualizing the data.",
119 | },
120 | {
121 | "id": self.view(ExampleWlfPlugin.Ids.TABLE_VIEW)
122 | .settings_group(TableView.Ids.TABLE_SETTINGS)
123 | .component_unique_id(TableViewSettingsGroup.Ids.ORDER_SELECTOR),
124 | "content": "You can change the order of the table here.",
125 | },
126 | ]
127 |
--------------------------------------------------------------------------------
/webviz_config/_deployment/github_cli.py:
--------------------------------------------------------------------------------
1 | import json
2 | import shutil
3 | import tempfile
4 | import subprocess
5 | from pathlib import Path
6 |
7 |
8 | def binary_available() -> bool:
9 | """Returns True if the GitHub CLI binary (gh) is
10 | availabile in $PATH, otherwise returns False.
11 | """
12 | return shutil.which("gh") is not None
13 |
14 |
15 | def logged_in() -> bool:
16 | # pylint: disable=subprocess-run-check
17 | result = subprocess.run(
18 | ["gh", "auth", "status"],
19 | capture_output=True,
20 | )
21 | return b"Logged in to github.com" in result.stderr
22 |
23 |
24 | def log_in() -> None:
25 | # pylint: disable=subprocess-run-check
26 | subprocess.run(
27 | ["gh", "auth", "login"],
28 | )
29 |
30 |
31 | def repo_exists(github_slug: str) -> bool:
32 | # pylint: disable=subprocess-run-check
33 | result = subprocess.run(
34 | ["gh", "repo", "view", github_slug],
35 | capture_output=True,
36 | )
37 |
38 | if not result.stderr:
39 | return True
40 | if b"Could not resolve" in result.stderr:
41 | return False
42 | raise RuntimeError(result.stderr)
43 |
44 |
45 | def create_github_repository(github_slug: str, directory: Path) -> Path:
46 | """Creates a new private github repository. github_slug is on format
47 | owner/reponame.
48 | """
49 | if not "/" in github_slug:
50 | raise ValueError("repo_path argument should be on format owner/reponame")
51 |
52 | subprocess.run(
53 | ["gh", "repo", "create", github_slug, "--private", "--confirm"],
54 | capture_output=True,
55 | check=True,
56 | cwd=directory,
57 | )
58 |
59 | subprocess.run(
60 | ["gh", "repo", "clone", github_slug],
61 | capture_output=True,
62 | check=True,
63 | cwd=directory,
64 | )
65 |
66 | return directory / github_slug.split("/")[1]
67 |
68 |
69 | def turn_on_github_vulnerability_alers(directory: Path) -> None:
70 | subprocess.run(
71 | [
72 | "gh",
73 | "api",
74 | "repos/:owner/:repo/vulnerability-alerts",
75 | "--method",
76 | "PUT",
77 | "--header",
78 | "Accept: application/vnd.github.dorian-preview+json",
79 | ],
80 | check=True,
81 | cwd=directory,
82 | )
83 |
84 |
85 | def _call_post_api(endpoint: str, data: dict, directory: Path) -> None:
86 | subprocess.run(
87 | [
88 | "gh",
89 | "api",
90 | endpoint,
91 | "--method",
92 | "POST",
93 | "--input",
94 | "-",
95 | "--silent",
96 | ],
97 | input=json.dumps(data),
98 | check=True,
99 | cwd=directory,
100 | text=True,
101 | )
102 |
103 |
104 | def add_webhook(directory: Path, receiver_url: str, secret: str) -> None:
105 | data = {
106 | "name": "web",
107 | "active": True,
108 | "config": {
109 | "url": receiver_url,
110 | "content_type": "json",
111 | "secret": secret,
112 | },
113 | }
114 | _call_post_api(data=data, endpoint="repos/:owner/:repo/hooks", directory=directory)
115 |
116 |
117 | def add_deploy_key(directory: Path, title: str, key: str) -> None:
118 | _call_post_api(
119 | data={"title": title, "key": key},
120 | endpoint="repos/:owner/:repo/keys",
121 | directory=directory,
122 | )
123 |
124 |
125 | def read_file_in_repository(github_slug: str, filename: str) -> str:
126 | with tempfile.TemporaryDirectory() as tmp_dir:
127 | temp_dir = Path(tmp_dir)
128 | subprocess.run(
129 | ["git", "clone", f"git@github.com:{github_slug}"],
130 | check=True,
131 | cwd=temp_dir,
132 | capture_output=True,
133 | )
134 | clone_path = temp_dir / github_slug.split("/")[1]
135 |
136 | return (clone_path / Path(filename)).read_text()
137 |
138 |
139 | def commit_portable_webviz(
140 | github_slug: str,
141 | source_directory: Path,
142 | commit_message: str = "Initial commit",
143 | branch_name: str = "main",
144 | ) -> None:
145 |
146 | with tempfile.TemporaryDirectory() as tmp_dir:
147 | temp_dir = Path(tmp_dir)
148 | subprocess.run(
149 | ["git", "clone", f"git@github.com:{github_slug}"],
150 | check=True,
151 | cwd=temp_dir,
152 | capture_output=True,
153 | )
154 |
155 | clone_path = temp_dir / github_slug.split("/")[1]
156 |
157 | shutil.copytree(
158 | source_directory,
159 | clone_path,
160 | dirs_exist_ok=True,
161 | ignore=shutil.ignore_patterns("resources"),
162 | )
163 |
164 | commands = [
165 | ["git", "add", "."],
166 | ["git", "commit", "-m", commit_message, "--allow-empty"],
167 | ["git", "branch", "-M", branch_name],
168 | ["git", "push", "-u", "origin", branch_name],
169 | ]
170 | for command in commands:
171 | subprocess.run(
172 | command,
173 | check=True,
174 | cwd=clone_path,
175 | capture_output=True,
176 | )
177 |
--------------------------------------------------------------------------------
/webviz_config/utils/_callback_typecheck.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=line-too-long
2 | from typing import Any, Callable, get_args, get_origin, _TypedDictMeta, TypeVar, Union # type: ignore[attr-defined]
3 | import inspect
4 |
5 | T = TypeVar("T")
6 |
7 |
8 | class ConversionError(Exception):
9 | pass
10 |
11 |
12 | def _isinstance(arg: Any, annotation: Any) -> bool:
13 | # pylint: disable=too-many-return-statements, too-many-branches
14 | if annotation is type(None) or annotation is None:
15 | return arg is None
16 |
17 | if annotation is Any:
18 | return True
19 |
20 | if get_origin(annotation) is None:
21 | try:
22 | return isinstance(arg, annotation)
23 | except TypeError:
24 | return False
25 |
26 | if get_origin(annotation) == Union:
27 | for annotation_arg in get_args(annotation):
28 | if _isinstance(arg, annotation_arg):
29 | return True
30 |
31 | if get_origin(annotation) is list and isinstance(arg, list):
32 | result = True
33 | type_args = get_args(annotation)
34 | if len(type_args) == 1:
35 | for annotation_arg in arg:
36 | result &= _isinstance(annotation_arg, type_args[0])
37 | return result
38 |
39 | if get_origin(annotation) is dict and isinstance(arg, dict):
40 | result = True
41 | type_args = get_args(annotation)
42 | if len(type_args) == 2:
43 | for key, value in arg.items():
44 | result &= _isinstance(key, type_args[0])
45 | result &= _isinstance(value, type_args[1])
46 | return result
47 |
48 | return False
49 |
50 |
51 | def convert(arg: Any, convert_to: T) -> T:
52 | # pylint: disable=too-many-return-statements, too-many-branches
53 | additional_error_message: str = ""
54 | try:
55 | if _isinstance(arg, convert_to):
56 | return arg
57 | if (
58 | inspect.isclass(convert_to)
59 | and not isinstance(convert_to, _TypedDictMeta)
60 | and arg is not None
61 | ):
62 | return convert_to(arg)
63 | if (
64 | isinstance(convert_to, _TypedDictMeta)
65 | and "__annotations__" in dir(convert_to)
66 | and isinstance(arg, dict)
67 | ):
68 | new_dict = convert_to()
69 | for key, value in arg.items():
70 | if key in list(convert_to.__annotations__.keys()):
71 | new_dict[key] = convert(value, convert_to.__annotations__[key])
72 | else:
73 | raise Exception(
74 | f"""
75 | Key '{key}' not allowed in '{convert_to}'.\n
76 | Allowed keys are: {', '.join(list(convert_to.__annotations__.keys()))}
77 | """
78 | )
79 |
80 | if not convert_to.__total__ or len(new_dict.keys()) == len(
81 | convert_to.__annotations__.keys()
82 | ):
83 | return new_dict
84 |
85 | if convert_to is list and isinstance(arg, list):
86 | return arg # type: ignore[return-value]
87 | if get_origin(convert_to) is list and isinstance(arg, list):
88 | return [convert(a, get_args(convert_to)[0]) for a in arg] # type: ignore[return-value]
89 | if convert_to is dict and isinstance(arg, dict):
90 | return arg # type: ignore[return-value]
91 | if get_origin(convert_to) is dict and isinstance(arg, dict):
92 | if len(get_args(convert_to)) == 2:
93 | return { # type: ignore[return-value]
94 | convert(key, get_args(convert_to)[0]): convert(
95 | value, get_args(convert_to)[1]
96 | )
97 | for key, value in arg.items()
98 | }
99 | if get_origin(convert_to) is Union and "__args__" in dir(convert_to):
100 | for convert_type in get_args(convert_to):
101 | try:
102 | return convert(arg, convert_type)
103 | except ConversionError:
104 | pass
105 |
106 | # pylint: disable=broad-except
107 | except Exception as exception:
108 | additional_error_message = f"\n\nMore details:\n{exception}"
109 |
110 | raise ConversionError(
111 | f"Argument of type '{type(arg)}' cannot be converted to type '{convert_to}'.{additional_error_message}"
112 | )
113 |
114 |
115 | def callback_typecheck(func: Callable) -> Callable:
116 | signature = inspect.signature(func)
117 | parameters = list(signature.parameters.values())
118 |
119 | def wrapper(*_args) -> signature.return_annotation: # type: ignore[no-untyped-def,name-defined]
120 | adjusted_args: list = []
121 |
122 | for index, arg in enumerate(_args):
123 | try:
124 | adjusted_args.append(convert(arg, parameters[index].annotation))
125 | except ConversionError as exception:
126 | raise ConversionError(
127 | f"Error while converting input to argument '{parameters[index].name}' of function '{func.__name__}' in file '{func.__globals__['__file__']}': {exception}"
128 | ) from exception
129 |
130 | return func(*adjusted_args)
131 |
132 | return wrapper
133 |
--------------------------------------------------------------------------------
/webviz_config/_build_webviz.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import shutil
4 | import pathlib
5 | import tempfile
6 | import subprocess # nosec
7 | import argparse
8 |
9 | from yaml import YAMLError
10 |
11 | from ._config_parser import ParserError
12 | from ._write_script import write_script
13 | from ._dockerize import create_docker_setup
14 | from .themes import installed_themes
15 | from .utils import terminal_colors
16 |
17 | BUILD_FILENAME = "webviz_app.py"
18 | STATIC_FOLDER = pathlib.Path(__file__).resolve().parent / "static"
19 |
20 |
21 | # Restructure here before merge.
22 | # pylint: disable=too-many-branches
23 | def build_webviz(args: argparse.Namespace) -> None:
24 |
25 | if args.theme not in installed_themes:
26 | raise ValueError(f"Theme `{args.theme}` is not installed.")
27 |
28 | if args.portable is None:
29 | build_directory = pathlib.Path(tempfile.mkdtemp())
30 | else:
31 | build_directory = args.portable.resolve()
32 | build_directory.mkdir(parents=True)
33 |
34 | shutil.copytree(STATIC_FOLDER / "assets", build_directory / "resources" / "assets")
35 |
36 | for asset in installed_themes[args.theme].assets:
37 | shutil.copy(asset, build_directory / "resources" / "assets")
38 |
39 | (build_directory / "theme_settings.json").write_text(
40 | installed_themes[args.theme].to_json()
41 | )
42 |
43 | try:
44 | if args.portable:
45 | print(
46 | f"{terminal_colors.BLUE}{terminal_colors.BOLD}"
47 | "Saving requested data to build folder "
48 | "such that the webviz instance is portable."
49 | f"{terminal_colors.END}"
50 | )
51 |
52 | write_script(
53 | args, build_directory, "copy_data_template.py.jinja2", "copy_data.py"
54 | )
55 |
56 | if subprocess.call( # nosec
57 | [sys.executable, "copy_data.py"], cwd=build_directory
58 | ):
59 | # We simply exit here with a non-zero status in order to not clutter
60 | # the subprocess traceback with unnecessary information
61 | sys.exit(1)
62 |
63 | (build_directory / "copy_data.py").unlink()
64 |
65 | print(
66 | f"{terminal_colors.GREEN}{terminal_colors.BOLD}"
67 | "Finished data extraction. All output saved."
68 | f"{terminal_colors.END}"
69 | )
70 |
71 | non_default_assets, plugin_metadata = write_script(
72 | args, build_directory, "webviz_template.py.jinja2", BUILD_FILENAME
73 | )
74 |
75 | for asset in non_default_assets:
76 | shutil.copy(asset, build_directory / "resources" / "assets")
77 |
78 | if args.portable:
79 | for filename in ["README.md", ".dockerignore", ".gitignore"]:
80 | shutil.copy(STATIC_FOLDER / filename, build_directory)
81 | create_docker_setup(build_directory, plugin_metadata)
82 | else:
83 | run_webviz(args, build_directory)
84 |
85 | finally:
86 | if not args.portable:
87 | shutil.rmtree(build_directory)
88 |
89 |
90 | def run_webviz(args: argparse.Namespace, build_directory: pathlib.Path) -> None:
91 |
92 | print(
93 | f"{terminal_colors.YELLOW}"
94 | " Starting up your webviz application. Please wait..."
95 | f"{terminal_colors.END}"
96 | )
97 |
98 | with subprocess.Popen( # nosec
99 | [sys.executable, BUILD_FILENAME], cwd=build_directory
100 | ) as app_process:
101 |
102 | lastmtime = args.yaml_file.stat().st_mtime
103 |
104 | while app_process.poll() is None:
105 | try:
106 | time.sleep(1)
107 |
108 | if lastmtime != args.yaml_file.stat().st_mtime:
109 | lastmtime = args.yaml_file.stat().st_mtime
110 | write_script(
111 | args,
112 | build_directory,
113 | "webviz_template.py.jinja2",
114 | BUILD_FILENAME,
115 | )
116 | print(
117 | f"{terminal_colors.BLUE}{terminal_colors.BOLD}"
118 | " Rebuilt webviz dash app from configuration file"
119 | f"{terminal_colors.END}"
120 | )
121 |
122 | except (ParserError, YAMLError) as excep:
123 | print(
124 | f"{excep} {terminal_colors.RED}{terminal_colors.BOLD}"
125 | "Fix the error and save the configuration file in "
126 | "order to trigger a new rebuild."
127 | f"{terminal_colors.END}"
128 | )
129 |
130 | except KeyboardInterrupt:
131 | app_process.kill()
132 | print(
133 | f"\r{terminal_colors.BLUE}{terminal_colors.BOLD}"
134 | " Shutting down the webviz application on user request."
135 | f"{terminal_colors.END}"
136 | )
137 |
138 | except Exception as excep:
139 | app_process.kill()
140 | print(
141 | f"{terminal_colors.RED}{terminal_colors.BOLD}"
142 | "Unexpected error. Killing the webviz application process."
143 | f"{terminal_colors.END}"
144 | )
145 | raise excep
146 |
--------------------------------------------------------------------------------