Skip to content

Commit 9a46f59

Browse files
committed
Improved Docker and cloud support
1 parent 5184ebc commit 9a46f59

33 files changed

+1838
-159
lines changed

.github/workflows/webviz-config.yml

+8
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ jobs:
7474
webviz docs --portable ./docs_build --skip-open
7575
webviz schema
7676
77+
- name: 🐳 Build Docker example image
78+
run: |
79+
export GIT_POINTER_WEBVIZ_CONFIG=$GITHUB_REF
80+
webviz build ./examples/basic_example.yaml --portable ./some_portable_app
81+
pushd ./some_portable_app
82+
docker build .
83+
rm -rf ./some_portable_app
84+
7785
- name: 🚢 Build and deploy Python package
7886
if: github.event_name == 'release' && matrix.python-version == '3.6'
7987
env:

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,22 @@ generated docs when having type hinted return value of plugin `__init__` functio
2121

2222
## [0.2.3] - 2020-11-26
2323

24+
### Changed
25+
- [#318](https://github.com/equinor/webviz-config/pull/318) - Ad-hoc plugins not
26+
supported anymore (i.e. plugins not being part of a Python project with `setup.py`).
27+
This enables us to reserve the configuration file syntax `prefix.PluginName` to later
28+
have a way for the user to specify which plugin project `PluginName` should be taken
29+
from, in cases where multiple plugin projects provide a plugin with the same name.
30+
Requiring a formal Python project also enables useful features in the framework
31+
(see the `Added` section for this PR).
32+
2433
### Added
34+
- [#318](https://github.com/equinor/webviz-config/pull/318) - `webviz-config`
35+
now facilitates automatically including necessary plugin projects as dependencies
36+
in generated Docker setup. Private repositories are also supported, however the
37+
Docker build process would then need to be given deploy keys as environment variable
38+
secrets. First iteration of automatically creating a corresponding
39+
[Radix config](https://www.radix.equinor.com/) is also added.
2540
- [#337](https://github.com/equinor/webviz-config/pull/337) - New generic plugin to
2641
generate a [Pivot table](https://en.wikipedia.org/wiki/Pivot_table) based on
2742
[Dash Pivottable](https://github.com/plotly/dash-pivottable).

CONTRIBUTING.md

+39-54
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
- [User provided arguments](#user-provided-arguments)
99
- [Data input](#data-input)
1010
- [Deattaching data from its original source](#deattaching-data-from-its-original-source)
11-
- [Custom ad-hoc plugins](#custom-ad-hoc-plugins)
1211
- [Run tests](#run-tests)
1312
- [Build documentation](#build-documentation)
13+
- [Improve plugin documentation](#improve-plugin-documentation)
14+
- [Make your plugin project available](#make-your-plugin-project-available)
1415

1516
## Creating a new plugin
1617

@@ -385,7 +386,7 @@ The core of `webviz-config` will do the following:
385386
2) If the user asks for a portable version, it will
386387
1) Before writing the actual dash code, it will run all decorated functions
387388
with the given argument combinations. The resulting dataframes are stored
388-
in a folder `./webviz_storage` as parquet files.
389+
in a folder `./resources/webviz_storage` as parquet files.
389390
2) It writes the webviz-dash code (as usual), but this the decorated
390391
functions will return the dataframe from the stored `.parquet` files,
391392
instead of running the actual function code.
@@ -457,58 +458,6 @@ signature is not necessary, however if you do you will get a
457458
instance representing the absolute path to the configuration file that was used, and/or
458459
a boolean value stating if the Webviz application running is a portable one.
459460

460-
### Custom ad-hoc plugins
461-
462-
It is possible to create custom plugins which still can be included through
463-
the configuration file, which could be useful for quick prototyping.
464-
465-
As an example, assume someone on your project has made the Python file
466-
467-
```python
468-
import dash_html_components as html
469-
from webviz_config import WebvizPluginABC
470-
471-
472-
class OurCustomPlugin(WebvizPluginABC):
473-
474-
def __init__(self, title: str):
475-
476-
super().__init__()
477-
478-
self.title = title
479-
480-
@property
481-
def layout(self):
482-
return html.Div([
483-
html.H1(self.title),
484-
'This is just some ordinary text'
485-
])
486-
```
487-
488-
If this is saved such that it is available through e.g. a
489-
[module](https://docs.python.org/3/tutorial/modules.html)
490-
`ourmodule`, the user can include the custom plugin the same way as a standard
491-
plugin, with the only change of also naming the module:
492-
```yaml
493-
title: Simple Webviz example
494-
495-
pages:
496-
497-
- title: Front page
498-
content:
499-
- ourmodule.OurCustomPlugin:
500-
title: Title of my custom plugin
501-
```
502-
503-
Note that this might involve appending your `$PYTHONPATH` environment
504-
variable with the path where your custom module is located. The same principle
505-
applies if the custom plugin is saved in a package with submodule(s),
506-
```yaml
507-
...
508-
- ourpackage.ourmodule.OurCustomPlugin:
509-
...
510-
```
511-
512461
## Run tests
513462

514463
To run tests it is necessary to first install the [selenium chrome driver](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver).
@@ -584,3 +533,39 @@ $$\alpha = \frac{\beta}{\gamma}$$
584533

585534
Example of auto-built documentation for `webviz-config` can be seen
586535
[here on github](https://equinor.github.io/webviz-config/).
536+
537+
## Make your plugin project available
538+
539+
It is strongly recommended to store your plugin project code base in a `git` solution,
540+
e.g. [in a new GitHub repository](https://github.com/new). If possible, it is also
541+
_highly_ recommended to let it be an open source repository. This has several advantages:
542+
- Others can reuse your work - and help each other achieve better code quality, add more features and share maintenance
543+
- You will from the beginning make sure you keep a good separation between visualization code and data loading/processing code.
544+
- It becomes easier for your plugin users to use it in e.g. Docker and in cloud hosting.
545+
546+
`webviz-config` will make sure that portable builds, which uses one (or more) plugins
547+
from your plugin project, includes installation instructions for the project also in
548+
the Docker build instructions. In order for `webviz-config` to do this, you should add
549+
[`project_urls` metadata](https://packaging.python.org/guides/distributing-packages-using-setuptools/#project-urls)
550+
in the project's `setup.py`. An example:
551+
```
552+
project_urls={
553+
"Download": "https://pypi.org/project/webviz-config",
554+
"Source": "https://github.com/equinor/webviz-config",
555+
},
556+
```
557+
The following logic applies when the user creates a portable application:
558+
- If `Download` is specified *and* the plugin project version installed is available
559+
on PyPI, the Dockerfile will `pip install` the same version from PyPI.
560+
- Otherwise the Dockerfile will install from the `Source` url. From the `setuptools_scm`
561+
provided version, the correct commit/version/tag will be installed.
562+
563+
Note that if you are a developer and working on a fork, you might want to temporarily
564+
override the `Source` url to your fork. You can do this by specifying an environment
565+
variable `SOURCE_URL_NAME_PLUGIN_PROJECT=https://urlyourfork`. If you also want to
566+
explicitly state git pointer/reference (thereby not use the one derived from
567+
`setuptools_scm` version) you can set the environment variable
568+
`GIT_POINTER_NAME_PLUGIN_PROJECT`.
569+
570+
For private repositories, a GitHub SSH deploy key will need to be provided to the Docker
571+
build process (see instructions in `README` created with the portable application).

INTRODUCTION.md

+16
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,19 @@ If you are using Visual Studio Code, we recommend [Red Hat's YAML extension](htt
113113
}
114114
```
115115
to your `settings.json` file, you will get help from the editor on YAML files following the namepatterns to the right (might have to restart the editor after updating the settings).
116+
117+
#### Deployment
118+
119+
When you have created a portable Webviz application, you are approximately one command away of either creating a new application in cloud (or updating an existing application).
120+
121+
##### Azure
122+
123+
Automatic deployment to Azure Web app service is very much possible, and only a community PR away (most of the machinery and Azure CLI wrappers are already in place through the Radix deployment feature).
124+
125+
##### Radix
126+
127+
[Radix is an open source hosting framework](https://github.com/equinor/radix-platform/) by Equinor. If you execute
128+
```bash
129+
webviz deploy radix ./your_portable_app owner/reponame --initial
130+
```
131+
Webviz will do the following:

setup.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,24 @@ def get_long_description() -> str:
8282
"pandas>=1.0",
8383
"pyarrow>=0.16",
8484
"pyyaml>=5.1",
85+
"requests>=2.20",
8586
"tqdm>=4.8",
8687
"importlib-metadata>=1.7; python_version<'3.8'",
8788
"typing-extensions>=3.7; python_version<'3.8'",
8889
"webviz-core-components>=0.1.0",
8990
],
90-
tests_require=TESTS_REQUIRES,
91-
extras_require={"tests": TESTS_REQUIRES},
91+
extras_require={
92+
"tests": TESTS_REQUIRES,
93+
"deploy": ["azure-cli"],
94+
},
9295
setup_requires=["setuptools_scm~=3.2"],
9396
python_requires="~=3.6",
9497
use_scm_version=True,
9598
zip_safe=False,
9699
project_urls={
97100
"Documentation": "https://equinor.github.io/webviz-config",
98101
"Download": "https://pypi.org/project/webviz-config",
102+
"Source": "https://github.com/equinor/webviz-config",
99103
"Tracker": "https://github.com/equinor/webviz-config/issues",
100104
},
101105
classifiers=[

tests/test_plugin_init.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, entry_points, name):
3030
def test_no_warning():
3131
globals_mock = {}
3232
with warnings.catch_warnings(record=True) as warn:
33-
metadata = load_webviz_plugins_with_metadata(
33+
metadata, _ = load_webviz_plugins_with_metadata(
3434
[dist_mock1, dist_mock3], globals_mock
3535
)
3636
assert len(warn) == 0, "Too many warnings"
@@ -43,7 +43,7 @@ def test_no_warning():
4343
def test_warning_multiple():
4444
globals_mock = {}
4545
with warnings.catch_warnings(record=True) as warn:
46-
metadata = load_webviz_plugins_with_metadata(
46+
metadata, _ = load_webviz_plugins_with_metadata(
4747
[dist_mock1, dist_mock2], globals_mock
4848
)
4949

tests/test_plugin_metadata.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import webviz_config
2-
from webviz_config.plugins import metadata
2+
from webviz_config.plugins import plugin_project_metadata
33

44

55
def test_webviz_config_metadata():
6-
meta = metadata["BannerImage"]
76

8-
assert meta["dist_name"] == "webviz-config"
9-
assert meta["dist_version"] == webviz_config.__version__
7+
metadata = plugin_project_metadata["webviz-config"]
108

11-
assert meta["documentation_url"] == "https://equinor.github.io/webviz-config"
12-
assert meta["download_url"] == "https://pypi.org/project/webviz-config"
13-
assert meta["tracker_url"] == "https://github.com/equinor/webviz-config/issues"
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"

webviz_config/_build_webviz.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010

1111
from ._config_parser import ParserError
1212
from ._write_script import write_script
13+
from ._dockerize import create_docker_setup
1314
from .themes import installed_themes
1415
from .utils import terminal_colors
1516

1617
BUILD_FILENAME = "webviz_app.py"
1718
STATIC_FOLDER = pathlib.Path(__file__).resolve().parent / "static"
1819

1920

21+
# Restructure here before merge.
22+
# pylint: disable=too-many-branches
2023
def build_webviz(args: argparse.Namespace) -> None:
2124

2225
if args.theme not in installed_themes:
@@ -28,13 +31,10 @@ def build_webviz(args: argparse.Namespace) -> None:
2831
build_directory = args.portable.resolve()
2932
build_directory.mkdir(parents=True)
3033

31-
shutil.copytree(STATIC_FOLDER / "assets", build_directory / "assets")
32-
33-
for filename in ["README.md", "Dockerfile", ".dockerignore"]:
34-
shutil.copy(STATIC_FOLDER / filename, build_directory)
34+
shutil.copytree(STATIC_FOLDER / "assets", build_directory / "resources" / "assets")
3535

3636
for asset in installed_themes[args.theme].assets:
37-
shutil.copy(asset, build_directory / "assets")
37+
shutil.copy(asset, build_directory / "resources" / "assets")
3838

3939
(build_directory / "theme_settings.json").write_text(
4040
installed_themes[args.theme].to_json()
@@ -71,14 +71,18 @@ def build_webviz(args: argparse.Namespace) -> None:
7171
f"{terminal_colors.END}"
7272
)
7373

74-
non_default_assets = write_script(
74+
non_default_assets, plugin_metadata = write_script(
7575
args, build_directory, "webviz_template.py.jinja2", BUILD_FILENAME
7676
)
7777

7878
for asset in non_default_assets:
79-
shutil.copy(asset, build_directory / "assets")
79+
shutil.copy(asset, build_directory / "resources" / "assets")
8080

81-
if not args.portable:
81+
if args.portable:
82+
for filename in ["README.md", ".dockerignore", ".gitignore"]:
83+
shutil.copy(STATIC_FOLDER / filename, build_directory)
84+
create_docker_setup(build_directory, plugin_metadata)
85+
else:
8286
run_webviz(args, build_directory)
8387

8488
finally:

webviz_config/_config_parser.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import sys
33
import pathlib
44
import inspect
5-
import typing
5+
from typing import Dict, List, Optional
66

77
import yaml
88

9-
from . import plugins as standard_plugins
9+
import webviz_config.plugins
1010
from .utils import terminal_colors
1111
from .utils._get_webviz_plugins import _get_webviz_plugins
1212

@@ -17,7 +17,7 @@ def _call_signature(
1717
plugin_name: str,
1818
kwargs: dict,
1919
config_folder: pathlib.Path,
20-
contact_person: typing.Optional[dict] = None,
20+
contact_person: Optional[dict] = None,
2121
) -> tuple:
2222
# pylint: disable=too-many-branches,too-many-statements
2323
"""Takes as input the name of a plugin together with user given arguments
@@ -31,7 +31,9 @@ def _call_signature(
3131
* If there is type mismatch between user given argument value, and type
3232
hint in __init__ signature (given that type hint exist)
3333
"""
34-
argspec = inspect.getfullargspec(getattr(standard_plugins, plugin_name).__init__)
34+
argspec = inspect.getfullargspec(
35+
getattr(webviz_config.plugins, plugin_name).__init__
36+
)
3537

3638
if argspec.defaults is not None:
3739
required_args = argspec.args[: -len(argspec.defaults)]
@@ -90,7 +92,7 @@ def _call_signature(
9092

9193
if expected_type == pathlib.Path:
9294
kwargs[arg] = (config_folder / pathlib.Path(kwargs[arg])).resolve()
93-
elif expected_type == typing.List[pathlib.Path]:
95+
elif expected_type == List[pathlib.Path]:
9496
kwargs[arg] = [
9597
(config_folder / pathlib.Path(patharg)).resolve()
9698
for patharg in kwargs[arg]
@@ -126,7 +128,9 @@ class ParserError(Exception):
126128

127129
class ConfigParser:
128130

129-
STANDARD_PLUGINS = [name for (name, _) in _get_webviz_plugins(standard_plugins)]
131+
STANDARD_PLUGINS = [
132+
name for (name, _) in _get_webviz_plugins(webviz_config.plugins)
133+
]
130134

131135
def __init__(self, yaml_file: pathlib.Path):
132136

@@ -151,8 +155,9 @@ def __init__(self, yaml_file: pathlib.Path):
151155
).with_traceback(sys.exc_info()[2])
152156

153157
self._config_folder = pathlib.Path(yaml_file).parent
154-
self._page_ids: typing.List[str] = []
158+
self._page_ids: List[str] = []
155159
self._assets: set = set()
160+
self._plugin_metadata: Dict[str, dict] = {}
156161
self._used_plugin_packages: set = set()
157162
self.clean_configuration()
158163

@@ -281,7 +286,10 @@ def clean_configuration(self) -> None:
281286
self._config_folder,
282287
)
283288

284-
self.assets.update(getattr(standard_plugins, plugin_name).ASSETS)
289+
self._assets.update(getattr(webviz_config.plugins, plugin_name).ASSETS)
290+
self._plugin_metadata[
291+
plugin_name
292+
] = webviz_config.plugins.plugin_metadata[plugin_name]
285293

286294
@property
287295
def configuration(self) -> dict:
@@ -294,3 +302,10 @@ def shared_settings(self) -> dict:
294302
@property
295303
def assets(self) -> set:
296304
return self._assets
305+
306+
@property
307+
def plugin_metadata(self) -> Dict[str, dict]:
308+
"""Returns a dictionary of plugin metadata, and only for
309+
plugins included in the configuration file.
310+
"""
311+
return self._plugin_metadata

webviz_config/_deployment/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .radix import radix_deployment

0 commit comments

Comments
 (0)