vizro-mcp 0.0.1.dev0__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.
vizro_mcp/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ import logging
2
+ import sys
3
+
4
+ from .server import mcp
5
+
6
+ __version__ = "0.0.1.dev0"
7
+
8
+
9
+ def main():
10
+ """Run the Vizro MCP server - makes charts and dashboards available to AI assistants."""
11
+ # Configure logging to show warnings by default
12
+ logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
13
+
14
+ # Run the MCP server
15
+ mcp.run()
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,29 @@
1
+ from .schemas import (
2
+ MODEL_GROUPS,
3
+ AgGridEnhanced,
4
+ ChartPlan,
5
+ ContainerSimplified,
6
+ DashboardSimplified,
7
+ FilterSimplified,
8
+ GraphEnhanced,
9
+ PageSimplified,
10
+ ParameterSimplified,
11
+ TabsSimplified,
12
+ get_overview_vizro_models,
13
+ get_simple_dashboard_config,
14
+ )
15
+
16
+ __all__ = [
17
+ "MODEL_GROUPS",
18
+ "AgGridEnhanced",
19
+ "ChartPlan",
20
+ "ContainerSimplified",
21
+ "DashboardSimplified",
22
+ "FilterSimplified",
23
+ "GraphEnhanced",
24
+ "PageSimplified",
25
+ "ParameterSimplified",
26
+ "TabsSimplified",
27
+ "get_overview_vizro_models",
28
+ "get_simple_dashboard_config",
29
+ ]
@@ -0,0 +1,297 @@
1
+ """Schema defining pydantic models for usage in the MCP server."""
2
+
3
+ from typing import Annotated, Any, Literal, Optional
4
+
5
+ import vizro.models as vm
6
+ from pydantic import AfterValidator, BaseModel, Field, PrivateAttr, conlist
7
+
8
+ from vizro_mcp._utils import SAMPLE_DASHBOARD_CONFIG, DFMetaData
9
+
10
+ # Constants used in chart validation
11
+ CUSTOM_CHART_NAME = "custom_chart"
12
+ ADDITIONAL_IMPORTS = [
13
+ "import vizro.plotly.express as px",
14
+ "import plotly.graph_objects as go",
15
+ "import pandas as pd",
16
+ "import numpy as np",
17
+ "from vizro.models.types import capture",
18
+ ]
19
+
20
+ # These types are used to simplify the schema for the LLM.
21
+ SimplifiedComponentType = Literal["Card", "Button", "Text", "Container", "Tabs", "Graph", "AgGrid"]
22
+ SimplifiedSelectorType = Literal[
23
+ "Dropdown", "RadioItems", "Checklist", "DatePicker", "Slider", "RangeSlider", "DatePicker"
24
+ ]
25
+ SimplifiedControlType = Literal["Filter", "Parameter"]
26
+ SimplifiedLayoutType = Literal["Grid", "Flex"]
27
+
28
+ # This dict is used to give the model and overview of what is available in the vizro.models namespace.
29
+ # It helps it to narrow down the choices when asking for a model.
30
+ MODEL_GROUPS: dict[str, list[type[vm.VizroBaseModel]]] = {
31
+ "main": [vm.Dashboard, vm.Page],
32
+ "components": [vm.Card, vm.Button, vm.Text, vm.Container, vm.Tabs, vm.Graph, vm.AgGrid], #'Figure', 'Table'
33
+ "layouts": [vm.Grid, vm.Flex],
34
+ "controls": [vm.Filter, vm.Parameter],
35
+ "selectors": [
36
+ vm.Dropdown,
37
+ vm.RadioItems,
38
+ vm.Checklist,
39
+ vm.DatePicker,
40
+ vm.Slider,
41
+ vm.RangeSlider,
42
+ vm.DatePicker,
43
+ ],
44
+ "navigation": [vm.Navigation, vm.NavBar, vm.NavLink],
45
+ }
46
+
47
+
48
+ # These simplified page, container, tabs and dashboard models are used to return a flatter schema to the LLM in order to
49
+ # reduce the context size. Especially the dashboard model schema is huge as it contains all other models.
50
+
51
+
52
+ class FilterSimplified(vm.Filter):
53
+ """Simplified Filter model for reduced schema. LLM should remember to insert actual components."""
54
+
55
+ selector: Optional[SimplifiedSelectorType] = Field(
56
+ default=None, description="Selector to be displayed. Only provide if asked for!"
57
+ )
58
+
59
+
60
+ class ParameterSimplified(vm.Parameter):
61
+ """Simplified Parameter model for reduced schema. LLM should remember to insert actual components."""
62
+
63
+ selector: SimplifiedSelectorType = Field(description="Selector to be displayed.")
64
+
65
+
66
+ class ContainerSimplified(vm.Container):
67
+ """Simplified Container model for reduced schema. LLM should remember to insert actual components."""
68
+
69
+ components: list[SimplifiedComponentType] = Field(description="List of component names to be displayed.")
70
+ layout: Optional[SimplifiedLayoutType] = Field(
71
+ default=None, description="Layout to place components in. Only provide if asked for!"
72
+ )
73
+
74
+
75
+ class TabsSimplified(vm.Tabs):
76
+ """Simplified Tabs model for reduced schema. LLM should remember to insert actual components."""
77
+
78
+ tabs: conlist(ContainerSimplified, min_length=1)
79
+
80
+
81
+ class PageSimplified(BaseModel):
82
+ """Simplified Page modes for reduced schema. LLM should remember to insert actual components."""
83
+
84
+ components: list[SimplifiedComponentType] = Field(description="List of component names to be displayed.")
85
+ title: str = Field(description="Title to be displayed.")
86
+ description: str = Field(default="", description="Description for meta tags.")
87
+ layout: Optional[SimplifiedLayoutType] = Field(
88
+ default=None, description="Layout to place components in. Only provide if asked for!"
89
+ )
90
+ controls: list[SimplifiedControlType] = Field(default=[], description="Controls to be displayed.")
91
+
92
+
93
+ class DashboardSimplified(BaseModel):
94
+ """Simplified Dashboard model for reduced schema. LLM should remember to insert actual components."""
95
+
96
+ pages: list[Literal["Page"]] = Field(description="List of page names to be included in the dashboard.")
97
+ theme: Literal["vizro_dark", "vizro_light"] = Field(
98
+ default="vizro_dark", description="Theme to be applied across dashboard. Defaults to `vizro_dark`."
99
+ )
100
+ navigation: Optional[Literal["Navigation"]] = Field(
101
+ default=None, description="Navigation component for the dashboard. Only provide if asked for!"
102
+ )
103
+ title: str = Field(default="", description="Dashboard title to appear on every page on top left-side.")
104
+
105
+
106
+ # These enhanced models are used to return a more complete schema to the LLM. Although we do not have actual schemas for
107
+ # the figure fields, we can prompt the model via the description to produce something likely correct.
108
+ class GraphEnhanced(vm.Graph):
109
+ """A Graph model that uses Plotly Express to create the figure."""
110
+
111
+ figure: dict[str, Any] = Field(
112
+ description="""
113
+ This is the plotly express figure to be displayed. Only use valid plotly express functions to create the figure.
114
+ Only use the arguments that are supported by the function you are using and where no extra modules such as statsmodels
115
+ are needed (e.g. trendline).
116
+
117
+ - Configure a dictionary as if this would be added as **kwargs to the function you are using.
118
+ - You must use the key: "_target_: "<function_name>" to specify the function you are using. Do NOT precede by
119
+ namespace (like px.line)
120
+ - you must refer to the dataframe by name, for now it is one of "gapminder", "iris", "tips".
121
+ - do not use a title if your Graph model already has a title.
122
+ """
123
+ )
124
+
125
+
126
+ class AgGridEnhanced(vm.AgGrid):
127
+ """AgGrid model that uses dash-ag-grid to create the figure."""
128
+
129
+ figure: dict[str, Any] = Field(
130
+ description="""
131
+ This is the ag-grid figure to be displayed. Only use arguments from the [`dash-ag-grid` function](https://dash.plotly.com/dash-ag-grid/reference).
132
+
133
+ The only difference to the dash version is that:
134
+ - you must use the key: "_target_: "dash_ag_grid"
135
+ - you must refer to data via "data_frame": <data_frame_name> and NOT via columnDefs and rowData (do NOT set)
136
+ """
137
+ )
138
+
139
+
140
+ ###### Chart functionality - not sure if I should include this in the MCP server
141
+ def _strip_markdown(code_string: str) -> str:
142
+ """Remove any code block wrappers (markdown or triple quotes)."""
143
+ wrappers = [("```python\n", "```"), ("```py\n", "```"), ("```\n", "```"), ('"""', '"""'), ("'''", "'''")]
144
+
145
+ for start, end in wrappers:
146
+ if code_string.startswith(start) and code_string.endswith(end):
147
+ code_string = code_string[len(start) : -len(end)]
148
+ break
149
+
150
+ return code_string.strip()
151
+
152
+
153
+ def _check_chart_code(v: str) -> str:
154
+ v = _strip_markdown(v)
155
+
156
+ # TODO: add more checks: ends with return, has return, no second function def, only one indented line
157
+ func_def = f"def {CUSTOM_CHART_NAME}("
158
+ if func_def not in v:
159
+ raise ValueError(f"The chart code must be wrapped in a function named `{CUSTOM_CHART_NAME}`")
160
+
161
+ v = v[v.index(func_def) :].strip()
162
+
163
+ first_line = v.split("\n")[0].strip()
164
+ if "data_frame" not in first_line:
165
+ raise ValueError(
166
+ """The chart code must accept a single argument `data_frame`,
167
+ and it should be the first argument of the chart."""
168
+ )
169
+ return v
170
+
171
+
172
+ class ChartPlan(BaseModel):
173
+ """Base chart plan used to generate chart code based on user visualization requirements."""
174
+
175
+ chart_type: str = Field(
176
+ description="""
177
+ Describes the chart type that best reflects the user request.
178
+ """,
179
+ )
180
+ imports: list[str] = Field(
181
+ description="""
182
+ List of import statements required to render the chart defined by the `chart_code` field. Ensure that every
183
+ import statement is a separate list/array entry: An example of valid list of import statements would be:
184
+
185
+ ["import pandas as pd",
186
+ "import plotly.express as px"]
187
+ """,
188
+ )
189
+ chart_code: Annotated[
190
+ str,
191
+ AfterValidator(_check_chart_code),
192
+ Field(
193
+ description="""
194
+ Python code that generates a generates a plotly go.Figure object. It must fulfill the following criteria:
195
+ 1. Must be wrapped in a function name
196
+ 2. Must accept a single argument `data_frame` which is a pandas DataFrame
197
+ 3. Must return a plotly go.Figure object
198
+ 4. All data used in the chart must be derived from the data_frame argument, all data manipulations
199
+ must be done within the function.
200
+ """,
201
+ ),
202
+ ]
203
+
204
+ _additional_vizro_imports: list[str] = PrivateAttr(ADDITIONAL_IMPORTS)
205
+
206
+ def get_imports(self, vizro: bool = False):
207
+ imports = list(dict.fromkeys(self.imports + self._additional_vizro_imports)) # remove duplicates
208
+ if vizro: # TODO: improve code of below
209
+ imports = [imp for imp in imports if "import plotly.express as px" not in imp]
210
+ else:
211
+ imports = [imp for imp in imports if "vizro" not in imp]
212
+ return "\n".join(imports) + "\n"
213
+
214
+ def get_chart_code(self, chart_name: Optional[str] = None, vizro: bool = False):
215
+ chart_code = self.chart_code
216
+ if vizro:
217
+ chart_code = chart_code.replace(f"def {CUSTOM_CHART_NAME}", f"@capture('graph')\ndef {CUSTOM_CHART_NAME}")
218
+ if chart_name is not None:
219
+ chart_code = chart_code.replace(f"def {CUSTOM_CHART_NAME}", f"def {chart_name}")
220
+ return chart_code
221
+
222
+ def get_dashboard_template(self, data_info: DFMetaData) -> str:
223
+ """Create a simple dashboard template for displaying the chart.
224
+
225
+ Args:
226
+ data_info: The metadata of the dataset to use.
227
+
228
+ Returns:
229
+ Complete Python code for a Vizro dashboard displaying the chart.
230
+ """
231
+ chart_code = self.get_chart_code(vizro=True)
232
+ imports = self.get_imports(vizro=True)
233
+
234
+ # Add the Vizro-specific imports if not present
235
+ additional_imports = [
236
+ "import vizro.models as vm",
237
+ "from vizro import Vizro",
238
+ "from vizro.managers import data_manager",
239
+ ]
240
+
241
+ # Combine imports without duplicates
242
+ all_imports = list(dict.fromkeys(additional_imports + imports.split("\n")))
243
+
244
+ dashboard_template = f"""
245
+ {chr(10).join(imp for imp in all_imports if imp)}
246
+
247
+ # Load the data
248
+ data_manager["{data_info.file_name}"] = {data_info.read_function_string}("{data_info.file_path_or_url}")
249
+
250
+
251
+ # Custom chart code
252
+ {chart_code}
253
+
254
+ # Create a dashboard to display the chart
255
+ dashboard = vm.Dashboard(
256
+ pages=[
257
+ vm.Page(
258
+ title="{self.chart_type.capitalize()} Chart",
259
+ components=[
260
+ vm.Graph(
261
+ id="{self.chart_type}_graph",
262
+ figure={CUSTOM_CHART_NAME}("{data_info.file_name}"),
263
+ )
264
+ ],
265
+ )
266
+ ],
267
+ title="{self.chart_type.capitalize()} Dashboard",
268
+ )
269
+
270
+ # Run the dashboard
271
+ Vizro().build(dashboard).run()
272
+ """
273
+
274
+ return dashboard_template
275
+
276
+
277
+ def get_overview_vizro_models() -> dict[str, list[dict[str, str]]]:
278
+ """Get all available models in the vizro.models namespace.
279
+
280
+ Returns:
281
+ Dictionary with categories of models and their descriptions
282
+ """
283
+ result: dict[str, list[dict[str, str]]] = {}
284
+ for category, models_list in MODEL_GROUPS.items():
285
+ result[category] = [
286
+ {
287
+ "name": model_class.__name__,
288
+ "description": (model_class.__doc__ or "No description available").split("\n")[0],
289
+ }
290
+ for model_class in models_list
291
+ ]
292
+ return result
293
+
294
+
295
+ def get_simple_dashboard_config() -> str:
296
+ """Very simple Vizro dashboard configuration. Use this config as a starter when no other config is provided."""
297
+ return SAMPLE_DASHBOARD_CONFIG
@@ -0,0 +1,33 @@
1
+ from .utils import (
2
+ GAPMINDER,
3
+ IRIS,
4
+ SAMPLE_DASHBOARD_CONFIG,
5
+ STOCKS,
6
+ TIPS,
7
+ DFInfo,
8
+ DFMetaData,
9
+ VizroCodeAndPreviewLink,
10
+ convert_github_url_to_raw,
11
+ create_pycafe_url,
12
+ get_dataframe_info,
13
+ get_python_code_and_preview_link,
14
+ load_dataframe_by_format,
15
+ path_or_url_check,
16
+ )
17
+
18
+ __all__ = [
19
+ "GAPMINDER",
20
+ "IRIS",
21
+ "SAMPLE_DASHBOARD_CONFIG",
22
+ "STOCKS",
23
+ "TIPS",
24
+ "DFInfo",
25
+ "DFMetaData",
26
+ "VizroCodeAndPreviewLink",
27
+ "convert_github_url_to_raw",
28
+ "create_pycafe_url",
29
+ "get_dataframe_info",
30
+ "get_python_code_and_preview_link",
31
+ "load_dataframe_by_format",
32
+ "path_or_url_check",
33
+ ]
@@ -0,0 +1,300 @@
1
+ """Utility functions for the Vizro MCP."""
2
+
3
+ import base64
4
+ import gzip
5
+ import io
6
+ import json
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Literal, Optional, Union
11
+ from urllib.parse import quote, urlencode
12
+
13
+ import pandas as pd
14
+ import vizro.models as vm
15
+
16
+ # PyCafe URL for Vizro snippets
17
+ PYCAFE_URL = "https://py.cafe"
18
+
19
+
20
+ @dataclass
21
+ class VizroCodeAndPreviewLink:
22
+ python_code: str
23
+ pycafe_url: str
24
+
25
+
26
+ @dataclass
27
+ class DFMetaData:
28
+ file_name: str
29
+ file_path_or_url: str
30
+ file_location_type: Literal["local", "remote"]
31
+ read_function_string: Literal["pd.read_csv", "pd.read_json", "pd.read_html", "pd.read_parquet", "pd.read_excel"]
32
+ column_names_types: Optional[dict[str, str]] = None
33
+
34
+
35
+ @dataclass
36
+ class DFInfo:
37
+ general_info: str
38
+ sample: dict[str, Any]
39
+
40
+
41
+ IRIS = DFMetaData(
42
+ file_name="iris_data",
43
+ file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/iris-id.csv",
44
+ file_location_type="remote",
45
+ read_function_string="pd.read_csv",
46
+ column_names_types={
47
+ "sepal_length": "float",
48
+ "sepal_width": "float",
49
+ "petal_length": "float",
50
+ "petal_width": "float",
51
+ "species": "str",
52
+ },
53
+ )
54
+
55
+ TIPS = DFMetaData(
56
+ file_name="tips_data",
57
+ file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/tips.csv",
58
+ file_location_type="remote",
59
+ read_function_string="pd.read_csv",
60
+ column_names_types={
61
+ "total_bill": "float",
62
+ "tip": "float",
63
+ "sex": "str",
64
+ "smoker": "str",
65
+ "day": "str",
66
+ "time": "str",
67
+ "size": "int",
68
+ },
69
+ )
70
+
71
+ STOCKS = DFMetaData(
72
+ file_name="stocks_data",
73
+ file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/stockdata.csv",
74
+ file_location_type="remote",
75
+ read_function_string="pd.read_csv",
76
+ column_names_types={
77
+ "Date": "str",
78
+ "IBM": "float",
79
+ "MSFT": "float",
80
+ "SBUX": "float",
81
+ "AAPL": "float",
82
+ "GSPC": "float",
83
+ },
84
+ )
85
+
86
+ GAPMINDER = DFMetaData(
87
+ file_name="gapminder_data",
88
+ file_path_or_url="https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv",
89
+ file_location_type="remote",
90
+ read_function_string="pd.read_csv",
91
+ column_names_types={
92
+ "country": "str",
93
+ "continent": "str",
94
+ "year": "int",
95
+ "lifeExp": "float",
96
+ "pop": "int",
97
+ "gdpPercap": "float",
98
+ },
99
+ )
100
+
101
+ SAMPLE_DASHBOARD_CONFIG = """
102
+ {
103
+ `config`: {
104
+ `pages`: [
105
+ {
106
+ `title`: `Iris Data Analysis`,
107
+ `controls`: [
108
+ {
109
+ `id`: `species_filter`,
110
+ `type`: `filter`,
111
+ `column`: `species`,
112
+ `targets`: [
113
+ `scatter_plot`
114
+ ],
115
+ `selector`: {
116
+ `type`: `dropdown`,
117
+ `multi`: true
118
+ }
119
+ }
120
+ ],
121
+ `components`: [
122
+ {
123
+ `id`: `scatter_plot`,
124
+ `type`: `graph`,
125
+ `title`: `Sepal Dimensions by Species`,
126
+ `figure`: {
127
+ `x`: `sepal_length`,
128
+ `y`: `sepal_width`,
129
+ `color`: `species`,
130
+ `_target_`: `scatter`,
131
+ `data_frame`: `iris_data`,
132
+ `hover_data`: [
133
+ `petal_length`,
134
+ `petal_width`
135
+ ]
136
+ }
137
+ }
138
+ ]
139
+ }
140
+ ],
141
+ `theme`: `vizro_dark`,
142
+ `title`: `Iris Dashboard`
143
+ },
144
+ `data_infos`: `
145
+ [
146
+ {
147
+ \"file_name\": \"iris_data\",
148
+ \"file_path_or_url\": \"https://raw.githubusercontent.com/plotly/datasets/master/iris-id.csv\",
149
+ \"file_location_type\": \"remote\",
150
+ \"read_function_string\": \"pd.read_csv\",
151
+ }
152
+ ]
153
+ `
154
+ }
155
+
156
+ """
157
+
158
+
159
+ def convert_github_url_to_raw(path_or_url: str) -> str:
160
+ """Convert a GitHub URL to a raw URL if it's a GitHub URL, otherwise return the original path or URL."""
161
+ github_pattern = r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/([^/]+)/(.+)"
162
+ github_match = re.match(github_pattern, path_or_url)
163
+
164
+ if github_match:
165
+ user, repo, branch, file_path = github_match.groups()
166
+ return f"https://raw.githubusercontent.com/{user}/{repo}/{branch}/{file_path}"
167
+
168
+ return path_or_url
169
+
170
+
171
+ def load_dataframe_by_format(
172
+ path_or_url: Union[str, Path], mime_type: Optional[str] = None
173
+ ) -> tuple[pd.DataFrame, Literal["pd.read_csv", "pd.read_json", "pd.read_html", "pd.read_excel", "pd.read_parquet"]]:
174
+ """Load a dataframe based on file format determined by MIME type or file extension."""
175
+ file_path_str_lower = str(path_or_url).lower()
176
+
177
+ # Determine format
178
+ if mime_type == "text/csv" or file_path_str_lower.endswith(".csv"):
179
+ df = pd.read_csv(
180
+ path_or_url,
181
+ on_bad_lines="warn",
182
+ low_memory=False,
183
+ )
184
+ read_fn = "pd.read_csv"
185
+ elif mime_type == "application/json" or file_path_str_lower.endswith(".json"):
186
+ df = pd.read_json(path_or_url)
187
+ read_fn = "pd.read_json"
188
+ elif mime_type == "text/html" or file_path_str_lower.endswith((".html", ".htm")):
189
+ tables = pd.read_html(path_or_url)
190
+ if not tables:
191
+ raise ValueError("No HTML tables found in the provided file or URL")
192
+ df = tables[0] # Get the first table by default
193
+ read_fn = "pd.read_html"
194
+ elif mime_type in [
195
+ "application/vnd.ms-excel",
196
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
197
+ "application/vnd.oasis.opendocument.spreadsheet",
198
+ ] or any(file_path_str_lower.endswith(ext) for ext in [".xls", ".xlsx", ".ods"]):
199
+ df = pd.read_excel(path_or_url) # opens only sheet 0
200
+ read_fn = "pd.read_excel"
201
+ elif mime_type == "application/vnd.apache.parquet" or file_path_str_lower.endswith(
202
+ ".parquet"
203
+ ): # mime type exists but I did not manage to ever extract it
204
+ df = pd.read_parquet(path_or_url)
205
+ read_fn = "pd.read_parquet"
206
+ else:
207
+ raise ValueError("Could not determine file format")
208
+
209
+ # Check if the result is a Series and convert to DataFrame if needed
210
+ if isinstance(df, pd.Series):
211
+ df = df.to_frame()
212
+
213
+ return df, read_fn
214
+
215
+
216
+ def path_or_url_check(string: str) -> str:
217
+ """Check if a string is a link or a file path."""
218
+ if string.startswith(("http://", "https://", "www.")):
219
+ return "remote"
220
+
221
+ if Path(string).is_file():
222
+ return "local"
223
+
224
+ return "invalid"
225
+
226
+
227
+ def get_dataframe_info(df: pd.DataFrame) -> DFInfo:
228
+ """Get the info of a DataFrame."""
229
+ buffer = io.StringIO()
230
+ df.info(buf=buffer)
231
+ info_string = buffer.getvalue()
232
+
233
+ # Sample only as many rows as exist in the dataframe, up to 5
234
+ sample_size = min(5, len(df)) if not df.empty else 0
235
+
236
+ return DFInfo(general_info=info_string, sample=df.sample(sample_size).to_dict() if sample_size > 0 else {})
237
+
238
+
239
+ def create_pycafe_url(python_code: str) -> str:
240
+ """Create a PyCafe URL for a given Python code."""
241
+ # Create JSON object for py.cafe
242
+ json_object = {
243
+ "code": python_code,
244
+ "requirements": "vizro==0.1.38",
245
+ "files": [],
246
+ }
247
+
248
+ # Convert to compressed base64 URL
249
+ json_text = json.dumps(json_object)
250
+ compressed_json_text = gzip.compress(json_text.encode("utf8"))
251
+ base64_text = base64.b64encode(compressed_json_text).decode("utf8")
252
+ query = urlencode({"c": base64_text}, quote_via=quote)
253
+ pycafe_url = f"{PYCAFE_URL}/snippet/vizro/v1?{query}"
254
+
255
+ return pycafe_url
256
+
257
+
258
+ def get_python_code_and_preview_link(
259
+ model_object: vm.VizroBaseModel, data_infos: list[DFMetaData]
260
+ ) -> VizroCodeAndPreviewLink:
261
+ """Get the Python code and preview link for a Vizro model object."""
262
+ # Get the Python code
263
+ python_code = model_object._to_python()
264
+
265
+ # Add imports after the first empty line
266
+ lines = python_code.splitlines()
267
+ for i, line in enumerate(lines):
268
+ if not line.strip():
269
+ # Found first empty line, insert imports here
270
+ imports_to_add = [
271
+ "from vizro import Vizro",
272
+ "import pandas as pd",
273
+ "from vizro.managers import data_manager",
274
+ ]
275
+ lines[i:i] = imports_to_add
276
+ break
277
+
278
+ python_code = "\n".join(lines)
279
+
280
+ # Prepare data loading code
281
+ data_loading_code = "\n".join(
282
+ f'data_manager["{info.file_name}"] = {info.read_function_string}("{info.file_path_or_url}")'
283
+ for info in data_infos
284
+ )
285
+
286
+ # Patterns to identify the data manager section
287
+ data_manager_start_marker = "####### Data Manager Settings #####"
288
+ data_manager_end_marker = "########### Model code ############"
289
+
290
+ # Replace everything between the markers with our data loading code
291
+ pattern = re.compile(f"{data_manager_start_marker}.*?{data_manager_end_marker}", re.DOTALL)
292
+ replacement = f"{data_manager_start_marker}\n{data_loading_code}\n\n{data_manager_end_marker}"
293
+ python_code = pattern.sub(replacement, python_code)
294
+
295
+ # Add final run line
296
+ python_code += "\n\nVizro().build(model).run()"
297
+
298
+ pycafe_url = create_pycafe_url(python_code)
299
+
300
+ return VizroCodeAndPreviewLink(python_code=python_code, pycafe_url=pycafe_url)
vizro_mcp/py.typed ADDED
File without changes