more attempts at the python runner
Bobby Grayson 1 day ago 2 files (+160, -23)
MODIFIED
lib/blog/python_runner.ex
MODIFIED
lib/blog/python_runner.ex
@@ -6,19 +6,95 @@require Logger@doc """+ Checks if the application is running on Gigalixir.+ """+ def running_on_gigalixir? do+ # Check for typical Gigalixir environment indicators+ System.get_env("GIGALIXIR") == "true" ||+ String.contains?(System.get_env("RELEASE_COOKIE", ""), "gigalixir") ||+ File.exists?("/app/rel")+ end++ @doc """+ Checks if a directory is writable by attempting to create a test file.+ """+ def directory_writable?(path) do+ test_file = Path.join(path, "write_test_#{:rand.uniform(1000000)}")+ try do+ :ok = File.write(test_file, "test")+ File.rm(test_file)+ true+ rescue+ _ -> false+ end+ end++ @doc """+ Gets Python environment information for debugging.+ """+ def get_python_info do+ python_path = System.get_env("PYTHONX_PYTHON_PATH") || "not set"+ cache_dir = System.get_env("PYTHONX_CACHE_DIR") || "not set"++ # Check if key directories are writable+ tmp_writable = directory_writable?("/tmp")+ app_cache_writable = directory_writable?("/app/.cache")+ current_dir_writable = directory_writable?(".")++ %{+ pythonx_version: Application.spec(:pythonx, :vsn) |> to_string(),+ python_path: python_path,+ cache_dir: cache_dir,+ cache_dir_exists: if(cache_dir != "not set", do: File.exists?(cache_dir), else: false),+ cache_dir_writable: if(cache_dir != "not set", do: directory_writable?(cache_dir), else: false),+ tmp_writable: tmp_writable,+ app_cache_writable: app_cache_writable,+ current_dir_writable: current_dir_writable,+ inets_started: Application.started_applications() |> Enum.any?(fn {app, _, _} -> app == :inets end),+ on_gigalixir: running_on_gigalixir?()+ }+ end++ @doc """Initializes a basic Python environment with no additional packages.Safely handles the case where Python is already initialized."""def init do+ # Ensure cache directory exists+ cache_dir = System.get_env("PYTHONX_CACHE_DIR", "/tmp")++ # Try to create all possible cache directories+ try do+ File.mkdir_p!("/tmp/pythonx_cache")+ File.mkdir_p!("/tmp/pythonx_venv")+ File.mkdir_p!(cache_dir)+ rescue+ e -> Logger.warning("Failed to create cache directories: #{inspect(e)}")+ end+try do# Initialize Python with minimal configuration- Pythonx.uv_init("""+ # Use a simpler project configuration with no dependencies+ config_str = """[project]name = "python_demo"version = "0.0.1"requires-python = ">=3.8"- dependencies = []- """)+ """++ # Log Python environment information for debugging+ python_info = get_python_info()+ Logger.info("Python environment details: #{inspect(python_info)}")++ # Options for uv_init - use absolute paths for all values+ options = [+ cache_dir: "/tmp",+ venv_path: "/tmp/pythonx_venv",+ python_path: System.get_env("PYTHONX_PYTHON_PATH", "/usr/bin/python3")+ ]++ # Use uv_init with explicit settings+ Pythonx.uv_init(config_str, options)Logger.info("Python environment initialized successfully"):ok@@ -29,8 +105,25 @@ Logger.info("Python interpreter was already initialized, continuing"):okelseLogger.error("Failed to initialize Python: #{Exception.message(e)}")- reraise e, __STACKTRACE__+ {:error, Exception.message(e)}end++ e in UndefinedFunctionError ->+ Logger.error("UndefinedFunctionError: #{inspect(e)}")+ if e.function == :stop and e.arity == 2 and e.module == :inets do+ Logger.error("The :inets application is not started. This is required by Pythonx.")+ {:error, ":inets not started"}+ else+ {:error, "Undefined function: #{e.module}.#{e.function}/#{e.arity}"}+ end++ e in File.Error ->+ Logger.error("File system error: #{inspect(e)}")+ {:error, "File system error: #{Exception.message(e)}"}++ e ->+ Logger.error("Unexpected error initializing Python: #{inspect(e)}")+ {:error, "Failed to initialize Python: #{inspect(e)}"}endend@@ -46,15 +139,33 @@ result = say_hello()result"""- {result, _} = Pythonx.eval(python_code, %{})- Pythonx.decode(result)+ case init() do+ :ok ->+ try do+ {result, _} = Pythonx.eval(python_code, %{})+ Pythonx.decode(result)+ rescue+ e -> "Error executing Python code: #{inspect(e)}"+ end+ {:error, reason} ->+ "Python is unavailable: #{reason}"+ endend@doc """Runs arbitrary Python code and returns the result."""def run_code(code) when is_binary(code) do- {result, _} = Pythonx.eval(code, %{})- Pythonx.decode(result)+ case init() do+ :ok ->+ try do+ {result, _} = Pythonx.eval(code, %{})+ Pythonx.decode(result)+ rescue+ e -> "Error executing Python code: #{inspect(e)}"+ end+ {:error, reason} ->+ "Python is unavailable: #{reason}"+ endendend
@@ -13,35 +13,30 @@ """@impl truedef mount(_params, _session, socket) do- # Initialize the Python environment - now returns :ok instead of {:ok, _}+ # Get Python environment information+ python_info = PythonRunner.get_python_info()++ # Initialize the Python environmentpython_status = case PythonRunner.init() do:ok -> :available{:error, reason} -> {:unavailable, reason}end- # Get the hello world result+ # Get the hello world result if Python is available+ hello_result = PythonRunner.hello_world(){:ok, assign(socket,code: @python_example,result: "",- python_status: python_status+ hello_result: hello_result,+ python_status: python_status,+ python_info: python_info)}end@impl truedef handle_event("run-code", %{"code" => code}, socket) do- result = if socket.assigns.python_status == :available do- try do- PythonRunner.run_code(code)- rescue- e ->- Logger.error("Error executing Python code: #{inspect(e)}")- "Error: #{inspect(e)}"- end- else- "Python is unavailable"- end-+ result = PythonRunner.run_code(code)Logger.info("Python code execution result: #{inspect(result)}"){:noreply, assign(socket, result: result)}end@@ -54,6 +49,37 @@ <h1 class="text-2xl font-bold mb-4">Python Integration Demo</h1><div class="mb-6 p-4 rounded bg-gray-100"><h2 class="text-xl font-semibold mb-2">Python Status</h2>+ <%= if @python_status == :available do %>+ <div class="text-green-600 font-bold">✅ Python is available</div>+ <% else %>+ <div class="text-red-600 font-bold">❌ Python is unavailable</div>+ <div class="mt-2 p-2 bg-yellow-100 rounded">+ <p>Reason: <%= if is_tuple(@python_status), do: elem(@python_status, 1), else: @python_status %></p>+ </div>+ <% end %>++ <div class="mt-4">+ <h3 class="text-lg font-semibold">Python Environment Info</h3>+ <div class="bg-white p-2 rounded mt-2 text-sm font-mono">+ <div><span class="font-bold">Pythonx Version:</span> <%= @python_info.pythonx_version %></div>+ <div><span class="font-bold">Python Path:</span> <%= @python_info.python_path %></div>+ <div><span class="font-bold">Cache Dir:</span> <%= @python_info.cache_dir %></div>+ <div><span class="font-bold">Cache Exists:</span> <%= @python_info.cache_dir_exists %></div>+ <div><span class="font-bold">Cache Writable:</span> <%= @python_info.cache_dir_writable %></div>+ <div><span class="font-bold">/tmp Writable:</span> <%= @python_info.tmp_writable %></div>+ <div><span class="font-bold">/app/.cache Writable:</span> <%= @python_info.app_cache_writable %></div>+ <div><span class="font-bold">Current Dir Writable:</span> <%= @python_info.current_dir_writable %></div>+ <div><span class="font-bold">Inets Started:</span> <%= @python_info.inets_started %></div>+ <div><span class="font-bold">On Gigalixir:</span> <%= @python_info.on_gigalixir %></div>+ </div>+ </div>+ </div>++ <div class="mb-6 p-4 rounded bg-gray-100">+ <h2 class="text-xl font-semibold mb-2">Hello from Python</h2>+ <div class="p-2 bg-white rounded border">+ <%= @hello_result %>+ </div></div><div class="mb-6">