dashiyaml 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dashi/__init__.py +1 -0
- dashi/charts/__init__.py +6 -0
- dashi/charts/bar.py +18 -0
- dashi/charts/chart.py +23 -0
- dashi/charts/line.py +18 -0
- dashi/charts/pie.py +20 -0
- dashi/charts/registry.py +13 -0
- dashi/charts/scatter.py +19 -0
- dashi/charts/table.py +13 -0
- dashi/cli.py +61 -0
- dashi/config/__init__.py +1 -0
- dashi/config/yaml_parser.py +43 -0
- dashi/dashboard.py +88 -0
- dashi/datasources/__init__.py +6 -0
- dashi/datasources/base_datasource.py +18 -0
- dashi/datasources/csv_datasource.py +10 -0
- dashi/datasources/datasources.py +40 -0
- dashi/datasources/duckdb_datasource.py +15 -0
- dashi/datasources/file_datasource.py +44 -0
- dashi/datasources/json_datasource.py +9 -0
- dashi/datasources/login_mixin.py +21 -0
- dashi/datasources/postgres_datasource.py +59 -0
- dashi/datasources/registry.py +12 -0
- dashi/datasources/sql_datasource.py +12 -0
- dashi/helpers.py +2 -0
- dashi/render.py +13 -0
- dashi/serve.py +16 -0
- dashi/structure/__init__.py +0 -0
- dashi/structure/cleaner.py +10 -0
- dashi/structure/initializer.py +139 -0
- dashi/transforms/__init__.py +0 -0
- dashi/transforms/transforms.py +31 -0
- dashiyaml-0.0.2.dist-info/METADATA +184 -0
- dashiyaml-0.0.2.dist-info/RECORD +38 -0
- dashiyaml-0.0.2.dist-info/WHEEL +5 -0
- dashiyaml-0.0.2.dist-info/entry_points.txt +2 -0
- dashiyaml-0.0.2.dist-info/licenses/LICENSE +21 -0
- dashiyaml-0.0.2.dist-info/top_level.txt +1 -0
dashi/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
dashi/charts/__init__.py
ADDED
dashi/charts/bar.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .chart import BaseChart
|
|
2
|
+
import plotly.express as px
|
|
3
|
+
import polars as pl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BarChart(BaseChart):
|
|
7
|
+
def build(
|
|
8
|
+
self,
|
|
9
|
+
chart_name: str,
|
|
10
|
+
df: pl.DataFrame,
|
|
11
|
+
x: str,
|
|
12
|
+
y: str,
|
|
13
|
+
options: dict[str, str] | None = None,
|
|
14
|
+
):
|
|
15
|
+
fig = px.bar(data_frame=df, x=x, y=y, title=chart_name)
|
|
16
|
+
if options is not None:
|
|
17
|
+
self.update_layout(fig, options)
|
|
18
|
+
return fig
|
dashi/charts/chart.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseChart(ABC):
|
|
6
|
+
def build(
|
|
7
|
+
self,
|
|
8
|
+
chart_name: str,
|
|
9
|
+
df: pl.DataFrame,
|
|
10
|
+
x: str,
|
|
11
|
+
y: str,
|
|
12
|
+
options: dict[str, str] | None,
|
|
13
|
+
):
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
def update_layout(self, fig, options: dict):
|
|
17
|
+
try:
|
|
18
|
+
fig.update_layout(**options)
|
|
19
|
+
return fig
|
|
20
|
+
except ValueError:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"One of the options provided in the chart definition is incorrect"
|
|
23
|
+
)
|
dashi/charts/line.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .chart import BaseChart
|
|
2
|
+
import polars as pl
|
|
3
|
+
import plotly.express as px
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LineChart(BaseChart):
|
|
7
|
+
def build(
|
|
8
|
+
self,
|
|
9
|
+
chart_name: str,
|
|
10
|
+
df: pl.DataFrame,
|
|
11
|
+
x: str,
|
|
12
|
+
y: str,
|
|
13
|
+
options: dict[str, str] | None = None,
|
|
14
|
+
):
|
|
15
|
+
fig = px.line(df, x=x, y=y, title=chart_name)
|
|
16
|
+
if options is not None:
|
|
17
|
+
self.update_layout(fig, options)
|
|
18
|
+
return fig
|
dashi/charts/pie.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
from .chart import BaseChart
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PieChart(BaseChart):
|
|
8
|
+
def build(
|
|
9
|
+
self,
|
|
10
|
+
chart_name: str,
|
|
11
|
+
df: pl.DataFrame,
|
|
12
|
+
x: str,
|
|
13
|
+
y: str,
|
|
14
|
+
options: dict[str, str] | None = None,
|
|
15
|
+
):
|
|
16
|
+
fig = px.pie(data_frame=df, values=x, names=y, title=chart_name)
|
|
17
|
+
if options is not None:
|
|
18
|
+
fig = fig.update_layout(options)
|
|
19
|
+
|
|
20
|
+
return fig
|
dashi/charts/registry.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .pie import PieChart
|
|
2
|
+
from .table import Table
|
|
3
|
+
from .bar import BarChart
|
|
4
|
+
from .line import LineChart
|
|
5
|
+
from .scatter import ScatterChart
|
|
6
|
+
|
|
7
|
+
CHARTS = {
|
|
8
|
+
"line": LineChart(),
|
|
9
|
+
"bar": BarChart(),
|
|
10
|
+
"pie": PieChart(),
|
|
11
|
+
"scatter": ScatterChart(),
|
|
12
|
+
"table": Table(),
|
|
13
|
+
}
|
dashi/charts/scatter.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
from .chart import BaseChart
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ScatterChart(BaseChart):
|
|
8
|
+
def build(
|
|
9
|
+
self,
|
|
10
|
+
chart_name: str,
|
|
11
|
+
df: pl.DataFrame,
|
|
12
|
+
x: str,
|
|
13
|
+
y: str,
|
|
14
|
+
options: dict[str, str] | None = None,
|
|
15
|
+
):
|
|
16
|
+
fig = px.scatter(df, x=x, y=y, title=chart_name)
|
|
17
|
+
if options is not None:
|
|
18
|
+
self.update_layout(fig, options)
|
|
19
|
+
return fig
|
dashi/charts/table.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from plotly import graph_objects as go
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Table:
|
|
7
|
+
def build(self, title: str, df: pl.DataFrame, columns: List[str]) -> go.Figure:
|
|
8
|
+
headers = [col for col in df.columns if col in columns]
|
|
9
|
+
data = [col for col in df.select(headers).to_dict(as_series=False).values()]
|
|
10
|
+
table = go.Figure(
|
|
11
|
+
data=go.Table(header=dict(values=headers), cells=dict(values=data))
|
|
12
|
+
)
|
|
13
|
+
return table
|
dashi/cli.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import plotly.io as pio
|
|
3
|
+
|
|
4
|
+
from .structure.cleaner import clean_builds
|
|
5
|
+
from .datasources import Datasources
|
|
6
|
+
from .dashboard import Dashboard
|
|
7
|
+
from .render import render_dashboard
|
|
8
|
+
from .serve import serve as serve_dashboard
|
|
9
|
+
from .structure.initializer import (
|
|
10
|
+
create_dashboard_template,
|
|
11
|
+
structure_already_present,
|
|
12
|
+
create_structure,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def init() -> None:
|
|
20
|
+
"""
|
|
21
|
+
Initializes dashi folder structure.
|
|
22
|
+
If the structure is already present, it will be skipped.
|
|
23
|
+
After creating the structure, creates the dashboard template, which will be used to generate new dashboards.
|
|
24
|
+
"""
|
|
25
|
+
if not structure_already_present():
|
|
26
|
+
create_structure()
|
|
27
|
+
else:
|
|
28
|
+
print("Structure is already present, skipping creation")
|
|
29
|
+
|
|
30
|
+
create_dashboard_template()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
def build() -> None:
|
|
35
|
+
data_sources = Datasources()
|
|
36
|
+
dashboard = Dashboard(data_sources)
|
|
37
|
+
charts = [
|
|
38
|
+
{"id": f"chart_{i}", "figure": pio.to_json(chart)}
|
|
39
|
+
for i, chart in enumerate(dashboard.charts)
|
|
40
|
+
]
|
|
41
|
+
render_dashboard(dashboard, charts)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def serve() -> None:
|
|
46
|
+
"""
|
|
47
|
+
Creates a simple server to display the generated dashboards
|
|
48
|
+
"""
|
|
49
|
+
serve_dashboard()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def clean() -> None:
|
|
54
|
+
"""
|
|
55
|
+
Cleans the dashboard folder from the generated dashboards.
|
|
56
|
+
"""
|
|
57
|
+
clean_builds()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
app()
|
dashi/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .yaml_parser import parse_yaml, NoConfigFile
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from yaml import CLoader as Loader, CDumper as Dumper
|
|
7
|
+
except ImportError:
|
|
8
|
+
from yaml import Loader, Dumper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NoConfigFile(Exception):
|
|
12
|
+
def __init__(self, message: str) -> None:
|
|
13
|
+
self.message = message
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_yaml(file_path: Path, file_type: str) -> dict:
|
|
18
|
+
"""
|
|
19
|
+
Parses the yaml files in the provided file_path and returns their content as a dict
|
|
20
|
+
Args:
|
|
21
|
+
file_path: a Path object representing the folder to parse
|
|
22
|
+
file_type: the type of file parsed, which is used for the function to
|
|
23
|
+
return the sub-dictionnary with the actual content
|
|
24
|
+
Returns:
|
|
25
|
+
a dictionnary representing the content of the yaml file
|
|
26
|
+
"""
|
|
27
|
+
files: list = list(file_path.glob("*.y*ml"))
|
|
28
|
+
|
|
29
|
+
if not files:
|
|
30
|
+
raise NoConfigFile(f"No YAML config found in {file_path}")
|
|
31
|
+
|
|
32
|
+
file = files[0]
|
|
33
|
+
with open(file, "r") as f:
|
|
34
|
+
config: dict[Any, Any] | None = yaml.load(f, Loader=Loader)
|
|
35
|
+
|
|
36
|
+
if config is None:
|
|
37
|
+
raise ValueError(f"{file} is empty")
|
|
38
|
+
try:
|
|
39
|
+
return config[file_type]
|
|
40
|
+
except KeyError:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"There is a problem with the datasource file (the format is incorrect)"
|
|
43
|
+
)
|
dashi/dashboard.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from .charts.chart import BaseChart
|
|
2
|
+
from .helpers import prettify_title
|
|
3
|
+
from .transforms.transforms import apply_transforms
|
|
4
|
+
from .config.yaml_parser import parse_yaml
|
|
5
|
+
from .charts.registry import CHARTS
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from .datasources import Datasources
|
|
8
|
+
from plotly.graph_objs import Figure
|
|
9
|
+
from typing import List
|
|
10
|
+
import polars as pl
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoChartType(ValueError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Dashboard:
|
|
18
|
+
DASHBOARD_FOLDER = Path.cwd() / "dashboards"
|
|
19
|
+
|
|
20
|
+
def __init__(self, data_sources: Datasources) -> None:
|
|
21
|
+
self.dash_data: dict = self.load_dashboard()
|
|
22
|
+
self.title: str = self.dash_data["title"]
|
|
23
|
+
self.rows: int = self.dash_data["layout"].get("rows", 1)
|
|
24
|
+
self.cols: int = self.dash_data["layout"].get("columns", 1)
|
|
25
|
+
self.datasources: Datasources = data_sources
|
|
26
|
+
self.charts: List[Figure] = [
|
|
27
|
+
self.generate_chart(chart) for chart in self.dash_data["charts"]
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def load_dashboard(self) -> dict[str, str]:
|
|
31
|
+
"""Retrieve the yaml parametrization from the dashboard folder and return the parametrization as a dict.
|
|
32
|
+
Returns:
|
|
33
|
+
A dict mapping keys to a corresponding dashboard parameter, or chart.
|
|
34
|
+
Example: {"title": "sample_dashboard"}
|
|
35
|
+
"""
|
|
36
|
+
data: dict[str, str] = parse_yaml(self.DASHBOARD_FOLDER, "dashboard")
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
def generate_chart(self, chart_data: dict) -> Figure:
|
|
40
|
+
"""Create the Plotly figure based on chart data retrieved from the yaml.
|
|
41
|
+
The chart data contains chart type, data source, values for x and y. The actual data has to be fetched from the datasource, which should be kept
|
|
42
|
+
as an instance of the class Datasource.
|
|
43
|
+
Args:
|
|
44
|
+
chart_data: a dictionnary representing the chart parametrization
|
|
45
|
+
Example: {"name": "example_chart", "type": "line", "datasource": "sample_datasource", "x": "x_data", "y": "y_data"}
|
|
46
|
+
Returns:
|
|
47
|
+
A Plotly figure, with its type corresponding to the value of the "type" key in the passed chart_data dict
|
|
48
|
+
"""
|
|
49
|
+
chart_name: str = chart_data["name"]
|
|
50
|
+
chart_type: str = chart_data["type"]
|
|
51
|
+
chart_datasource: pl.DataFrame = self.datasources.find_datasource(
|
|
52
|
+
chart_data["datasource"]
|
|
53
|
+
).load_data()
|
|
54
|
+
|
|
55
|
+
chart_settings = {}
|
|
56
|
+
|
|
57
|
+
if chart_type != "table":
|
|
58
|
+
chart_transform = chart_data.get("transform", None)
|
|
59
|
+
|
|
60
|
+
if chart_transform is not None:
|
|
61
|
+
chart_datasource = apply_transforms(chart_datasource, chart_transform)
|
|
62
|
+
|
|
63
|
+
chart_settings["x"] = (
|
|
64
|
+
chart_data.get("x")
|
|
65
|
+
if chart_data.get("x") is not None
|
|
66
|
+
else chart_data.get("values")
|
|
67
|
+
)
|
|
68
|
+
chart_settings["y"] = (
|
|
69
|
+
chart_data.get("y")
|
|
70
|
+
if chart_data.get("y") is not None
|
|
71
|
+
else chart_data.get("names")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if chart_settings["x"] is None or chart_settings["y"] is None:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"The dashboard parametrization is missing x/values or y/names"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
chart_settings["options"] = chart_data.get("options", None)
|
|
80
|
+
else:
|
|
81
|
+
table_cols = chart_data.get("columns")
|
|
82
|
+
chart_settings["columns"] = table_cols
|
|
83
|
+
|
|
84
|
+
builder: BaseChart = CHARTS[chart_type]
|
|
85
|
+
|
|
86
|
+
return builder.build(
|
|
87
|
+
prettify_title(chart_name), chart_datasource, **chart_settings
|
|
88
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .base_datasource import BaseDatasource
|
|
2
|
+
from .csv_datasource import CsvDatasource
|
|
3
|
+
from .json_datasource import JsonDatasource
|
|
4
|
+
from .datasources import Datasources
|
|
5
|
+
from .postgres_datasource import PostgresDatasource
|
|
6
|
+
from .duckdb_datasource import DuckDBDatasource
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseDatasource(ABC):
|
|
7
|
+
STAGING_DATA_PATH = Path.cwd() / "staging_data"
|
|
8
|
+
|
|
9
|
+
def __init__(self, source_def: dict, *args, **kwargs) -> None:
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.name: str = source_def["name"]
|
|
12
|
+
self.data_type: str = source_def["type"]
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"Datasource {self.name}, from {self.data_type}"
|
|
16
|
+
|
|
17
|
+
def load_data(self) -> pl.DataFrame:
|
|
18
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .base_datasource import BaseDatasource
|
|
2
|
+
from .file_datasource import FileDatasource
|
|
3
|
+
import polars as pl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CsvDatasource(FileDatasource):
|
|
7
|
+
def load_data(self) -> pl.DataFrame:
|
|
8
|
+
schema: dict = self.load_schema()
|
|
9
|
+
data: pl.DataFrame = pl.read_csv(self.path, schema=schema)
|
|
10
|
+
return data
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Any
|
|
3
|
+
from dashi.config.yaml_parser import parse_yaml, NoConfigFile
|
|
4
|
+
from .registry import DATASOURCES
|
|
5
|
+
from .base_datasource import BaseDatasource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Datasources:
|
|
9
|
+
DATA_SOURCES_PATH = Path.cwd() / "data_sources"
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.sources: List[BaseDatasource] = self.load_sources()
|
|
13
|
+
|
|
14
|
+
def load_sources(self):
|
|
15
|
+
sources = []
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
data: dict[Any, Any] = parse_yaml(self.DATA_SOURCES_PATH, "datasources")
|
|
19
|
+
|
|
20
|
+
for ds_settings in data:
|
|
21
|
+
sources.append(DATASOURCES[ds_settings["type"]](ds_settings))
|
|
22
|
+
|
|
23
|
+
except NoConfigFile as e:
|
|
24
|
+
print(e.message)
|
|
25
|
+
return sources
|
|
26
|
+
|
|
27
|
+
def find_datasource(self, source_name: str) -> BaseDatasource:
|
|
28
|
+
try:
|
|
29
|
+
return [source for source in self.sources if source.name == source_name][0]
|
|
30
|
+
except IndexError:
|
|
31
|
+
raise IndexError(
|
|
32
|
+
"The name of the datasource in the dashboard configuration doesn't correspond to any existing datasource"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
datasoures = Datasources()
|
|
38
|
+
|
|
39
|
+
for source in datasoures.sources:
|
|
40
|
+
print(source.data)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from .sql_datasource import SqlDatasource
|
|
3
|
+
import duckdb
|
|
4
|
+
import polars as pl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DuckDBDatasource(SqlDatasource):
|
|
8
|
+
def __init__(self, source_def: dict) -> None:
|
|
9
|
+
super().__init__(source_def)
|
|
10
|
+
self.path: Path = self.STAGING_DATA_PATH / f"{source_def['name']}.duckdb"
|
|
11
|
+
|
|
12
|
+
def load_data(self) -> pl.DataFrame:
|
|
13
|
+
con = duckdb.connect(database=self.path)
|
|
14
|
+
df: pl.DataFrame = con.sql(self.query).pl()
|
|
15
|
+
return df
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
from .base_datasource import BaseDatasource
|
|
4
|
+
from polars.datatypes import String, Float32, Int32, Date, Unknown
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileDatasource(BaseDatasource):
|
|
8
|
+
STAGING_DATA_PATH = Path.cwd() / "staging_data"
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
source_def: dict,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(source_def)
|
|
15
|
+
self.columns: List[dict] = source_def["columns"]
|
|
16
|
+
if source_def.get("path") is None:
|
|
17
|
+
self.path: Path = (
|
|
18
|
+
self.STAGING_DATA_PATH / f"{source_def['name']}.{source_def['type']}"
|
|
19
|
+
)
|
|
20
|
+
else:
|
|
21
|
+
self.path = source_def["path"]
|
|
22
|
+
|
|
23
|
+
def load_schema(self) -> dict:
|
|
24
|
+
schema: dict = {
|
|
25
|
+
col["name"]: self.convert_type_to_pl_datatype(col["type"])
|
|
26
|
+
for col in self.columns
|
|
27
|
+
}
|
|
28
|
+
return schema
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def convert_type_to_pl_datatype(
|
|
32
|
+
datatype: str,
|
|
33
|
+
) -> dict[
|
|
34
|
+
str, Union[type[String], type[Int32], type[Float32], type[Date], type[Unknown]]
|
|
35
|
+
]:
|
|
36
|
+
datatypes_dict = {
|
|
37
|
+
"string": String,
|
|
38
|
+
"integer": Int32,
|
|
39
|
+
"int32": Int32,
|
|
40
|
+
"float32": Float32,
|
|
41
|
+
"float": Float32,
|
|
42
|
+
"date": Date,
|
|
43
|
+
}
|
|
44
|
+
return datatypes_dict.get(datatype, Unknown)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .base_datasource import BaseDatasource
|
|
2
|
+
from .file_datasource import FileDatasource
|
|
3
|
+
import polars as pl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JsonDatasource(FileDatasource):
|
|
7
|
+
def load_data(self) -> pl.DataFrame:
|
|
8
|
+
schema: dict = self.load_schema()
|
|
9
|
+
return pl.read_json(self.path, schema=schema)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LoginMixin(ABC):
|
|
5
|
+
def build_connection(self, source_def: dict) -> dict[str, str]:
|
|
6
|
+
host = source_def.get("host")
|
|
7
|
+
port = source_def.get("port")
|
|
8
|
+
username = source_def.get("username")
|
|
9
|
+
password = source_def.get("password")
|
|
10
|
+
dbname = source_def.get("dbname")
|
|
11
|
+
connection_info: dict = {
|
|
12
|
+
"dbname": dbname,
|
|
13
|
+
"host": host,
|
|
14
|
+
"port": port,
|
|
15
|
+
"user": username,
|
|
16
|
+
"password": password,
|
|
17
|
+
}
|
|
18
|
+
return connection_info
|
|
19
|
+
|
|
20
|
+
def login(self, connection_info: dict):
|
|
21
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from .login_mixin import LoginMixin
|
|
3
|
+
from .sql_datasource import SqlDatasource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PostgresDatasource(SqlDatasource, LoginMixin):
|
|
7
|
+
DATATYPES: dict[str, type] = {
|
|
8
|
+
"bool": bool,
|
|
9
|
+
"varchar": str,
|
|
10
|
+
"text": str,
|
|
11
|
+
"float4": float,
|
|
12
|
+
"float8": float,
|
|
13
|
+
"money": float,
|
|
14
|
+
"date": str,
|
|
15
|
+
"time": str,
|
|
16
|
+
"timestamp": str,
|
|
17
|
+
"numeric": float,
|
|
18
|
+
"char": str,
|
|
19
|
+
"int8": int,
|
|
20
|
+
"int4": int,
|
|
21
|
+
"int2": int,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __init__(self, source_def: dict) -> None:
|
|
25
|
+
super().__init__(source_def)
|
|
26
|
+
self.connection_info = self.build_connection(source_def)
|
|
27
|
+
self.conn = self.login(self.connection_info)
|
|
28
|
+
self._datatypes = self._load_datatypes()
|
|
29
|
+
|
|
30
|
+
def login(self, connection_info: dict):
|
|
31
|
+
try:
|
|
32
|
+
import psycopg2
|
|
33
|
+
except ImportError:
|
|
34
|
+
raise ImportError(
|
|
35
|
+
"psycopg2 is required for PostgreSQL support. "
|
|
36
|
+
'Install it with: pip install "dashi[postgres]'
|
|
37
|
+
)
|
|
38
|
+
try:
|
|
39
|
+
return psycopg2.connect(**connection_info)
|
|
40
|
+
except psycopg2.DatabaseError as err:
|
|
41
|
+
raise err
|
|
42
|
+
|
|
43
|
+
def load_data(self) -> pl.DataFrame:
|
|
44
|
+
with self.conn.cursor() as curs:
|
|
45
|
+
curs.execute(self.query)
|
|
46
|
+
res = curs.fetchall()
|
|
47
|
+
meta = {info[0]: self._datatypes.get(info[1]) for info in curs.description}
|
|
48
|
+
self.conn.close()
|
|
49
|
+
df = pl.DataFrame(data=res, schema=meta, orient="row")
|
|
50
|
+
return df
|
|
51
|
+
|
|
52
|
+
def _load_datatypes(self) -> dict:
|
|
53
|
+
with self.conn.cursor() as curs:
|
|
54
|
+
curs.execute("select oid,typname from pg_type")
|
|
55
|
+
res = curs.fetchall()
|
|
56
|
+
return {dtype[0]: self._pg_datatype_to_python(dtype[1]) for dtype in res}
|
|
57
|
+
|
|
58
|
+
def _pg_datatype_to_python(self, pg_datatype: str):
|
|
59
|
+
return self.DATATYPES.get(pg_datatype)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .postgres_datasource import PostgresDatasource
|
|
2
|
+
from .csv_datasource import CsvDatasource
|
|
3
|
+
from .json_datasource import JsonDatasource
|
|
4
|
+
from .sql_datasource import SqlDatasource
|
|
5
|
+
from .duckdb_datasource import DuckDBDatasource
|
|
6
|
+
|
|
7
|
+
DATASOURCES = {
|
|
8
|
+
"csv": CsvDatasource,
|
|
9
|
+
"json": JsonDatasource,
|
|
10
|
+
"duckdb": DuckDBDatasource,
|
|
11
|
+
"postgres": PostgresDatasource,
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from .base_datasource import BaseDatasource
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SqlDatasource(BaseDatasource, ABC):
|
|
7
|
+
def __init__(self, source_def: dict, *args, **kwargs) -> None:
|
|
8
|
+
super().__init__(source_def, *args, **kwargs)
|
|
9
|
+
self.query: str = source_def["query"]
|
|
10
|
+
|
|
11
|
+
def load_data(self) -> pl.DataFrame:
|
|
12
|
+
raise NotImplementedError
|
dashi/helpers.py
ADDED
dashi/render.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from jinja2 import Environment, FileSystemLoader
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render_dashboard(dashboard, charts):
|
|
5
|
+
env = Environment(loader=FileSystemLoader("templates"))
|
|
6
|
+
template = env.get_template("dashboard_template.html")
|
|
7
|
+
|
|
8
|
+
html = template.render(
|
|
9
|
+
title=dashboard.title, rows=dashboard.rows, cols=dashboard.cols, charts=charts
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
with open(f"builds/{dashboard.title}.html", "w") as f:
|
|
13
|
+
f.write(html)
|
dashi/serve.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import http.server
|
|
2
|
+
import socketserver
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
PORT = 8000
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def serve() -> None:
|
|
10
|
+
handler = http.server.SimpleHTTPRequestHandler
|
|
11
|
+
|
|
12
|
+
os.chdir(Path.cwd() / "builds")
|
|
13
|
+
|
|
14
|
+
with socketserver.TCPServer(("", PORT), handler) as httpd:
|
|
15
|
+
print(f"Serving on http://localhost:{PORT}")
|
|
16
|
+
httpd.serve_forever()
|
|
File without changes
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
DASHI_FOLDERS = ["data_sources", "staging_data", "dashboards", "templates"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def structure_already_present() -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Checks if any of the DASHI_FOLDERS is alread present in the current project structure
|
|
9
|
+
Return:
|
|
10
|
+
A boolean value indicating if any folder from the DASHI_FOLDERS is found in the project structure
|
|
11
|
+
"""
|
|
12
|
+
if any([Path.exists(Path.cwd() / folder) for folder in DASHI_FOLDERS]):
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_structure() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Creates the wanted folder structure for dashi to work.
|
|
20
|
+
To do this, the function creates folders in the constant DASHI_FOLDERS
|
|
21
|
+
"""
|
|
22
|
+
root = Path.cwd()
|
|
23
|
+
for folder in DASHI_FOLDERS:
|
|
24
|
+
new_folder = Path(root / folder)
|
|
25
|
+
new_folder.mkdir()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_dashboard_template() -> None:
|
|
29
|
+
"""
|
|
30
|
+
Creates the dashboard template which will be used to generate new dashboards
|
|
31
|
+
"""
|
|
32
|
+
dashboard_template_file = Path().cwd() / "templates" / "dashboard_template.html"
|
|
33
|
+
|
|
34
|
+
with open(dashboard_template_file, "w") as f:
|
|
35
|
+
f.write(
|
|
36
|
+
"""
|
|
37
|
+
<!DOCTYPE html>
|
|
38
|
+
<html>
|
|
39
|
+
<head>
|
|
40
|
+
<title>{{ title }}</title>
|
|
41
|
+
|
|
42
|
+
<script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script>
|
|
43
|
+
<link rel="stylesheet" href="static/style.css"/>
|
|
44
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
45
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
46
|
+
<link href="https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet">
|
|
47
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
48
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
49
|
+
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
|
50
|
+
</head>
|
|
51
|
+
|
|
52
|
+
<body>
|
|
53
|
+
|
|
54
|
+
<header>
|
|
55
|
+
<h1>Dashi</h1>
|
|
56
|
+
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<main>
|
|
60
|
+
<h2>{{ title }}</h2>
|
|
61
|
+
<div class="grid" style="grid-template-columns: repeat({{cols}}, 1fr);">
|
|
62
|
+
|
|
63
|
+
{% for chart in charts %}
|
|
64
|
+
<div id="{{ chart.id }}" class="chart"></div>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
</main>
|
|
69
|
+
<script>
|
|
70
|
+
{% for chart in charts %}
|
|
71
|
+
|
|
72
|
+
const fig_{{ loop.index }} = {{ chart.figure | safe }};
|
|
73
|
+
|
|
74
|
+
Plotly.newPlot(
|
|
75
|
+
"{{ chart.id }}",
|
|
76
|
+
fig_{{ loop.index }}.data,
|
|
77
|
+
fig_{{ loop.index }}.layout
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
{% endfor %}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
85
|
+
"""
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_stylesheet() -> None:
|
|
90
|
+
"""
|
|
91
|
+
Creates the css stylesheet used for the dashboards
|
|
92
|
+
"""
|
|
93
|
+
staticfile = Path().cwd() / "builds" / "static" / "style.css"
|
|
94
|
+
|
|
95
|
+
with open(staticfile, "w") as f:
|
|
96
|
+
f.write(
|
|
97
|
+
"""
|
|
98
|
+
body, html {
|
|
99
|
+
margin: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
display: block;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.grid {
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: 1fr 1fr;
|
|
107
|
+
gap: 20px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.chart {
|
|
111
|
+
border: 1px solid #eee;
|
|
112
|
+
padding: 10px;
|
|
113
|
+
display: flex;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
header {
|
|
118
|
+
background-color: black;
|
|
119
|
+
height: 4rem;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items:center;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
h1 {
|
|
125
|
+
font-family: "Silkscreen", sans-serif;
|
|
126
|
+
color: white;
|
|
127
|
+
margin: 2rem;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
h2 {
|
|
132
|
+
font-family: "Open Sans", sans-serif;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main {
|
|
136
|
+
padding: 0.5rem 2rem;
|
|
137
|
+
}
|
|
138
|
+
"""
|
|
139
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def apply_transforms(dataframe: pl.DataFrame, transforms: dict) -> pl.DataFrame:
|
|
5
|
+
"""
|
|
6
|
+
Takes a Dataframe and applies transformations if needed.
|
|
7
|
+
Args:
|
|
8
|
+
dataframe: A polars dataframe
|
|
9
|
+
transforms: A list of transformations to apply (group_by, sum, count ...)
|
|
10
|
+
Returns:
|
|
11
|
+
A polars dataframe updated with the applied transforms
|
|
12
|
+
"""
|
|
13
|
+
groupby = transforms.get("groupby")
|
|
14
|
+
metrics = transforms.get("metrics")
|
|
15
|
+
|
|
16
|
+
if not groupby:
|
|
17
|
+
return dataframe
|
|
18
|
+
|
|
19
|
+
aggs = []
|
|
20
|
+
|
|
21
|
+
for col, op in metrics.items():
|
|
22
|
+
if op == "sum":
|
|
23
|
+
aggs.append(pl.col(col).sum())
|
|
24
|
+
if op == "count":
|
|
25
|
+
aggs.append(pl.col(col).count())
|
|
26
|
+
if op == "average":
|
|
27
|
+
aggs.append(pl.col(col).mean())
|
|
28
|
+
if op == "median":
|
|
29
|
+
aggs.append(pl.col(col).median())
|
|
30
|
+
|
|
31
|
+
return dataframe.group_by(groupby).agg(aggs)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dashiyaml
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Dashboard as code tool - turn YAML configs into standalone HTML dashboards
|
|
5
|
+
Author: Corentin Dupriez
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Corentin-dupriez/dashi
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Corentin-dupriez/dashi/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: polars
|
|
25
|
+
Requires-Dist: plotly
|
|
26
|
+
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: pyyaml
|
|
28
|
+
Requires-Dist: jinja2
|
|
29
|
+
Requires-Dist: typer
|
|
30
|
+
Requires-Dist: duckdb
|
|
31
|
+
Provides-Extra: postgres
|
|
32
|
+
Requires-Dist: psycopg2-binary; extra == "postgres"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# dashi
|
|
36
|
+
|
|
37
|
+
dashi is a dashboard-as-code application that allows you to create dashboards
|
|
38
|
+
from yaml files. It used Polars for fast and efficient dataframe processing,
|
|
39
|
+
and Plotly for visualisation.
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
⚠️ dashi is currently in early development.
|
|
44
|
+
The API and configuration format may change.
|
|
45
|
+
|
|
46
|
+
## Philosophy
|
|
47
|
+
|
|
48
|
+
dashi follows a **dashboard-as-code** approach.
|
|
49
|
+
|
|
50
|
+
Instead of building dashboards through a GUI, dashboards are defined
|
|
51
|
+
as YAML files that can be versioned, reviewed, and deployed like code.
|
|
52
|
+
|
|
53
|
+
This allows dashboards to be:
|
|
54
|
+
|
|
55
|
+
- version controlled
|
|
56
|
+
- reproducible
|
|
57
|
+
- easy to review in pull requests
|
|
58
|
+
|
|
59
|
+
## How to use
|
|
60
|
+
|
|
61
|
+
### Install Dashi
|
|
62
|
+
|
|
63
|
+
First, make sure Python 3.9 or above is installed:
|
|
64
|
+
`python --version`
|
|
65
|
+
|
|
66
|
+
Install dashi from Pypi:
|
|
67
|
+
`pip install dashiyaml`
|
|
68
|
+
|
|
69
|
+
For PostgreSQL datasource support, install the optional extra:
|
|
70
|
+
`pip install dashi[postgres]`
|
|
71
|
+
|
|
72
|
+
### Initialize folder structure
|
|
73
|
+
|
|
74
|
+
After having cloned the repo and installed dependencies, you can initiate the
|
|
75
|
+
folder structure with
|
|
76
|
+
|
|
77
|
+
`dashi init`
|
|
78
|
+
|
|
79
|
+
This will create the following structure (if it doesn't already exist)
|
|
80
|
+
|
|
81
|
+
```txt
|
|
82
|
+
+ data_sources
|
|
83
|
+
+ staging_data
|
|
84
|
+
+ templates
|
|
85
|
+
+ dashboard_template.html
|
|
86
|
+
+ builds
|
|
87
|
+
+ static
|
|
88
|
+
+ style.css
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Configuring data sources
|
|
92
|
+
|
|
93
|
+
The first step of creating visualisations is done by defining data sources.
|
|
94
|
+
This should be done in the data_sources folder as a yaml file. This file
|
|
95
|
+
should contain the parametrization listed below.
|
|
96
|
+
|
|
97
|
+
#### Csv and JSON datasources
|
|
98
|
+
|
|
99
|
+
Csv and JSON
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
datasources:
|
|
103
|
+
- name: sample_data
|
|
104
|
+
type: csv
|
|
105
|
+
columns:
|
|
106
|
+
- name: id
|
|
107
|
+
type: integer
|
|
108
|
+
- name: name
|
|
109
|
+
type: string
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For the moment, only the csv, json, duckdb and PostgreSQL datasource types are supported,
|
|
113
|
+
but more formats will be supported soon.
|
|
114
|
+
The datasource name must match the filename (without the extension)
|
|
115
|
+
of the data file stored in the `staging_data` folder.
|
|
116
|
+
When using the `dashi build` command, dashi
|
|
117
|
+
will then look within the staging_data folder, and generate a polars dataframe
|
|
118
|
+
by using the defined schema.
|
|
119
|
+
|
|
120
|
+
### Configuring the dashboards
|
|
121
|
+
|
|
122
|
+
Once the datasources are defined, the dashboard itself can be defined within the
|
|
123
|
+
`dashboards` folder as a yaml file. Each yaml file should correspond to
|
|
124
|
+
one dashboard, but each dashboard is composed of several charts.
|
|
125
|
+
The file should contain the dashboard name, as well as a list of charts.
|
|
126
|
+
Each chart should be defined with:
|
|
127
|
+
|
|
128
|
+
- title
|
|
129
|
+
- chart type (ex: line, bar)
|
|
130
|
+
- datasource (corresponding to a datasource defined in `data_sources`)
|
|
131
|
+
- x (corresponding to a column defined for the datasource)
|
|
132
|
+
- y (corresponding to a column defined for the datasource)
|
|
133
|
+
- options: allow a pass through of plotly options to the widget. For example:
|
|
134
|
+
- theme
|
|
135
|
+
- title
|
|
136
|
+
- font
|
|
137
|
+
- transforms: contains instructions on transformation of the data before plotting.
|
|
138
|
+
This includes:
|
|
139
|
+
- group by
|
|
140
|
+
- sum
|
|
141
|
+
- average
|
|
142
|
+
- count
|
|
143
|
+
|
|
144
|
+
All the options that can be passed can be seen [on the Plotly documentation](https://plotly.com/python/reference/layout/)
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
dashboard:
|
|
148
|
+
title: sample_dashboard
|
|
149
|
+
|
|
150
|
+
charts:
|
|
151
|
+
- name: sample_chart
|
|
152
|
+
type: bar
|
|
153
|
+
datasource: sample_data
|
|
154
|
+
x: channel
|
|
155
|
+
y: orders
|
|
156
|
+
options:
|
|
157
|
+
title: Orders by channel
|
|
158
|
+
template: plotly_white
|
|
159
|
+
transform:
|
|
160
|
+
groupby: channel
|
|
161
|
+
metrics:
|
|
162
|
+
orders: sum
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### dashi build
|
|
167
|
+
|
|
168
|
+
Once both datasources and dashboards have been defined, the `dashi build`
|
|
169
|
+
command can be used to create the dashboards based on the provided definitions.
|
|
170
|
+
|
|
171
|
+
This command will create html files under the `builds` folder.
|
|
172
|
+
|
|
173
|
+
WARNING: `dashi build` will override previous version of the dashboards
|
|
174
|
+
you have already exported
|
|
175
|
+
|
|
176
|
+
### dashi serve
|
|
177
|
+
|
|
178
|
+
The command `dashi serve` will create a simple http server on the port 8000
|
|
179
|
+
by default. The created dashboards can be accessed from there.
|
|
180
|
+
|
|
181
|
+
### dashi clean
|
|
182
|
+
|
|
183
|
+
If you wish to clean up the created dashboards, you can use the `dashi clean` command.
|
|
184
|
+
It will delete every html file within the `builds` folder
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
dashi/__init__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
|
2
|
+
dashi/cli.py,sha256=rOczR1z5caUCq7zj0MF2wdVQJ0PZR4E_qTnXyvIhZCI,1417
|
|
3
|
+
dashi/dashboard.py,sha256=-BbKnQ4af3TPUcYGfQPLqdIV8GrYJaOO9rteEyTQY4A,3474
|
|
4
|
+
dashi/helpers.py,sha256=JWfi35JuoC9OHJcpV0SICDF87zCNotvr58iocP7KWUg,107
|
|
5
|
+
dashi/render.py,sha256=pbBEBQiIiNgeA61Nq3ovlPj9D4K_1fnz6glwoXF2mGk,415
|
|
6
|
+
dashi/serve.py,sha256=gNrrwcVotfczTY9m6VJ0d133Z_bV_9oWVTdNlDr0UEo,345
|
|
7
|
+
dashi/charts/__init__.py,sha256=CFCw7C80JXcO9Z5SRZxsnLunvnj_sRMKCUdcjH4NI4Y,172
|
|
8
|
+
dashi/charts/bar.py,sha256=EE87oVtjCsobdlgIienzwpLEFDNkTvJr1riQBQbhVkI,432
|
|
9
|
+
dashi/charts/chart.py,sha256=tjVJVOByKEX9jblNlttL6qFrB3lHjbrzXfGYE9-PLrc,538
|
|
10
|
+
dashi/charts/line.py,sha256=X3fzHByS5ik-rXZGpKQw2XNohqCgzubU3DP-zxXooWg,423
|
|
11
|
+
dashi/charts/pie.py,sha256=lvgZm-XECNLU7ZdbdZOZGpAJukVnmlBruoFKUwUPlxQ,443
|
|
12
|
+
dashi/charts/registry.py,sha256=kzuD3OwC_Zgemu84u1LD7qxGYNsyraflHGTGxLE0z-o,277
|
|
13
|
+
dashi/charts/scatter.py,sha256=6h9ebcEOzLkloNfenObZqP10EXiD6Wwp4NBbj-eRSKc,430
|
|
14
|
+
dashi/charts/table.py,sha256=n6f6LHvMOzmQuP98JO3PDuvk9TiXAzccfNPs2tTNUc4,469
|
|
15
|
+
dashi/config/__init__.py,sha256=oCbeEGPmY9IO8lEaSQzxbZNG2VVC-aUJPtCBDBRSLOQ,50
|
|
16
|
+
dashi/config/yaml_parser.py,sha256=oJsTfPxQBSZ6sFfYokQRuN8ObBjy1b5a5HhjRnue4GQ,1281
|
|
17
|
+
dashi/datasources/__init__.py,sha256=lgKBhcw2gMLnqZB_txdDn-hFC3sLQu7cQKyzvZQBdus,267
|
|
18
|
+
dashi/datasources/base_datasource.py,sha256=b-4SatW_Fc5heKucy-mNV_r3G2LXMMM5dgDCICr2jEg,521
|
|
19
|
+
dashi/datasources/csv_datasource.py,sha256=2CFgRzaMK_pcUaEyN5K7DkV7A8UfBr_Tiw107J76gPM,317
|
|
20
|
+
dashi/datasources/datasources.py,sha256=StGvkY-ZsunzlUmPKv74e7TFfiUWAkkWTxle2a9TBL4,1205
|
|
21
|
+
dashi/datasources/duckdb_datasource.py,sha256=9HL7dXMSlYu6-8P1AJ9HtUbO2kDgqT7CiwuFYUPfQCg,472
|
|
22
|
+
dashi/datasources/file_datasource.py,sha256=_UXtEqZpGk751DXUHgM6oL-Chus6oc09VfdqbeXfzRw,1314
|
|
23
|
+
dashi/datasources/json_datasource.py,sha256=81888IrRiu2wzC7Ta-_NR8ISP_MbHPWvjmVBy_z6Z2Q,285
|
|
24
|
+
dashi/datasources/login_mixin.py,sha256=ydebahhde_2NJGgM_DGjOwzmor6iQq5_U-q1wp0734o,623
|
|
25
|
+
dashi/datasources/postgres_datasource.py,sha256=opwey-NK3FUMFo5vwxUB5KGmhwug1IxK-NzbXW0US4E,1905
|
|
26
|
+
dashi/datasources/registry.py,sha256=l2QbM0SiT2FaLP6siEaT3gTnEQJUaLpPTI0pIo_hS6U,369
|
|
27
|
+
dashi/datasources/sql_datasource.py,sha256=CgOzHtjUruH5TfIkJfRIwcqQYDhaC2Tnt_ALS5EPGqI,371
|
|
28
|
+
dashi/structure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
dashi/structure/cleaner.py,sha256=nSTPgj7Dw106ZwEFFRIhP0bqF875lnQ9rDYvFKrdq1E,233
|
|
30
|
+
dashi/structure/initializer.py,sha256=tHVLBISoTL4tlkoJSbNQazRQIh1P7QY8evLVuCJc82I,3307
|
|
31
|
+
dashi/transforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
dashi/transforms/transforms.py,sha256=6Xq2wQ4UNoTxzSkwpy-QnI-XLsGpTuPTTM8kEQPcWf4,897
|
|
33
|
+
dashiyaml-0.0.2.dist-info/licenses/LICENSE,sha256=4-rUbcwMRML61WaPkVTFs26HWOM6aOUUYOof19KK-D8,1073
|
|
34
|
+
dashiyaml-0.0.2.dist-info/METADATA,sha256=9E2RGnWWrJKW5W66-S8FuNF_xnhugIJ6Js5sAVM_-78,5187
|
|
35
|
+
dashiyaml-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
36
|
+
dashiyaml-0.0.2.dist-info/entry_points.txt,sha256=MCZLxxuccCTRcOgxsuAx9N1iVNOu1So7Pd6QBYx4x9Y,40
|
|
37
|
+
dashiyaml-0.0.2.dist-info/top_level.txt,sha256=fZou1xoOz4q69Gsx9SoVHLxsq7Ky3P90g9_geulqLPE,6
|
|
38
|
+
dashiyaml-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corentin Dupriez
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dashi
|