diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index 1d0ffaf1c..fb1010664 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/pynecone/.templates/web/utils/state.js @@ -2,7 +2,9 @@ import axios from "axios"; import io from "socket.io-client"; import JSON5 from "json5"; +import config from "../pynecone.json" +const UPLOAD = config.uploadUrl; // Global variable to hold the token. let token; @@ -92,7 +94,7 @@ export const applyEvent = async (event, router, socket) => { if (event.name == "_set_value") { event.payload.ref.current.value = event.payload.value; - return false; + return false; } // Send the event to the server. @@ -106,6 +108,24 @@ export const applyEvent = async (event, router, socket) => { return false; }; +/** + * Process an event off the event queue. + * @param queue_event The current event + * @param state The state with the event queue. + * @param setResult The function to set the result. + */ +export const applyRestEvent = async ( + queue_event, + state, + setResult, +) => { + if (queue_event.handler == "uploadFiles") { + await uploadFiles(state, setResult, queue_event.name, UPLOAD) + } + +} + + /** * Process an event off the event queue. * @param state The state with the event queue. @@ -132,19 +152,29 @@ export const updateState = async ( setResult({ ...result, processing: true }); // Pop the next event off the queue and apply it. - const event = state.events.shift(); - + const queue_event = state.events.shift(); // Set new events to avoid reprocessing the same event. setState({ ...state, events: state.events }); - // Apply the event. - const eventSent = await applyEvent(event, router, socket); - if (!eventSent) { - // If no event was sent, set processing to false and return. - setResult({ ...state, processing: false }); + // Process events with handlers via REST and all others via websockets. + if (queue_event.handler) { + + await applyRestEvent(queue_event, state, setResult) + + } + else { + const eventSent = await applyEvent(queue_event, router, socket); + if (!eventSent) { + // If no event was sent, set processing to false and return. + setResult({ ...state, processing: false }); + } + } + + }; + /** * Connect to a websocket and set the handlers. * @param socket The socket object to connect. @@ -196,26 +226,21 @@ export const connect = async ( * * @param state The state to apply the delta to. * @param setResult The function to set the result. - * @param files The files to upload. * @param handler The handler to use. - * @param multiUpload Whether handler args on backend is multiupload * @param endpoint The endpoint to upload to. */ export const uploadFiles = async ( state, - result, setResult, - files, handler, endpoint ) => { - // If we are already processing an event, or there are no upload files, return. - if (result.processing || files.length == 0) { - return; - } + const files = state.files - // Set processing to true to block other events from being processed. - setResult({ ...result, processing: true }); + // return if there's no file to upload + if (files.length == 0) { + return + } const headers = { "Content-Type": files[0].type, @@ -246,10 +271,12 @@ export const uploadFiles = async ( * Create an event object. * @param name The name of the event. * @param payload The payload of the event. + * @param use_websocket Whether the event uses websocket. + * @param handler The client handler to process event. * @returns The event object. */ -export const E = (name, payload) => { - return { name, payload }; +export const E = (name, payload = {}, handler = null) => { + return { name, payload, handler }; }; @@ -259,5 +286,5 @@ export const E = (name, payload) => { * @returns True if the value is truthy, false otherwise. */ export const isTrue = (val) => { - return Array.isArray(val) ? val.length > 0 : !!val + return Array.isArray(val) ? val.length > 0 : !!val } diff --git a/pynecone/components/tags/tag.py b/pynecone/components/tags/tag.py index 3479a08af..44ce65906 100644 --- a/pynecone/components/tags/tag.py +++ b/pynecone/components/tags/tag.py @@ -77,10 +77,7 @@ class Tag(Base): elif isinstance(prop, EventChain): local_args = ",".join(([str(a) for a in prop.events[0].local_args])) - if len(prop.events) == 1 and prop.events[0].upload: - # Special case for upload events. - event = format.format_upload_event(prop.events[0]) - elif prop.full_control: + if prop.full_control: # Full control component events. event = format.format_full_control_event(prop) else: diff --git a/pynecone/event.py b/pynecone/event.py index 1746678f0..681b1f047 100644 --- a/pynecone/event.py +++ b/pynecone/event.py @@ -61,7 +61,10 @@ class EventHandler(Base): for arg in args: # Special case for file uploads. if isinstance(arg, FileUpload): - return EventSpec(handler=self, upload=True) + return EventSpec( + handler=self, + client_handler_name="uploadFiles", + ) # Otherwise, convert to JSON. try: @@ -86,15 +89,15 @@ class EventSpec(Base): # The event handler. handler: EventHandler + # The handler on the client to process event. + client_handler_name: str = "" + # The local arguments on the frontend. local_args: Tuple[Var, ...] = () # The arguments to pass to the function. args: Tuple[Tuple[Var, Var], ...] = () - # Whether to upload files. - upload: bool = False - class Config: """The Pydantic config.""" diff --git a/pynecone/pc.py b/pynecone/pc.py index 824aa56c4..cd55d5e73 100644 --- a/pynecone/pc.py +++ b/pynecone/pc.py @@ -83,6 +83,9 @@ def run( frontend_port = get_config().port if port is None else port backend_port = get_config().backend_port if backend_port is None else backend_port + # set the upload url in pynecone.json file + build.set_pynecone_upload_endpoint() + # If --no-frontend-only and no --backend-only, then turn on frontend and backend both if not frontend and not backend: frontend = True diff --git a/pynecone/utils/build.py b/pynecone/utils/build.py index d6535964a..7ebc35ee0 100644 --- a/pynecone/utils/build.py +++ b/pynecone/utils/build.py @@ -20,13 +20,33 @@ if TYPE_CHECKING: from pynecone.app import App +def update_json_file(file_path, key, value): + """Update the contents of a json file. + + Args: + file_path: the path to the JSON file. + key: object key to update. + value: value of key. + """ + with open(file_path) as f: # type: ignore + json_object = json.load(f) + json_object[key] = value + with open(file_path, "w") as f: + json.dump(json_object, f, ensure_ascii=False) + + def set_pynecone_project_hash(): """Write the hash of the Pynecone project to a PCVERSION_APP_FILE.""" - with open(constants.PCVERSION_APP_FILE) as f: # type: ignore - pynecone_json = json.load(f) - pynecone_json["project_hash"] = random.getrandbits(128) - with open(constants.PCVERSION_APP_FILE, "w") as f: - json.dump(pynecone_json, f, ensure_ascii=False) + update_json_file( + constants.PCVERSION_APP_FILE, "project_hash", random.getrandbits(128) + ) + + +def set_pynecone_upload_endpoint(): + """Write the upload url to a PCVERSION_APP_FILE.""" + update_json_file( + constants.PCVERSION_APP_FILE, "uploadUrl", constants.Endpoint.UPLOAD.get_url() + ) def generate_sitemap(deploy_url: str): diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index 150c2193d..774c17361 100644 --- a/pynecone/utils/format.py +++ b/pynecone/utils/format.py @@ -294,21 +294,8 @@ def format_event(event_spec: EventSpec) -> str: for name, val in event_spec.args ] ) - return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})" - - -def format_upload_event(event_spec: EventSpec) -> str: - """Format an upload event. - - Args: - event_spec: The event to format. - - Returns: - The compiled event. - """ - state, name = get_event_handler_parts(event_spec.handler) - parent_state = state.split(".")[0] - return f'uploadFiles({parent_state}, {constants.RESULT}, set{constants.RESULT.capitalize()}, {parent_state}.files, "{state}.{name}",UPLOAD)' + quote = '"' + return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')}, {wrap(event_spec.client_handler_name, quote) if event_spec.client_handler_name else ''})" def format_full_control_event(event_chain: EventChain) -> str: diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index d2f0e1ee7..42c059258 100644 --- a/tests/components/test_tag.py +++ b/tests/components/test_tag.py @@ -25,7 +25,7 @@ def mock_event(arg): ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'), ( EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]), - '{() => Event([E("mock_event", {})])}', + '{() => Event([E("mock_event", {}, )])}', ), ( EventChain( @@ -37,7 +37,7 @@ def mock_event(arg): ) ] ), - '{(_e) => Event([E("mock_event", {arg:_e.target.value})])}', + '{(_e) => Event([E("mock_event", {arg:_e.target.value}, )])}', ), ({"a": "red", "b": "blue"}, '{{"a": "red", "b": "blue"}}'), (BaseVar(name="var", type_="int"), "{var}"), diff --git a/tests/test_event.py b/tests/test_event.py index d60ce0fb7..f39068373 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -47,7 +47,7 @@ def test_call_event_handler(): assert event_spec.handler == handler assert event_spec.local_args == () assert event_spec.args == () - assert format.format_event(event_spec) == 'E("test_fn", {})' + assert format.format_event(event_spec) == 'E("test_fn", {}, )' handler = EventHandler(fn=test_fn_with_args) event_spec = handler(make_var("first"), make_var("second")) @@ -58,14 +58,14 @@ def test_call_event_handler(): assert event_spec.args == (("arg1", "first"), ("arg2", "second")) assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:first,arg2:second})' + == 'E("test_fn_with_args", {arg1:first,arg2:second}, )' ) # Passing args as strings should format differently. event_spec = handler("first", "second") # type: ignore assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:"first",arg2:"second"})' + == 'E("test_fn_with_args", {arg1:"first",arg2:"second"}, )' ) first, second = 123, "456" @@ -73,7 +73,7 @@ def test_call_event_handler(): event_spec = handler(first, second) # type: ignore assert ( format.format_event(event_spec) - == 'E("test_fn_with_args", {arg1:123,arg2:"456"})' + == 'E("test_fn_with_args", {arg1:123,arg2:"456"}, )' ) assert event_spec.handler == handler @@ -94,9 +94,9 @@ def test_event_redirect(): assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_redirect" assert spec.args == (("path", "/path"),) - assert format.format_event(spec) == 'E("_redirect", {path:"/path"})' + assert format.format_event(spec) == 'E("_redirect", {path:"/path"}, )' spec = event.redirect(Var.create_safe("path")) - assert format.format_event(spec) == 'E("_redirect", {path:path})' + assert format.format_event(spec) == 'E("_redirect", {path:path}, )' def test_event_console_log(): @@ -105,9 +105,9 @@ def test_event_console_log(): assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_console" assert spec.args == (("message", "message"),) - assert format.format_event(spec) == 'E("_console", {message:"message"})' + assert format.format_event(spec) == 'E("_console", {message:"message"}, )' spec = event.console_log(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_console", {message:message})' + assert format.format_event(spec) == 'E("_console", {message:message}, )' def test_event_window_alert(): @@ -116,9 +116,9 @@ def test_event_window_alert(): assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_alert" assert spec.args == (("message", "message"),) - assert format.format_event(spec) == 'E("_alert", {message:"message"})' + assert format.format_event(spec) == 'E("_alert", {message:"message"}, )' spec = event.window_alert(Var.create_safe("message")) - assert format.format_event(spec) == 'E("_alert", {message:message})' + assert format.format_event(spec) == 'E("_alert", {message:message}, )' def test_set_value(): @@ -130,8 +130,8 @@ def test_set_value(): ("ref", Var.create_safe("ref_input1")), ("value", ""), ) - assert format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:""})' + assert format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:""}, )' spec = event.set_value("input1", Var.create_safe("message")) assert ( - format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:message})' + format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:message}, )' ) diff --git a/tests/test_utils.py b/tests/test_utils.py index f0ea7088d..2b79e79a5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -366,33 +366,6 @@ def test_issubclass(cls: type, cls_check: type, expected: bool): assert types._issubclass(cls, cls_check) == expected -def test_format_sub_state_event(upload_sub_state_event_spec): - """Test formatting an upload event spec of substate. - - Args: - upload_sub_state_event_spec: The event spec fixture. - """ - assert ( - format.format_upload_event(upload_sub_state_event_spec) - == "uploadFiles(base_state, result, setResult, base_state.files, " - '"base_state.sub_upload_state.handle_upload",UPLOAD)' - ) - - -def test_format_upload_event(upload_event_spec): - """Test formatting an upload event spec. - - Args: - upload_event_spec: The event spec fixture. - """ - assert ( - format.format_upload_event(upload_event_spec) - == "uploadFiles(upload_state, result, setResult, " - 'upload_state.files, "upload_state.handle_upload1",' - "UPLOAD)" - ) - - @pytest.mark.parametrize( "app_name,expected_config_name", [