Add upload component (#622)
This commit is contained in:
parent
8ba22ed92d
commit
f7138bd53f
40
poetry.lock
generated
40
poetry.lock
generated
@ -480,14 +480,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.0.0"
|
||||
version = "3.1.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
|
||||
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
|
||||
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
|
||||
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -648,14 +648,14 @@ dev = ["twine (>=3.4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.1"
|
||||
version = "7.2.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||
{file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
|
||||
{file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -725,6 +725,20 @@ files = [
|
||||
asyncio-client = ["aiohttp (>=3.4)"]
|
||||
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.5"
|
||||
description = "A streaming multipart parser for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "python-socketio"
|
||||
version = "5.7.2"
|
||||
@ -847,6 +861,18 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
@ -1210,4 +1236,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "13e3d8aa740b5a1b24ec4cdd394791f1650ac3bcb618e4430f1620238c6e8a6d"
|
||||
content-hash = "b7272a6016a5b9fb3eea7ce834b9f539919e8f71c7f849d626adb2ee7a354d2f"
|
||||
|
Binary file not shown.
@ -23,6 +23,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "0.1.18"
|
||||
}
|
||||
"version": "0.1.19"
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
// State management for Pynecone web apps.
|
||||
import io from 'socket.io-client';
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
|
||||
// Global variable to hold the token.
|
||||
let token;
|
||||
@ -103,12 +104,19 @@ export const applyEvent = async (event, router, socket) => {
|
||||
* Process an event off the event queue.
|
||||
* @param state The state with the event queue.
|
||||
* @param setState The function to set the state.
|
||||
* @param result The current result
|
||||
* @param result The current result.
|
||||
* @param setResult The function to set the result.
|
||||
* @param router The router object.
|
||||
* @param socket The socket object to send the event on.
|
||||
*/
|
||||
export const updateState = async (state, setState, result, setResult, router, socket) => {
|
||||
export const updateState = async (
|
||||
state,
|
||||
setState,
|
||||
result,
|
||||
setResult,
|
||||
router,
|
||||
socket
|
||||
) => {
|
||||
// If we are already processing an event, or there are no events to process, return.
|
||||
if (result.processing || state.events.length == 0) {
|
||||
return;
|
||||
@ -118,7 +126,7 @@ export const updateState = async (state, setState, result, setResult, router, so
|
||||
setResult({ ...result, processing: true });
|
||||
|
||||
// Pop the next event off the queue and apply it.
|
||||
const event = state.events.shift()
|
||||
const event = state.events.shift();
|
||||
|
||||
// Set new events to avoid reprocessing the same event.
|
||||
setState({ ...state, events: state.events });
|
||||
@ -127,7 +135,7 @@ export const updateState = async (state, setState, result, setResult, router, so
|
||||
const eventSent = await applyEvent(event, router, socket);
|
||||
if (!eventSent) {
|
||||
// If no event was sent, set processing to false and return.
|
||||
setResult({...state, processing: false})
|
||||
setResult({ ...state, processing: false });
|
||||
}
|
||||
};
|
||||
|
||||
@ -136,26 +144,37 @@ export const updateState = async (state, setState, result, setResult, router, so
|
||||
* @param socket The socket object to connect.
|
||||
* @param state The state object to apply the deltas to.
|
||||
* @param setState The function to set the state.
|
||||
* @param result The current result.
|
||||
* @param setResult The function to set the result.
|
||||
* @param endpoint The endpoint to connect to.
|
||||
* @param transports The transports to use.
|
||||
*/
|
||||
export const connect = async (socket, state, setState, result, setResult, router, endpoint, transports) => {
|
||||
export const connect = async (
|
||||
socket,
|
||||
state,
|
||||
setState,
|
||||
result,
|
||||
setResult,
|
||||
router,
|
||||
endpoint,
|
||||
transports
|
||||
) => {
|
||||
// Get backend URL object from the endpoint
|
||||
const endpoint_url = new URL(endpoint)
|
||||
const endpoint_url = new URL(endpoint);
|
||||
// Create the socket.
|
||||
socket.current = io(endpoint, {
|
||||
path: endpoint_url['pathname'],
|
||||
path: endpoint_url["pathname"],
|
||||
transports: transports,
|
||||
autoUnref: false,
|
||||
});
|
||||
|
||||
// Once the socket is open, hydrate the page.
|
||||
socket.current.on('connect', () => {
|
||||
socket.current.on("connect", () => {
|
||||
updateState(state, setState, result, setResult, router, socket.current);
|
||||
});
|
||||
|
||||
// On each received message, apply the delta and set the result.
|
||||
socket.current.on('event', function (update) {
|
||||
socket.current.on("event", function (update) {
|
||||
update = JSON.parse(update);
|
||||
applyDelta(state, update.delta);
|
||||
setResult({
|
||||
@ -166,6 +185,56 @@ export const connect = async (socket, state, setState, result, setResult, router
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload files to the server.
|
||||
*
|
||||
* @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 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;
|
||||
}
|
||||
|
||||
// Set processing to true to block other events from being processed.
|
||||
setResult({ ...result, processing: true });
|
||||
|
||||
// Currently only supports uploading one file.
|
||||
const file = files[0];
|
||||
const headers = {
|
||||
"Content-Type": file.type,
|
||||
};
|
||||
const formdata = new FormData();
|
||||
|
||||
// Add the token and handler to the file name.
|
||||
formdata.append("file", file, getToken() + ":" + handler + ":" + file.name);
|
||||
|
||||
// Send the file to the server.
|
||||
await axios.post(endpoint, formdata, headers).then((response) => {
|
||||
// Apply the delta and set the result.
|
||||
const update = response.data;
|
||||
applyDelta(state, update.delta);
|
||||
|
||||
// Set processing to false and return.
|
||||
setResult({
|
||||
processing: false,
|
||||
state: state,
|
||||
events: update.events,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an event object.
|
||||
* @param name The name of the event.
|
||||
|
@ -3,14 +3,21 @@
|
||||
Anything imported here will be available in the default Pynecone import as `pc.*`.
|
||||
"""
|
||||
|
||||
from .app import App
|
||||
from .app import App, UploadFile
|
||||
from .base import Base
|
||||
from .components import *
|
||||
from .components.component import custom_component as component
|
||||
from .components.graphing.victory import data
|
||||
from .config import Config
|
||||
from .constants import Env, Transports
|
||||
from .event import EVENT_ARG, EventChain, console_log, redirect, window_alert
|
||||
from .event import (
|
||||
EVENT_ARG,
|
||||
EventChain,
|
||||
console_log,
|
||||
redirect,
|
||||
window_alert,
|
||||
)
|
||||
from .event import FileUpload as upload_files
|
||||
from .middleware import Middleware
|
||||
from .model import Model, session
|
||||
from .route import route
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, UploadFile
|
||||
from fastapi.middleware import cors
|
||||
from socketio import ASGIApp, AsyncNamespace, AsyncServer
|
||||
|
||||
@ -124,6 +124,9 @@ class App(Base):
|
||||
# To test the server.
|
||||
self.api.get(str(constants.Endpoint.PING))(ping)
|
||||
|
||||
# To upload files.
|
||||
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
||||
|
||||
def add_cors(self):
|
||||
"""Add CORS middleware to the app."""
|
||||
self.api.add_middleware(
|
||||
@ -131,6 +134,7 @@ class App(Base):
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_origins=["*"],
|
||||
)
|
||||
|
||||
def preprocess(self, state: State, event: Event) -> Optional[Delta]:
|
||||
@ -428,6 +432,38 @@ async def ping() -> str:
|
||||
return "pong"
|
||||
|
||||
|
||||
def upload(app: App):
|
||||
"""Upload a file.
|
||||
|
||||
Args:
|
||||
app: The app to upload the file for.
|
||||
|
||||
Returns:
|
||||
The upload function.
|
||||
"""
|
||||
|
||||
async def upload_file(file: UploadFile):
|
||||
"""Upload a file.
|
||||
|
||||
Args:
|
||||
file: The file to upload.
|
||||
|
||||
Returns:
|
||||
The state update after processing the event.
|
||||
"""
|
||||
# Get the token and filename.
|
||||
token, handler, filename = file.filename.split(":", 2)
|
||||
file.filename = filename
|
||||
|
||||
# Get the state for the session.
|
||||
state = app.state_manager.get_state(token)
|
||||
event = Event(token=token, name=handler, payload={"file": file})
|
||||
update = await state.process(event)
|
||||
return update
|
||||
|
||||
return upload_file
|
||||
|
||||
|
||||
class EventNamespace(AsyncNamespace):
|
||||
"""The event namespace."""
|
||||
|
||||
|
@ -15,7 +15,7 @@ from pynecone.style import Style
|
||||
DEFAULT_IMPORTS: ImportDict = {
|
||||
"react": {"useEffect", "useRef", "useState"},
|
||||
"next/router": {"useRouter"},
|
||||
f"/{constants.STATE_PATH}": {"connect", "updateState", "E"},
|
||||
f"/{constants.STATE_PATH}": {"connect", "updateState", "uploadFiles", "E"},
|
||||
"": {"focus-visible/dist/focus-visible"},
|
||||
"@chakra-ui/react": {constants.USE_COLOR_MODE},
|
||||
}
|
||||
|
@ -146,6 +146,14 @@ EVENT_FN = join(
|
||||
"}})",
|
||||
]
|
||||
).format
|
||||
UPLOAD_FN = join(
|
||||
[
|
||||
"const File = files => {set_state}({{",
|
||||
" ...{state},",
|
||||
" files,",
|
||||
"}})",
|
||||
]
|
||||
).format
|
||||
|
||||
|
||||
# Effects.
|
||||
|
@ -85,9 +85,11 @@ def compile_constants() -> str:
|
||||
Returns:
|
||||
A string of all the compiled constants.
|
||||
"""
|
||||
endpoint = constants.Endpoint.EVENT
|
||||
return templates.join(
|
||||
[compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())]
|
||||
[
|
||||
compile_constant_declaration(name=endpoint.name, value=endpoint.get_url())
|
||||
for endpoint in constants.Endpoint
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@ -104,6 +106,7 @@ def compile_state(state: Type[State]) -> str:
|
||||
initial_state.update(
|
||||
{
|
||||
"events": [{"name": utils.get_hydrate_event(state)}],
|
||||
"files": [],
|
||||
}
|
||||
)
|
||||
initial_state = utils.format_state(initial_state)
|
||||
@ -137,7 +140,12 @@ def compile_events(state: Type[State]) -> str:
|
||||
"""
|
||||
state_name = state.get_name()
|
||||
state_setter = templates.format_state_setter(state_name)
|
||||
return templates.EVENT_FN(state=state_name, set_state=state_setter)
|
||||
return templates.join(
|
||||
[
|
||||
templates.EVENT_FN(state=state_name, set_state=state_setter),
|
||||
templates.UPLOAD_FN(state=state_name, set_state=state_setter),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def compile_effects(state: Type[State]) -> str:
|
||||
|
@ -112,6 +112,7 @@ slider_thumb = SliderThumb.create
|
||||
slider_track = SliderTrack.create
|
||||
switch = Switch.create
|
||||
text_area = TextArea.create
|
||||
upload = Upload.create
|
||||
area = Area.create
|
||||
bar = Bar.create
|
||||
box_plot = BoxPlot.create
|
||||
|
@ -50,6 +50,9 @@ class Component(Base, ABC):
|
||||
# The class name for the component.
|
||||
class_name: Any = None
|
||||
|
||||
# Special component props.
|
||||
special_props: Set[Var] = set()
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
"""Set default properties.
|
||||
@ -290,7 +293,7 @@ class Component(Base, ABC):
|
||||
# Create the base tag.
|
||||
alias = self.get_alias()
|
||||
name = alias if alias is not None else self.tag
|
||||
tag = Tag(name=name)
|
||||
tag = Tag(name=name, special_props=self.special_props)
|
||||
|
||||
# Add component props to the tag.
|
||||
props = {attr: getattr(self, attr) for attr in self.get_props()}
|
||||
|
@ -27,5 +27,6 @@ from .select import Option, Select
|
||||
from .slider import Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack
|
||||
from .switch import Switch
|
||||
from .textarea import TextArea
|
||||
from .upload import Upload
|
||||
|
||||
__all__ = [f for f in dir() if f[0].isupper()] # type: ignore
|
||||
|
57
pynecone/components/forms/upload.py
Normal file
57
pynecone/components/forms/upload.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""A file upload component."""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from pynecone.components.component import EVENT_ARG, Component
|
||||
from pynecone.components.forms.input import Input
|
||||
from pynecone.components.layout.box import Box
|
||||
from pynecone.event import EventChain
|
||||
from pynecone.var import BaseVar, Var
|
||||
|
||||
upload_file = BaseVar(name="e => File(e)", type_=EventChain)
|
||||
|
||||
|
||||
class Upload(Component):
|
||||
"""A file upload component."""
|
||||
|
||||
library = "react-dropzone"
|
||||
|
||||
tag = "ReactDropzone"
|
||||
|
||||
@classmethod
|
||||
def create(cls, *children, **props) -> Component:
|
||||
"""Create an upload component.
|
||||
|
||||
Args:
|
||||
children: The children of the component.
|
||||
props: The properties of the component.
|
||||
|
||||
Returns:
|
||||
The upload component.
|
||||
"""
|
||||
# The file input to use.
|
||||
upload = Input.create(type_="file")
|
||||
upload.special_props = {BaseVar(name="{...getInputProps()}", type_=None)}
|
||||
|
||||
# The dropzone to use.
|
||||
zone = Box.create(upload, *children, **props)
|
||||
zone.special_props = {BaseVar(name="{...getRootProps()}", type_=None)}
|
||||
|
||||
# Create the component.
|
||||
return super().create(zone, on_drop=upload_file)
|
||||
|
||||
@classmethod
|
||||
def get_controlled_triggers(cls) -> Dict[str, Var]:
|
||||
"""Get the event triggers that pass the component's value to the handler.
|
||||
|
||||
Returns:
|
||||
A dict mapping the event trigger to the var that is passed to the handler.
|
||||
"""
|
||||
return {
|
||||
"on_drop": EVENT_ARG,
|
||||
}
|
||||
|
||||
def _render(self):
|
||||
out = super()._render()
|
||||
out.args = ("getRootProps", "getInputProps")
|
||||
return out
|
@ -1,4 +1,4 @@
|
||||
"""An image component."""
|
||||
"""An icon component."""
|
||||
|
||||
from pynecone import utils
|
||||
from pynecone.components.component import Component
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Tuple, Union
|
||||
|
||||
from plotly.graph_objects import Figure
|
||||
from plotly.io import to_json
|
||||
@ -31,6 +31,12 @@ class Tag(Base):
|
||||
# The inner contents of the tag.
|
||||
contents: str = ""
|
||||
|
||||
# Args to pass to the tag.
|
||||
args: Optional[Tuple[str, ...]] = None
|
||||
|
||||
# Special props that aren't key value pairs.
|
||||
special_props: Set[Var] = set()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the tag.
|
||||
|
||||
@ -68,8 +74,15 @@ class Tag(Base):
|
||||
# Handle event props.
|
||||
elif isinstance(prop, EventChain):
|
||||
local_args = ",".join(prop.events[0].local_args)
|
||||
events = ",".join([utils.format_event(event) for event in prop.events])
|
||||
prop = f"({local_args}) => Event([{events}])"
|
||||
|
||||
if len(prop.events) == 1 and prop.events[0].upload:
|
||||
# Special case for upload events.
|
||||
event = utils.format_upload_event(prop.events[0])
|
||||
else:
|
||||
# All other events.
|
||||
chain = ",".join([utils.format_event(event) for event in prop.events])
|
||||
event = f"Event([{chain}])"
|
||||
prop = f"({local_args}) => {event}"
|
||||
|
||||
# Handle other types.
|
||||
elif isinstance(prop, str):
|
||||
@ -125,6 +138,11 @@ class Tag(Base):
|
||||
"""
|
||||
# Get the tag props.
|
||||
props_str = self.format_props()
|
||||
|
||||
# Add the special props.
|
||||
props_str += " ".join([str(prop) for prop in self.special_props])
|
||||
|
||||
# Add a space if there are props.
|
||||
if len(props_str) > 0:
|
||||
props_str = " " + props_str
|
||||
|
||||
@ -132,10 +150,16 @@ class Tag(Base):
|
||||
# If there is no inner content, we don't need a closing tag.
|
||||
tag_str = utils.wrap(f"{self.name}{props_str}/", "<")
|
||||
else:
|
||||
if self.args is not None:
|
||||
# If there are args, wrap the tag in a function call.
|
||||
args_str = ", ".join(self.args)
|
||||
contents = f"{{({{{args_str}}}) => ({self.contents})}}"
|
||||
else:
|
||||
contents = self.contents
|
||||
# Otherwise wrap it in opening and closing tags.
|
||||
open = utils.wrap(f"{self.name}{props_str}", "<")
|
||||
close = utils.wrap(f"/{self.name}", "<")
|
||||
tag_str = utils.wrap(self.contents, open, close)
|
||||
tag_str = utils.wrap(contents, open, close)
|
||||
|
||||
return tag_str
|
||||
|
||||
|
@ -168,6 +168,7 @@ class Endpoint(Enum):
|
||||
|
||||
PING = "ping"
|
||||
EVENT = "event"
|
||||
UPLOAD = "upload"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Get the string representation of the endpoint.
|
||||
|
@ -62,6 +62,9 @@ class EventHandler(Base):
|
||||
values.append(arg.full_name)
|
||||
continue
|
||||
|
||||
if isinstance(arg, FileUpload):
|
||||
return EventSpec(handler=self, upload=True)
|
||||
|
||||
# Otherwise, convert to JSON.
|
||||
try:
|
||||
values.append(json.dumps(arg, ensure_ascii=False))
|
||||
@ -91,6 +94,9 @@ class EventSpec(Base):
|
||||
# The arguments to pass to the function.
|
||||
args: Tuple[Any, ...] = ()
|
||||
|
||||
# Whether to upload files.
|
||||
upload: bool = False
|
||||
|
||||
class Config:
|
||||
"""The Pydantic config."""
|
||||
|
||||
@ -122,6 +128,12 @@ class FrontendEvent(Base):
|
||||
EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
|
||||
|
||||
|
||||
class FileUpload(Base):
|
||||
"""Class to represent a file upload."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Special server-side events.
|
||||
def redirect(path: str) -> EventSpec:
|
||||
"""Redirect to a new path.
|
||||
|
@ -1138,21 +1138,21 @@ def format_cond(
|
||||
return expr
|
||||
|
||||
|
||||
def format_event_handler(handler: EventHandler) -> str:
|
||||
"""Format an event handler.
|
||||
def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
|
||||
"""Get the state and function name of an event handler.
|
||||
|
||||
Args:
|
||||
handler: The event handler to format.
|
||||
handler: The event handler to get the parts of.
|
||||
|
||||
Returns:
|
||||
The formatted function.
|
||||
The state and function name.
|
||||
"""
|
||||
# Get the class that defines the event handler.
|
||||
parts = handler.fn.__qualname__.split(".")
|
||||
|
||||
# If there's no enclosing class, just return the function name.
|
||||
if len(parts) == 1:
|
||||
return parts[-1]
|
||||
return ("", parts[-1])
|
||||
|
||||
# Get the state and the function name.
|
||||
state_name, name = parts[-2:]
|
||||
@ -1163,8 +1163,24 @@ def format_event_handler(handler: EventHandler) -> str:
|
||||
state = vars(sys.modules[handler.fn.__module__])[state_name]
|
||||
except Exception:
|
||||
# If the state isn't in the module, just return the function name.
|
||||
return handler.fn.__qualname__
|
||||
return ".".join([state.get_full_name(), name])
|
||||
return ("", handler.fn.__qualname__)
|
||||
|
||||
return (state.get_full_name(), name)
|
||||
|
||||
|
||||
def format_event_handler(handler: EventHandler) -> str:
|
||||
"""Format an event handler.
|
||||
|
||||
Args:
|
||||
handler: The event handler to format.
|
||||
|
||||
Returns:
|
||||
The formatted function.
|
||||
"""
|
||||
state, name = get_event_handler_parts(handler)
|
||||
if state == "":
|
||||
return name
|
||||
return f"{state}.{name}"
|
||||
|
||||
|
||||
def format_event(event_spec: EventSpec) -> str:
|
||||
@ -1180,6 +1196,21 @@ def format_event(event_spec: EventSpec) -> str:
|
||||
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.
|
||||
"""
|
||||
from pynecone.compiler import templates
|
||||
|
||||
state, name = get_event_handler_parts(event_spec.handler)
|
||||
return f'uploadFiles({state}, {templates.RESULT}, {templates.SET_RESULT}, {state}.files, "{name}", UPLOAD)'
|
||||
|
||||
|
||||
def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Convert back query params name to python-friendly case.
|
||||
|
||||
@ -1193,6 +1224,7 @@ def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
return {k.replace("-", "_"): v for k, v in params.items()}
|
||||
|
||||
|
||||
# Set of unique variable names.
|
||||
USED_VARIABLES = set()
|
||||
|
||||
|
||||
|
@ -38,6 +38,7 @@ python-socketio = "^5.7.2"
|
||||
psutil = "^5.9.4"
|
||||
websockets = "^10.4"
|
||||
cloudpickle = "^2.2.1"
|
||||
python-multipart = "^0.0.5"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.1.2"
|
||||
|
Loading…
Reference in New Issue
Block a user