OpenBB-finance / pywry

MIT License
80 stars 11 forks source link

PyTauri #119

Open AriBermeki opened 2 months ago

AriBermeki commented 2 months ago

Please contact me via email at [ malek.ali@yellow-sic.com ]. I plan to develop a Python version of the Tauri framework. The attached screenshot shows the use of PyWry, Next.js and Vite. There are urgent tasks that need to be handled by PyWry.

Screenshot 2024-06-17 052411
Screenshot 2024-06-17 054028

andrewkenreich commented 2 months ago

Can you explain here what you are requesting to happen? @AriBermeki

AriBermeki commented 2 months ago

Init Static Directory

without fastapi server

Screenshot 2024-06-19 154852

with headless = True

Screenshot 2024-06-19 161453


I strongly assume that the problem lies in the headless.rs file. To solve the problem, one needs to implement a code, like the fastapi code shown underneath,

It could be that this functionality is already implemented, but I'm not sure how to achieve it. I would like pywry to have a HTTP GET method that returns static files as a file response. This GET method should look for the requested file in a python directory. If the file exists, it should be returned as a file response; otherwise a 404 error code (not found) should be returned.

This should be done as part of the default router example. The endpoint @app.get("/") should return an HTML document as HTML response, otherwise a 404 error code should be returned. Similarly, @app.get("/{asset_path}") hould return the corresponding file or a 404 error.

Request example:


<script src="/_next/static/main.js"></script>
<link rel="stylesheet" href="/_next/static/min.css">

Or

<script src="/assets/main.js"></script>
<link rel="stylesheet" href="/assets/min.css">

Python example:


from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path

app = FastAPI()

index_html_path = Path("path/to/index.html")
directory_ = Path("path/to/static/files")
app.mount("/static", StaticFiles(directory=directory_ , html=True), name="static")
@app.get("/")
async def get_index():
    if not index_html_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(index_html_path, media_type='text/html')

@app.get("/{asset_path:path}")
async def get_assets(asset_path: str):
    asset_file_path = directory_ / asset_path
    if not asset_file_path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return FileResponse(asset_file_path)

maybe this code will be helpful for you


tauri assets system

https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/assets.rs

tauri html system


https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/html.rs


tauri mime_type system


https://github.com/tauri-apps/tauri/blob/dev/core/tauri-utils/src/mime_type.rs


IPC communication between Rust and Python:


It would be very helpful if json_data could forward events sent via IPC from the frontend back to the Python domain, e.g. via a socket. That means we need a Rust method or function that can be called from the Python side to set communication parameters such as the communication port or host, so that Wry on the Rust side knows which channels to use for communication with Python.

IPC example in Rust:



use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            let mut buf = [0; 1024];

            // In a loop, read data from the socket and write the data back.
            loop {
                let n = match socket.read(&mut buf).await {
                    // socket closed
                    Ok(n) if n == 0 => return,
                    Ok(n) => n,
                    Err(e) => {
                        eprintln!("failed to read from socket; err = {:?}", e);
                        return;
                    }
                };

                // Write the data back
                if let Err(e) = socket.write_all(&buf[0..n]).await {
                    eprintln!("failed to write to socket; err = {:?}", e);
                    return;
                }
            }
        });
    }
}

IPC example in Python:



import socket

def communicate_with_rust_server(message: str, server_ip: str = '127.0.0.1', server_port: int = 8080):
    # Create a TCP/IP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect the socket to the server's port
    server_address = (server_ip, server_port)
    print(f'Connecting to {server_ip}:{server_port}')
    sock.connect(server_address)

    try:
        # Send data
        print(f'Sending: {message}')
        sock.sendall(message.encode('utf-8'))

        # Look for the response (we'll receive the same data back from the server)
        amount_received = 0
        amount_expected = len(message)

        received_data = []

        while amount_received < amount_expected:
            data = sock.recv(1024)
            amount_received += len(data)
            received_data.append(data)

        print(f'Received: {"".join([chunk.decode("utf-8") for chunk in received_data])}')

    finally:
        print('Closing connection')
        sock.close()

# Example usage:
if __name__ == '__main__':
    communicate_with_rust_server("Hello, Rust server!")

Event control between frontend and backend:


When the frontend wants to trigger a Tao event, it sends the event name via IPC to Wry with a Pywry frontend command. Wry then sends it via IPC to Python, which receives the message and sends a Tao event as a dictionary back to Wry, which then triggers it with the iterateJsonObjectAndAttachTaoEventHandler function.

this is an example of how to implement iterateJsonObjectAndAttachTaoEventHandler function in javascript but it must be done in rust



const synthesizeTaoEventHandler = (event, e) => {
  switch(event) {
    case "onclick":
      console.log("Clicked!", e);
      return;
    // Weitere Fälle hier hinzufügen
  }
};

const iterateJsonObjectAndAttachTaoEventHandler = (json_object, eventList) => {
  const event_object = { ...json_object };
  for (const propName in event_object) {
    if (eventList.includes(propName)) {
      const eventTypeValue = event_object[propName];
      event_object[propName] = (e) => synthesizeTaoEventHandler(eventTypeValue, e);
    }
  }
  return event_object;
};

const jsonObject = {
  event_type: "onclick",
  event_props: {},
  window_id: "value"
};

const eventList = ["onclick", "onmouseover"];

const taoevent = iterateJsonObjectAndAttachTaoEventHandler(jsonObject, eventList);

Rust example


i don't know if they have the same eefecte



use serde_json::Value;

fn synthesize_tao_event_handler(event: &str, e: &Value) {
    match event {
        "onclick" => {
            println!("Clicked! {:?}", e);
        }
        // Add more cases here as needed
        _ => {}
    }
}

