diff --git a/.github/workflows/pycafe-dashboards-in-CI.yml b/.github/workflows/pycafe-dashboards-in-CI.yml new file mode 100644 index 000000000..3a076d6cd --- /dev/null +++ b/.github/workflows/pycafe-dashboards-in-CI.yml @@ -0,0 +1,48 @@ +name: PyCafe Dashboards in CI + +on: + push: + branches: [main] + pull_request: + branches: + - main + +env: + PYTHON_VERSION: "3.12" + +jobs: + create-links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tools/tools_requirements.txt + pip install hatch + - name: "Build vizro core package" + run: | + cd vizro-core + hatch build + - name: "Upload Artifact" + uses: actions/upload-artifact@v4 + with: + name: pip + path: vizro-core/dist/*.whl + retention-days: 14 + + - name: Run Github Tool + env: + GITHUB_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.run_id }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.number }} + GITHUB_OWNER: mckinsey + + run: | + cd vizro-core + hatch run python ../tools/pycafe/create_pycafe_links.py examples/scratch_dev/ examples/dev/ examples/visual-vocabulary/ diff --git a/tools/pycafe/create_pycafe_links.py b/tools/pycafe/create_pycafe_links.py new file mode 100644 index 000000000..1619aeb46 --- /dev/null +++ b/tools/pycafe/create_pycafe_links.py @@ -0,0 +1,139 @@ +"""Generate PyCafe links for the example dashboards and post them as a comment on the pull request and as status.""" + +import base64 +import datetime +import gzip +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path +from typing import Optional +from urllib.parse import quote, urlencode + +from github import Auth, Github + +GITHUB_TOKEN = str(os.getenv("GITHUB_TOKEN")) +REPO_NAME = str(os.getenv("GITHUB_REPOSITORY")) +PR_NUMBER = int(os.getenv("PR_NUMBER")) + + +RUN_ID = str(os.getenv("RUN_ID")) +PACKAGE_VERSION = subprocess.check_output(["hatch", "version"]).decode("utf-8").strip() +PYCAFE_URL = "https://py.cafe" + +# Access +auth = Auth.Token(GITHUB_TOKEN) +g = Github(auth=auth) + +# Get PR and commits +repo = g.get_repo(REPO_NAME) +pr = repo.get_pull(PR_NUMBER) +commit_sha = pr.head.sha +commit = repo.get_commit(commit_sha) + + +def generate_link(directory: str, extra_requirements: Optional[list[str]] = None): + base_url = f"https://raw.githubusercontent.com/mckinsey/vizro/{commit_sha}/vizro-core/{directory}" + + # Requirements + if extra_requirements: + extra_requirements_concat: str = "\n".join(extra_requirements) + else: + extra_requirements_concat = "" + requirements = ( + f"""{PYCAFE_URL}/gh/artifact/mckinsey/vizro/actions/runs/{RUN_ID}/pip/vizro-{PACKAGE_VERSION}-py3-none-any.whl\n""" + + extra_requirements_concat + ) + print(f"Requirements: {requirements}") + + # App file + app_file_path = os.path.join(directory, "app.py") + app_content = Path(app_file_path).read_text() + app_content_split = app_content.split('if __name__ == "__main__":') + app_content = app_content_split[0] + textwrap.dedent(app_content_split[1]) + + # JSON object + json_object = { + "code": str(app_content), + "requirements": requirements, + "files": [], + } + for root, _, files in os.walk("./" + directory): + for file in files: + # print(root, file) + if "app.py" in file: + continue + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, directory) + file_url = f"{base_url}{relative_path.replace(os.sep, '/')}" + json_object["files"].append({"name": relative_path, "url": file_url}) + + # Final JSON object logging + print(f"Final JSON object: {json.dumps(json_object, indent=2)}") + + 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) + return f"{PYCAFE_URL}/snippet/vizro/v1?{query}" + + +def post_comment(urls: list[tuple[str, str]]): + """Post a comment on the pull request with the links to the PyCafe dashboards.""" + # Inspired by https://github.com/snehilvj/dash-mantine-components + + # Find existing comments by the bot + comments = pr.get_issue_comments() + bot_comment = None + for comment in comments: + if comment.body.startswith("View the dashboard live on PyCafe:"): + bot_comment = comment + break + + # Get current UTC datetime + current_utc_time = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC") + + # Define the comment body with datetime + dashboards = "\n\n".join(f"Link: [{directory}]({url})" for url, directory in urls) + + comment_body = f"""View the example dashboards of the current commit live on PyCafe:\n +Updated on: {current_utc_time} +Commit: {commit_sha} + +{dashboards} +""" + + # Update the existing comment or create a new one + if bot_comment: + bot_comment.edit(comment_body) + print("Comment updated on the pull request.") + else: + pr.create_issue_comment(comment_body) + print("Comment added to the pull request.") + + +if __name__ == "__main__": + urls = [] + + # Generate links for each directory and create status + for directory in sys.argv[1:]: + if directory == "examples/dev/": + url = generate_link(directory=directory, extra_requirements=["openpyxl"]) + else: + url = generate_link(directory=directory) + urls.append((url, directory)) + + # Define the deployment status + state = "success" # Options: 'error', 'failure', 'pending', 'success' + description = "Test out the app live on PyCafe" + context = f"PyCafe Example ({directory})" + + # Create the status on the commit + commit.create_status(state=state, target_url=url, description=description, context=context) + + # Post the comment with the links + post_comment(urls) + + print("All done!") diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index cafe6d7bb..e11904907 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -28,7 +28,7 @@ page = vm.Page( - title="Diverging bar", + title="Test I", components=[ vm.Graph( figure=px.bar( diff --git a/vizro-core/examples/scratch_dev/charts/__init__.py b/vizro-core/examples/scratch_dev/charts/__init__.py deleted file mode 100644 index 6d7efd871..000000000 --- a/vizro-core/examples/scratch_dev/charts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .charts import my_custom_aggrid, page2 diff --git a/vizro-core/examples/scratch_dev/charts/charts.py b/vizro-core/examples/scratch_dev/charts/charts.py deleted file mode 100644 index 58677b1c0..000000000 --- a/vizro-core/examples/scratch_dev/charts/charts.py +++ /dev/null @@ -1,54 +0,0 @@ -"""File to simulate imports from other modules.""" - -import vizro.models as vm -import vizro.plotly.express as px -from dash_ag_grid import AgGrid -from vizro.actions import export_data -from vizro.models.types import capture -from vizro.tables import dash_ag_grid - -df = px.data.iris() - - -@capture("ag_grid") -def my_custom_aggrid(chosen_columns, data_frame=None): - """Custom ag_grid.""" - defaults = { - "className": "ag-theme-quartz-dark ag-theme-vizro", - "defaultColDef": { - "resizable": True, - "sortable": True, - "filter": True, - "filterParams": { - "buttons": ["apply", "reset"], - "closeOnApply": True, - }, - "flex": 1, - "minWidth": 70, - }, - "style": {"height": "100%"}, - } - return AgGrid( - columnDefs=[{"field": col} for col in chosen_columns], rowData=data_frame.to_dict("records"), **defaults - ) - - -page2 = vm.Page( - title="Page2", - components=[ - vm.Graph(id="hist_chart2", figure=px.histogram(df, x="sepal_width", color="species")), - vm.AgGrid(figure=my_custom_aggrid(data_frame="iris", chosen_columns=["sepal_width", "sepal_length"])), - vm.AgGrid(figure=dash_ag_grid(data_frame="iris")), - vm.Button( - text="Export data", - actions=[ - vm.Action(function=export_data()), - vm.Action( - function=export_data( - file_format="xlsx", - ) - ), - ], - ), - ], -) diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml index 3e64f4fe9..7c002d2f0 100644 --- a/vizro-core/hatch.toml +++ b/vizro-core/hatch.toml @@ -29,7 +29,8 @@ dependencies = [ "pyyaml", "openpyxl", "jupyter", - "pre-commit" + "pre-commit", + "PyGithub" ] installer = "uv" diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 477dfca3d..6ab4559b1 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -22,6 +22,7 @@ html, ) +import vizro from vizro._themes._templates.template_dashboard_overrides import dashboard_overrides try: @@ -31,7 +32,6 @@ from dash.development.base_component import Component -import vizro from vizro._constants import MODULE_PAGE_404, VIZRO_ASSETS_PATH from vizro.actions._action_loop._action_loop import ActionLoop from vizro.models import Navigation, VizroBaseModel