Spaces:
Sleeping
Sleeping
Richard
commited on
Commit
·
2994705
0
Parent(s):
Initial commit
Browse files- .gitignore +9 -0
- Dockerfile +32 -0
- README.md +26 -0
- main.py +18 -0
- requirements.txt +2 -0
- ruff.toml +2 -0
- 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
|