fn iterate_json_object_and_attach_tao_event_handler(
    json_object: &[Value],
    event_list: &[&str],
) -> Vec<Value> {
    let mut event_object = json_object.to_vec(); // Make a mutable copy of the input

    for obj in &mut event_object {
        if let Some(event_type_value) = obj.get_mut("event_type") {
            if let Some(event_type_str) = event_type_value.as_str() {
                if event_list.contains(&event_type_str) {
                    let handler_value = serde_json::json!({
                        "handler": event_type_str
                    });
                    *event_type_value = handler_value;
                }
            }
        }
    }

    event_object
}

fn main() {
    let json_data = r#"
    [
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        },
        {
            "event_type": "onclick",
            "event_props": {
                "widith": 800,
                "height": 600
            },
            "window_id": 1
        }
    ]
    "#;

    // Parse JSON data into a Vec<Value>
    let json_object: Vec<Value> = serde_json::from_str(json_data).unwrap();

    let event_list = vec!["onclick", "onmouseover"];

    let taoevent = iterate_json_object_and_attach_tao_event_handler(&json_object, &event_list);

    // Example usage of the handlers
    for handler_obj in &taoevent {
        if let Some(handler) = handler_obj.get("handler") {
            if let Value::String(event_type_value) = handler {
                synthesize_tao_event_handler(event_type_value, &Value::Null);
            }
        }
    }

    println!("{:?}", taoevent);
}

Rust functions for executing Python functions:


We need two Rust functions that can call Python functions (e.g. startup, shutdown) when starting or stopping pywry.


These functions are useful and important to take advantage of Python without starting a separate Python web server that requires additional effort and runtime.


Additional Note:


I assume you have developed pywry for a specific purpose. In my view the purpose is to use Python features for desktop applications without having to use Rust. I am trying to link javascript files via pywry. This does not work for me. I get a file not found error. I believe there will be developers who will get a similar problem. Please help me to solve the problem and extend the functionality of IPC. I would like to send real-time data (via IPC) back and forth.

My knowledge of Rust is almost zero, but I have experience in full-stack development and I know how web technology works in Python. I have experience with FastAPI, Flask, and Django. I believe Pywry has this ability without developing a Python web server, that requires a lot of work and runtime. I wrote these things so that we don't have to create an additional Python web server that runs in the background.

AriBermeki commented 2 months ago

@andrewkenreich

AriBermeki commented 2 months ago

@andrewkenreich

I have solved the first problem (Init Static Directory)


Your headless.rs file must have this response system. This system currently works for me without FastAPI only rust


neu_response


[dependencies]
mime_guess = "2.0.4"
tao = "0.28.1"
wry = "0.41.0"
cargo add mime_guess

use std::path::PathBuf;

use tao::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};
use wry::{
    http::{header::CONTENT_TYPE, Request, Response},
    WebViewBuilder,
};
use mime_guess::from_path;

fn main() -> wry::Result<()> {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
        .build(&event_loop).unwrap();

    #[cfg(any(
        target_os = "windows",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    ))]
    let builder = WebViewBuilder::new(&window);

    #[cfg(not(any(
        target_os = "windows",
        target_os = "macos",
        target_os = "ios",
        target_os = "android"
    )))]
    let builder = {
        use tao::platform::unix::WindowExtUnix;
        use wry::WebViewBuilderExtUnix;
        let vbox = window.default_vbox().unwrap();
        WebViewBuilder::new_gtk(vbox)
    };

    // the variables must be accessible from the python side and initialization script must also be able to interact with this url
    // with pywry.setup() function
    let root_path = PathBuf::from("../out");
    let frame_port = 3000;
    let development = true;
    let development_url = format!("http://localhost:{}", frame_port);
    let index_page = "index.html";

    let builder = builder
        .with_devtools(true)
        .with_hotkeys_zoom(true)
        .with_custom_protocol(
            "wry".into(), 
            move |request| {
                match get_pywry_response_protocol(request, index_page, &root_path) {
                    Ok(r) => r.map(Into::into),
                    Err(e) => Response::builder()
                        .header(CONTENT_TYPE, "text/plain")
                        .status(500)
                        .body(e.to_string().as_bytes().to_vec())
                        .unwrap()
                        .map(Into::into),
                }
            },
        );

    let builder = if development {
        builder.with_url(&development_url)
    } else {
        builder.with_url("wry://localhost")
    };

    let _webview = builder.build()?;

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        if let Event::WindowEvent {
            event: WindowEvent::CloseRequested,
            ..
        } = event
        {
            *control_flow = ControlFlow::Exit
        }
    });
}

fn get_pywry_response_protocol(
    request: Request<Vec<u8>>,
    index_page: &str,
    root_path: &PathBuf
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
    let path = request.uri().path();
    // Read the file content from file path

    let path = if path == "/" {
        index_page
    } else {
        // Removing leading slash
        &path[1..]
    };
    let full_path = std::fs::canonicalize(root_path.join(path))?;
    let content = std::fs::read(&full_path)?;
    #[cfg(target_os = "windows")]
    let headers = "https://wry.localhost".to_string();
    #[cfg(not(target_os = "windows"))]
    let headers = "wry://localhost".to_string();
    // Determine MIME type using `mime_guess`
    let mime_type = from_path(&full_path).first_or_octet_stream();

    Response::builder()
        .header(CONTENT_TYPE, mime_type.as_ref())
        .header("Access-Control-Allow-Origin", headers)
        .header("Cache-Control", "no-cache, no-store, must-revalidate")
        .header("Pragma", "no-cache")
        .header("Expires", "0")
        .header("Accept-Encoding", "gzip, compress, br, deflate")
        .body(content)
        .map_err(Into::into)
}