Py.Cafe

antonymilne/

vizro-python-to-yaml

Dashboard to YAML

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
######################################################################
# Steps to generate YAML configuration from Python dashboard:
# 1. Insert your dashboard configuration in block below.
# 2. Click "Click me" and follow instructions there.
######################################################################
import vizro.actions as va
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.tables import dash_ag_grid

tips = px.data.tips()

page = vm.Page(
    title="Cross-filter from table to graph",
    components=[
        vm.AgGrid(
            title="Click on a row to use that row's sex to filter graph",
            figure=dash_ag_grid(tips),
            actions=va.set_control(control="sex_filter", value="sex"),
        ),
        vm.Graph(id="tips_graph", figure=px.histogram(tips, x="tip")),  
    ],
    controls=[vm.Filter(id="sex_filter", column="sex", targets=["tips_graph"])],  
)

dashboard = vm.Dashboard(pages=[page])
# DON'T PUT Vizro().build(dashboard).run() here or the app won't work.
# The last line above should just define dashboardvariable.
######################################################################

import pyaml
import json
import vizro.tables as vt
import vizro.tables as vf
import vizro.plotly.express as px

# All this bit written by ChatGPT but seems to work well...
def serialize(obj):
    if isinstance(obj, vm.types.CapturedCallable):
        # return a dict representation
        name = obj._function.__name__
        if not hasattr(px, name) and not hasattr(vt, name) and not hasattr(vf, name):
            # Hack that assumes captured callable is written in the app.py file if it's not one of the 
            # built in functions.
            name = f"__main__.{name}"
        arguments = dict(obj._arguments)
        if "data_frame" in arguments:
            arguments["data_frame"] = "<REPLACE ME>"
        return {"_target_": name, **arguments}
    raise TypeError(f"Type {type(obj)} not serializable")

def insert_type(base, override):
    if isinstance(base, dict) and isinstance(override, dict):
        # First, recurse into all keys in base
        result = {}
        for k, v in base.items():
            result[k] = insert_type(v, override.get(k, {}))
        # Then, insert "type" from override if present at this level
        if "type" in override:
            result["type"] = override["type"]
        return result
    elif isinstance(base, list) and isinstance(override, list):
        # recurse element-wise for lists (assumes matching lengths)
        return [insert_type(b, o) for b, o in zip(base, override)]
    else:
        return base  # leave scalars / non-dict / non-list as-is

def dashboard_to_yaml(dashboard):
    # We want the dumped version to exclude unset fields but include `type`. There doesn't seem to be an easy way to do this 
    # with pydantic (maybe possible using Field-level include though, not sure?). So we just manually generate both and then 
    # take the type key out of the second one and insert into the first.
    # UPDATE: in recent pydantic there's exclude_if as a field which would probably do this for us more cleanly.
    dict1 = dashboard.model_dump(exclude_unset=True)
    dict2 = dashboard.model_dump()
    result = insert_type(dict1, dict2)

    # If we built this into Vizro then this serialization should be built into CapturedCallable itself rather than needing to 
    # use it in json.dumps.
    json_str = json.dumps(result, default=serialize)

    # Overall we do dashboard model -> dict -> json string -> dict -> yaml string. 
    # If we built serialization into model_dump itself then this could just be done as dashboard model -> dict -> yaml string.
    # There are other ways to directly do dashboard model -> yaml string (e.g. pydantic-yaml, yaml.add_representer) but probably not worth 
    # pursuing - we should instead keep JSON/dict as our "source of truth" rather than using specific techniques.
    # Format using pretty yaml seems more similar to our preferred using prettier.
    return pyaml.dump(json.loads(json_str), vspacing=dict(split_lines=1000))

requirements = """vizro
pyyaml
"""

app_py = """######################################################################
# Steps to check your YAML configuration from Python dashboard:
# 1. In dashboard.yaml, replace <REPLACE ME> with references to data sources.
# 2. In app.py, make sure data sources are registered in data manager.
# 3. In app.py, define any custom charts/figures/tables/actions.
# 4. Make sure dashboard works as expected.
######################################################################
from pathlib import Path

import vizro.plotly.express as px
import yaml
from vizro import Vizro
from vizro.managers import data_manager
from vizro.models import Dashboard
from vizro.models.types import capture

data_manager["gapminder"] = px.data.gapminder()
data_manager["tips"] = px.data.tips()
data_manager["iris"] = px.data.iris()

dashboard = yaml.safe_load(Path("dashboard.yaml").read_text(encoding="utf-8"))
dashboard = Dashboard(**dashboard)

Vizro().build(dashboard).run()
"""

import base64
import gzip
import io
import json
from urllib.parse import quote, urlencode

PYCAFE_URL = "https://py.cafe"

def create_pycafe_url(dashboard_yaml: str) -> str:
    """Create a PyCafe URL for a given Python code."""
    # Create JSON object for py.cafe
    json_object = {
        "code": app_py,
        "requirements": requirements,
        "files": [{"name": "dashboard.yaml", "content": dashboard_yaml}],
    }

    # Convert to compressed base64 URL
    json_text = json.dumps(json_object)
    compressed_json_text = gzip.compress(json_text.encode("utf8"))
    base64_text = base64.b64encode(compressed_json_text).decode("utf8")
    query = urlencode({"c": base64_text}, quote_via=quote)
    pycafe_url = f"{PYCAFE_URL}/snippet/vizro/v1?{query}"

    return pycafe_url

import vizro.models as vm
from vizro import Vizro
Vizro._reset()

page = vm.Page(
    title="YAML dashboard",
    components=[
        vm.Card(
            href=create_pycafe_url(dashboard_to_yaml(dashboard)), 
            text="Click me to generate and check your YAML configuration")
    ],
)
dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()