From e4bee0532ca697ebe1c4527f7f0106560da3d8cb Mon Sep 17 00:00:00 2001 From: Sahil Chhoker Date: Tue, 18 Mar 2025 20:07:11 +0530 Subject: [PATCH] feat: Added Model sharing through links --- mesa/model_share.py | 86 ++++++++++++++++++++++++++++++++ mesa/visualization/solara_viz.py | 9 ++++ 2 files changed, 95 insertions(+) create mode 100644 mesa/model_share.py diff --git a/mesa/model_share.py b/mesa/model_share.py new file mode 100644 index 00000000000..30d9a5e2748 --- /dev/null +++ b/mesa/model_share.py @@ -0,0 +1,86 @@ +"""This module contains functions to generate a shareable py.cafe link containing Python code and dependencies.""" + +import base64 +import gzip +import json +import os +from pathlib import Path +from urllib.parse import quote + + +def get_pycafe_link( + files: dict[str, str] | None = None, + requirements: str | None = None, + autodetect_files: bool = False, +) -> str: + r"""Generate a shareable py.cafe link containing Python code and dependencies. + + Args: + files(dict[str, str] | None): Map of file names to paths, if files are not in the current directory. + + requirements(str | None): Package dependencies, one per line other than the essential ones. + + autodetect_files(bool): If True, scans directory for Python files instead of using `files`. Mutually exclusive with `files`. + + Example: + >>> files = { + >>> "model.py": "wolf_sheep\\model.py", + >>> "agents.py": "wolf_sheep\\agents.py", + >>> "app.py": "wolf_sheep\\app.py" + >>> } + >>> get_pycafe_link(files=files) + + Returns: + str: URL with encoded application code and dependencies. + + """ + requirements = ( + requirements or "mesa\nmatplotlib\nnumpy\nnetworkx\nsolara\naltair\npandas" + ) + + app = "" + file_list = [] + + if autodetect_files: + all_files = _scan_python_files() + for file in all_files: + with open(file) as f: + if file.endswith("app.py"): + app += f.read() + else: + file_dict = {} + file_dict["name"] = os.path.basename(file) + file_dict["content"] = f.read() + file_list.append(file_dict) + else: + for file in files: + with open(files[file]) as f: + file_content = f.read() + if file == "app.py": + app += file_content + else: + file_dict = {"name": file, "content": file_content} + file_list.append(file_dict) + + json_object = {"code": app, "requirements": requirements, "files": file_list} + json_text = json.dumps(json_object) + # Compress using gzip to make the url shorter + compressed_json_text = gzip.compress(json_text.encode("utf8")) + # Encode in base64 + base64_text = base64.b64encode(compressed_json_text).decode("utf8") + c = quote(base64_text) + url = f"https://py.cafe/snippet/solara/v1#c={c}" + + return url + + +def _scan_python_files(directory_path: str = ".") -> list[str]: + """Scan a directory for specific Python files (model.py, app.py, agents.py).""" + path = Path(directory_path) + python_files = [ + str(file) + for file in path.glob("*.py") + if file.name in ["model.py", "app.py", "agents.py"] + ] + + return python_files diff --git a/mesa/visualization/solara_viz.py b/mesa/visualization/solara_viz.py index 5b907a6fd10..16e2f288fda 100644 --- a/mesa/visualization/solara_viz.py +++ b/mesa/visualization/solara_viz.py @@ -37,6 +37,7 @@ import mesa.visualization.components.altair_components as components_altair from mesa.experimental.devs.simulator import Simulator from mesa.mesa_logging import create_module_logger, function_logger +from mesa.model_share import get_pycafe_link from mesa.visualization.command_console import CommandConsole from mesa.visualization.user_param import Slider from mesa.visualization.utils import force_update, update_counter @@ -130,6 +131,14 @@ def SolaraViz( reactive_use_threads = solara.use_reactive(use_threads) with solara.AppBar(): solara.AppBarTitle(name if name else model.value.__class__.__name__) + solara.Button( + label="Open on PyCafe", + color="blue", + attributes={ + "href": get_pycafe_link(autodetect_files=True), + "target": "_blank", + }, + ) solara.lab.ThemeToggle() with solara.Sidebar(), solara.Column():