Richard commited on
Commit
2994705
·
0 Parent(s):

Initial commit

Browse files
Files changed (7) hide show
  1. .gitignore +9 -0
  2. Dockerfile +32 -0
  3. README.md +26 -0
  4. main.py +18 -0
  5. requirements.txt +2 -0
  6. ruff.toml +2 -0
  7. wsgi_app.py +148 -0
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Mesop
2
+ .env
3
+
4
+ # Python
5
+ __pycache__
6
+ .pytest_cache
7
+
8
+ # System
9
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10.14-bullseye
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y \
5
+ # General dependencies
6
+ locales \
7
+ locales-all && \
8
+ # Clean local repository of package files since they won't be needed anymore.
9
+ # Make sure this line is called after all apt-get update/install commands have
10
+ # run.
11
+ apt-get clean && \
12
+ # Also delete the index files which we also don't need anymore.
13
+ rm -rf /var/lib/apt/lists/*
14
+
15
+ ENV LC_ALL en_US.UTF-8
16
+ ENV LANG en_US.UTF-8
17
+ ENV LANGUAGE en_US.UTF-8
18
+
19
+ # Install dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install -r requirements.txt
22
+
23
+ # Create non-root user
24
+ RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop
25
+ USER mesop
26
+
27
+ # Add app code here
28
+ COPY --chown=mesop:mesop . /srv/mesop-app
29
+ WORKDIR /srv/mesop-app
30
+
31
+ # Run Mesop through gunicorn. Should be available at localhost:8080
32
+ CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:wsgi", "--timeout", "300"]
README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mesop App Runner
3
+ emoji: 🦀
4
+ colorFrom: pink
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ app_port: 8080
10
+ ---
11
+
12
+ # Mesop App Runner
13
+
14
+ The Mesop App Runner is used for running code generated by [Mesop App Maker](https://github.com/richard-to/mesop-app-maker).
15
+
16
+ ## Usage
17
+
18
+ The Mesop App Runner uses Docker to avoid potentially destructive code changes.
19
+
20
+ It can be started using these commands:
21
+
22
+ ```shell
23
+ docker stop mesop-app-runner;
24
+ docker rm mesop-app;
25
+ docker build -t mesop-app-runner . && docker run --name mesop-app-runner -d -p 8080:8080 mesop-app-runner;
26
+ ```
main.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mesop as me
2
+ import wsgi_app
3
+
4
+ wsgi = wsgi_app.wsgi_app
5
+
6
+
7
+ @me.page(
8
+ title="Mesop App Runner",
9
+ security_policy=me.SecurityPolicy(
10
+ allowed_iframe_parents=[
11
+ "localhost:*",
12
+ "https://richard-to-mesop-app-maker.hf.space",
13
+ "https://huggingface.co",
14
+ ]
15
+ ),
16
+ )
17
+ def main():
18
+ me.text("Hello World!")
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gunicorn
2
+ mesop==0.12.2
ruff.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ line-length = 100
2
+ indent-width = 2
wsgi_app.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import secrets
4
+ import sys
5
+ import traceback
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timedelta
8
+ from typing import Any, Callable
9
+
10
+ from absl import flags
11
+ from flask import Flask
12
+ from flask import request
13
+
14
+ from mesop.cli.execute_module import execute_module
15
+ from mesop.runtime import enable_debug_mode
16
+ from mesop.runtime import reset_runtime
17
+ from mesop.runtime import hot_reload_finished
18
+ from mesop.server.constants import PROD_PACKAGE_PATH
19
+ from mesop.server.flags import port
20
+ from mesop.server.logging import log_startup
21
+ from mesop.server.server import configure_flask_app
22
+ from mesop.server.static_file_serving import configure_static_file_serving
23
+
24
+ PAGE_EXPIRATION_MINUTES = 10
25
+ MAIN_MODULE = "main"
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RegisteredModule:
30
+ name: str = ""
31
+ created_at: datetime = field(default_factory=lambda: datetime.now())
32
+
33
+
34
+ registered_modules = set([RegisteredModule(MAIN_MODULE)])
35
+
36
+
37
+ class App:
38
+ _flask_app: Flask
39
+
40
+ def __init__(self, flask_app: Flask):
41
+ self._flask_app = flask_app
42
+
43
+ def run(self):
44
+ log_startup(port=port())
45
+ self._flask_app.run(host="::", port=port(), use_reloader=False)
46
+
47
+
48
+ def create_app(prod_mode: bool, run_block: Callable[..., None] | None = None) -> App:
49
+ flask_app = configure_flask_app(prod_mode=prod_mode)
50
+
51
+ # Enable debug mode so we can see errors with the code we're running.
52
+ enable_debug_mode()
53
+
54
+ if run_block is not None:
55
+ run_block()
56
+
57
+ configure_static_file_serving(flask_app, static_file_runfiles_base=PROD_PACKAGE_PATH)
58
+
59
+ @flask_app.route("/exec", methods=["POST"])
60
+ def exec_route():
61
+ global registered_modules
62
+
63
+ param = request.form.get("code")
64
+ new_module = RegisteredModule()
65
+ if param is None:
66
+ raise Exception("Missing request parameter")
67
+ try:
68
+ new_module = RegisteredModule(f"page_{secrets.token_urlsafe(8)}")
69
+
70
+ # Create a new page with the code to run
71
+ # We expect `@me.page()` here for this to work.
72
+ code = base64.urlsafe_b64decode(param)
73
+ code = code.decode("utf-8").replace(
74
+ "@me.page()",
75
+ f'@me.page(path="/{new_module.name}", security_policy=me.SecurityPolicy(allowed_iframe_parents=["localhost:*", "https://richard-to-mesop-app-maker.hf.space", "https://huggingface.co"]))',
76
+ )
77
+ # Write to tmp since Hugging Face does not allow writing to the repo directory.
78
+ with open(f"/tmp/{new_module.name}.py", "w") as file:
79
+ file.write(code)
80
+
81
+ # Add new registered path
82
+ registered_modules.add(new_module)
83
+
84
+ # Clean up old registered paths (except main)
85
+ registered_modules_to_delete = set()
86
+ for registered_module in registered_modules:
87
+ current_registered_module = registered_module
88
+ if (
89
+ registered_module.name != MAIN_MODULE
90
+ and registered_module.created_at
91
+ < datetime.now() - timedelta(minutes=PAGE_EXPIRATION_MINUTES)
92
+ ):
93
+ registered_modules_to_delete.add(registered_module)
94
+ registered_modules -= registered_modules_to_delete
95
+
96
+ # Manually hot reload
97
+ reset_runtime()
98
+ for module in registered_modules:
99
+ if module.name != "main":
100
+ execute_module(module_path=f"/tmp/{module.name}.py", module_name=module.name)
101
+ else:
102
+ execute_module(
103
+ module_path=make_path_absolute(f"{module.name}.py"),
104
+ module_name=module.name,
105
+ )
106
+ hot_reload_finished()
107
+
108
+ except Exception:
109
+ # If there was an error, it's likely that the code failed during hot reload, so
110
+ # we need to trigger another hot reload without the bad code.
111
+ # For simplicity, we just remove all the generated files
112
+ reset_runtime()
113
+ execute_module(
114
+ module_path=make_path_absolute("main.py"),
115
+ module_name="main",
116
+ )
117
+ registered_modules = set([RegisteredModule(MAIN_MODULE)])
118
+
119
+ hot_reload_finished()
120
+ # Get the current exception information
121
+ exc_type, exc_value, exc_traceback = sys.exc_info()
122
+ # Format the traceback as a string
123
+ tb_string = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
124
+ return tb_string, 500
125
+
126
+ # Return the page path that's running the new code, so we can update the iframe with
127
+ # the right path.
128
+ return f"/{new_module.name}"
129
+
130
+ return App(flask_app=flask_app)
131
+
132
+
133
+ _app = None
134
+
135
+
136
+ def wsgi_app(environ: dict[Any, Any], start_response: Callable[..., Any]):
137
+ global _app
138
+ if not _app:
139
+ flags.FLAGS(sys.argv[:1])
140
+ _app = create_app(prod_mode=True)
141
+ return _app._flask_app.wsgi_app(environ, start_response)
142
+
143
+
144
+ def make_path_absolute(file_path: str):
145
+ if os.path.isabs(file_path):
146
+ return file_path
147
+ absolute_path = os.path.join(os.getcwd(), file_path)
148
+ return absolute_path