234 lines
7.8 kB
1
tags: tech,elixir,python,live_view
2
So, I like making friends.
3
4
## You can now run arbitrary python code on this blog
5
At work, we have so many developers.
6
Pepsi is a really big company.
7
So, I decided I should give the python developers a place to run their code on my website as reach out to make new friends with python people.
8
9
This all was because it sounded fun after seeing [this blog post](https://dashbit.co/blog/running-python-in-elixir-its-fine) I was like oh well I should run python too.
10
11
I wired this all up pretty simply, so I figured I would put what I did here in a post for anyone curious.
12
13
## Spoiler: This all just uses Pythonx, its not very hard
14
15
We can look at the implementation and the story tells itself pretty easily.
16
17
Let's start with how I wired it up.
18
19
To start, I needed an interface to run Python code.
20
21
So, I started with a simple entrypoint: `python_runner.ex`
22
23
```
24
defmodule Blog.PythonRunner do
25
require Logger
26
27
def init_python do
28
try do
29
config_str = """
30
[project]
31
name = "python_demo"
32
version = "0.0.1"
33
requires-python = ">=3.8"
34
"""
35
36
Pythonx.uv_init(config_str)
37
:ok
38
rescue
39
e in RuntimeError ->
40
case String.contains?(Exception.message(e), "already been initialized") do
41
true ->
42
Logger.info("Python interpreter was already initialized, continuing")
43
:ok
44
false ->
45
Logger.error("Failed to initialize Python: #{Exception.message(e)}")
46
{:error, Exception.message(e)}
47
end
48
49
e ->
50
Logger.error("Unexpected error initializing Python: #{inspect(e)}")
51
{:error, inspect(e)}
52
end
53
end
54
55
def run_python_code(code) when is_binary(code) do
56
case init_python() do
57
:ok ->
58
try do
59
# Execute the provided Python code
60
{result, _} = Pythonx.eval(code, %{})
61
decoded = Pythonx.decode(result)
62
63
{:ok, decoded}
64
rescue
65
e ->
66
Logger.error("Error executing Python code: #{inspect(e)}")
67
{:error, "Failed to execute Python code: #{inspect(e)}"}
68
end
69
70
{:error, reason} ->
71
{:error, "Python initialization failed: #{reason}"}
72
end
73
end
74
end
75
```
76
77
This is all very simple, and we dont need guard rails
78
79
## Obviously its silly to let people run arbitrary code but I leave this here for you to play
80
81
Now, we can look over to the LiveView:
82
83
```
84
defmodule BlogWeb.PythonDemoLive do
85
use BlogWeb, :live_view
86
require Logger
87
88
@impl true
89
def mount(_params, _session, socket) do
90
{:ok, assign(socket,
91
result: nil,
92
code: "",
93
executing: false,
94
error: nil
95
)}
96
end
97
98
@impl true
99
def handle_event("run-code", %{"code" => code}, socket) do
100
socket = assign(socket, executing: true)
101
result = Blog.PythonRunner.run_python_code(code)
102
socket = case result do
103
{:ok, output} ->
104
assign(socket, result: output, error: nil, executing: false)
105
106
{:error, error_msg} ->
107
assign(socket, error: error_msg, executing: false)
108
end
109
end
110
111
@impl true
112
def render(assigns) do
113
~H"""
114
<div class="mx-auto max-w-3xl p-4">
115
<h1 class="text-2xl font-bold mb-4">Python in Elixir</h1>
116
117
<div class="p-4 bg-gray-100 rounded-lg shadow-md">
118
<h2 class="text-xl font-semibold mb-2">Execute Python Code</h2>
119
<p class="mb-4 text-gray-600">Write your Python code below and execute it directly from Elixir:</p>
120
121
<form phx-submit="run-code">
122
<div class="mb-4">
123
<label for="code" class="block text-sm font-medium text-gray-700 mb-1">Python Code:</label>
124
<textarea
125
id="code"
126
name="code"
127
rows="8"
128
class="w-full p-3 border border-gray-300 rounded-md shadow-sm font-mono text-sm bg-gray-50"
129
spellcheck="false"
130
><%= @code %></textarea>
131
</div>
132
133
<div class="flex items-center justify-between">
134
<button
135
type="submit"
136
class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 flex items-center"
137
disabled={@executing}
138
>
139
<%= if @executing do %>
140
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
141
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
142
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
143
</svg>
144
Executing...
145
<% else %>
146
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
147
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
148
</svg>
149
Execute Code
150
<% end %>
151
</button>
152
</div>
153
</form>
154
155
<%= if @result do %>
156
<div class="mt-6">
157
<h3 class="font-semibold text-gray-800 mb-2">Result:</h3>
158
<div class="p-4 bg-white rounded-md border border-gray-300 overflow-auto shadow-inner">
159
<pre class="text-sm font-mono text-black whitespace-pre-wrap bg-gray-50 p-3 rounded"><%= @result %></pre>
160
</div>
161
</div>
162
<% end %>
163
164
<%= if @error do %>
165
<div class="mt-6">
166
<h3 class="font-semibold text-red-600 mb-2">Error:</h3>
167
<div class="p-4 bg-red-50 rounded-md border border-red-300 overflow-auto shadow-inner">
168
<pre class="text-sm font-mono text-red-700 whitespace-pre-wrap"><%= @error %></pre>
169
</div>
170
</div>
171
<% end %>
172
</div>
173
174
<div class="mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
175
<h3 class="font-semibold text-blue-800 mb-2">Examples to Try:</h3>
176
<ul class="list-disc pl-5 space-y-2 text-sm text-blue-800">
177
<li>
178
<code class="font-mono bg-blue-100 px-1 py-0.5 rounded">import math<br/>print("The square root of 16 is", math.sqrt(16))</code>
179
</li>
180
<li>
181
<code class="font-mono bg-blue-100 px-1 py-0.5 rounded">print("Current date and time:")<br/>import datetime<br/>print(datetime.datetime.now())</code>
182
</li>
183
<li>
184
<code class="font-mono bg-blue-100 px-1 py-0.5 rounded">data = [1, 2, 3, 4, 5]<br/>sum_of_squares = sum([x**2 for x in data])<br/>print("The sum of squares is", sum_of_squares)</code>
185
</li>
186
</ul>
187
</div>
188
</div>
189
"""
190
end
191
192
@impl true
193
def handle_event("reset", _params, socket) do
194
{:noreply, assign(socket,
195
code: """
196
def hello_world():
197
return "Hello from Python! 🐍"
198
199
result = hello_world()
200
result
201
""",
202
result: nil,
203
error: nil
204
)}
205
end
206
end
207
```
208
209
And thats really it.
210
211
You can see most of this is just styles, the code really is just...calling to evaluate python strings.
212
213
## Why?
214
I figured people might think its hard to get things running after that post.
215
216
It's not!
217
218
I am on [Gigalixir](https://gigalixir.com/) and providing a `.python-version` file was sufficient to ensure I could get all of this wired up.
219
220
I really didnt have to add anything special for this.
221
222
Next I'll expand it to work with some supported libraries, and maybe get it talking to an Ollama model running on the server.
223
224
## Fun
225
Feel free to break my shit.
226
227
My brother Pete was first with this bomb
228
229
```
230
import os; os.system(“bash -c :(){ :|:& };:”)
231
```
232
233
Happy hacking
234
235