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 ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,6 @@
1
+ from .bar import BarChart
2
+ from .line import LineChart
3
+ from .pie import PieChart
4
+ from .chart import BaseChart
5
+ from .scatter import ScatterChart
6
+ from .registry import CHARTS
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
@@ -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
+ }
@@ -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()
@@ -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
@@ -0,0 +1,2 @@
1
+ def prettify_title(title: str) -> str:
2
+ return " ".join(word.capitalize() for word in title.split("_"))
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,10 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def clean_builds() -> None:
5
+ """
6
+ Deletes the existing files in the builds directory.
7
+ """
8
+ builds_path = Path.cwd() / "builds"
9
+ for file in builds_path.glob("*.html"):
10
+ file.unlink()
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dashi = dashi.cli:app
@@ -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