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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
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\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
|
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
|
||||||
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
|
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -648,14 +648,14 @@ dev = ["twine (>=3.4.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.2.1"
|
version = "7.2.2"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
{file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
|
||||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
{file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -725,6 +725,20 @@ files = [
|
|||||||
asyncio-client = ["aiohttp (>=3.4)"]
|
asyncio-client = ["aiohttp (>=3.4)"]
|
||||||
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
|
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]]
|
[[package]]
|
||||||
name = "python-socketio"
|
name = "python-socketio"
|
||||||
version = "5.7.2"
|
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 = ["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"]
|
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]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -1210,4 +1236,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
content-hash = "13e3d8aa740b5a1b24ec4cdd394791f1650ac3bcb618e4430f1620238c6e8a6d"
|
content-hash = "b7272a6016a5b9fb3eea7ce834b9f539919e8f71c7f849d626adb2ee7a354d2f"
|
||||||
|
Binary file not shown.
@ -23,6 +23,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-plotly.js": "^2.6.0",
|
"react-plotly.js": "^2.6.0",
|
||||||
"react-syntax-highlighter": "^15.5.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.
|
// 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.
|
// Global variable to hold the token.
|
||||||
let token;
|
let token;
|
||||||
@ -103,12 +104,19 @@ export const applyEvent = async (event, router, socket) => {
|
|||||||
* Process an event off the event queue.
|
* Process an event off the event queue.
|
||||||
* @param state The state with the event queue.
|
* @param state The state with the event queue.
|
||||||
* @param setState The function to set the state.
|
* @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 setResult The function to set the result.
|
||||||
* @param router The router object.
|
* @param router The router object.
|
||||||
* @param socket The socket object to send the event on.
|
* @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 we are already processing an event, or there are no events to process, return.
|
||||||
if (result.processing || state.events.length == 0) {
|
if (result.processing || state.events.length == 0) {
|
||||||
return;
|
return;
|
||||||
@ -118,7 +126,7 @@ export const updateState = async (state, setState, result, setResult, router, so
|
|||||||
setResult({ ...result, processing: true });
|
setResult({ ...result, processing: true });
|
||||||
|
|
||||||
// Pop the next event off the queue and apply it.
|
// 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.
|
// Set new events to avoid reprocessing the same event.
|
||||||
setState({ ...state, events: state.events });
|
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);
|
const eventSent = await applyEvent(event, router, socket);
|
||||||
if (!eventSent) {
|
if (!eventSent) {
|
||||||
// If no event was sent, set processing to false and return.
|
// 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 socket The socket object to connect.
|
||||||
* @param state The state object to apply the deltas to.
|
* @param state The state object to apply the deltas to.
|
||||||
* @param setState The function to set the state.
|
* @param setState The function to set the state.
|
||||||
|
* @param result The current result.
|
||||||
* @param setResult The function to set the result.
|
* @param setResult The function to set the result.
|
||||||
* @param endpoint The endpoint to connect to.
|
* @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
|
// Get backend URL object from the endpoint
|
||||||
const endpoint_url = new URL(endpoint)
|
const endpoint_url = new URL(endpoint);
|
||||||
// Create the socket.
|
// Create the socket.
|
||||||
socket.current = io(endpoint, {
|
socket.current = io(endpoint, {
|
||||||
path: endpoint_url['pathname'],
|
path: endpoint_url["pathname"],
|
||||||
transports: transports,
|
transports: transports,
|
||||||
autoUnref: false,
|
autoUnref: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Once the socket is open, hydrate the page.
|
// Once the socket is open, hydrate the page.
|
||||||
socket.current.on('connect', () => {
|
socket.current.on("connect", () => {
|
||||||
updateState(state, setState, result, setResult, router, socket.current);
|
updateState(state, setState, result, setResult, router, socket.current);
|
||||||
});
|
});
|
||||||
|
|
||||||
// On each received message, apply the delta and set the result.
|
// 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);
|
update = JSON.parse(update);
|
||||||
applyDelta(state, update.delta);
|
applyDelta(state, update.delta);
|
||||||
setResult({
|
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.
|
* Create an event object.
|
||||||
* @param name The name of the event.
|
* @param name The name of the event.
|
||||||
|
@ -3,14 +3,21 @@
|
|||||||
Anything imported here will be available in the default Pynecone import as `pc.*`.
|
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 .base import Base
|
||||||
from .components import *
|
from .components import *
|
||||||
from .components.component import custom_component as component
|
from .components.component import custom_component as component
|
||||||
from .components.graphing.victory import data
|
from .components.graphing.victory import data
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .constants import Env, Transports
|
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 .middleware import Middleware
|
||||||
from .model import Model, session
|
from .model import Model, session
|
||||||
from .route import route
|
from .route import route
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union
|
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 fastapi.middleware import cors
|
||||||
from socketio import ASGIApp, AsyncNamespace, AsyncServer
|
from socketio import ASGIApp, AsyncNamespace, AsyncServer
|
||||||
|
|
||||||
@ -124,6 +124,9 @@ class App(Base):
|
|||||||
# To test the server.
|
# To test the server.
|
||||||
self.api.get(str(constants.Endpoint.PING))(ping)
|
self.api.get(str(constants.Endpoint.PING))(ping)
|
||||||
|
|
||||||
|
# To upload files.
|
||||||
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
||||||
|
|
||||||
def add_cors(self):
|
def add_cors(self):
|
||||||
"""Add CORS middleware to the app."""
|
"""Add CORS middleware to the app."""
|
||||||
self.api.add_middleware(
|
self.api.add_middleware(
|
||||||
@ -131,6 +134,7 @@ class App(Base):
|
|||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
allow_origins=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def preprocess(self, state: State, event: Event) -> Optional[Delta]:
|
def preprocess(self, state: State, event: Event) -> Optional[Delta]:
|
||||||
@ -428,6 +432,38 @@ async def ping() -> str:
|
|||||||
return "pong"
|
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):
|
class EventNamespace(AsyncNamespace):
|
||||||
"""The event namespace."""
|
"""The event namespace."""
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from pynecone.style import Style
|
|||||||
DEFAULT_IMPORTS: ImportDict = {
|
DEFAULT_IMPORTS: ImportDict = {
|
||||||
"react": {"useEffect", "useRef", "useState"},
|
"react": {"useEffect", "useRef", "useState"},
|
||||||
"next/router": {"useRouter"},
|
"next/router": {"useRouter"},
|
||||||
f"/{constants.STATE_PATH}": {"connect", "updateState", "E"},
|
f"/{constants.STATE_PATH}": {"connect", "updateState", "uploadFiles", "E"},
|
||||||
"": {"focus-visible/dist/focus-visible"},
|
"": {"focus-visible/dist/focus-visible"},
|
||||||
"@chakra-ui/react": {constants.USE_COLOR_MODE},
|
"@chakra-ui/react": {constants.USE_COLOR_MODE},
|
||||||
}
|
}
|
||||||
|
@ -146,6 +146,14 @@ EVENT_FN = join(
|
|||||||
"}})",
|
"}})",
|
||||||
]
|
]
|
||||||
).format
|
).format
|
||||||
|
UPLOAD_FN = join(
|
||||||
|
[
|
||||||
|
"const File = files => {set_state}({{",
|
||||||
|
" ...{state},",
|
||||||
|
" files,",
|
||||||
|
"}})",
|
||||||
|
]
|
||||||
|
).format
|
||||||
|
|
||||||
|
|
||||||
# Effects.
|
# Effects.
|
||||||
|
@ -85,9 +85,11 @@ def compile_constants() -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
A string of all the compiled constants.
|
A string of all the compiled constants.
|
||||||
"""
|
"""
|
||||||
endpoint = constants.Endpoint.EVENT
|
|
||||||
return templates.join(
|
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(
|
initial_state.update(
|
||||||
{
|
{
|
||||||
"events": [{"name": utils.get_hydrate_event(state)}],
|
"events": [{"name": utils.get_hydrate_event(state)}],
|
||||||
|
"files": [],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
initial_state = utils.format_state(initial_state)
|
initial_state = utils.format_state(initial_state)
|
||||||
@ -137,7 +140,12 @@ def compile_events(state: Type[State]) -> str:
|
|||||||
"""
|
"""
|
||||||
state_name = state.get_name()
|
state_name = state.get_name()
|
||||||
state_setter = templates.format_state_setter(state_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:
|
def compile_effects(state: Type[State]) -> str:
|
||||||
|
@ -112,6 +112,7 @@ slider_thumb = SliderThumb.create
|
|||||||
slider_track = SliderTrack.create
|
slider_track = SliderTrack.create
|
||||||
switch = Switch.create
|
switch = Switch.create
|
||||||
text_area = TextArea.create
|
text_area = TextArea.create
|
||||||
|
upload = Upload.create
|
||||||
area = Area.create
|
area = Area.create
|
||||||
bar = Bar.create
|
bar = Bar.create
|
||||||
box_plot = BoxPlot.create
|
box_plot = BoxPlot.create
|
||||||
|
@ -50,6 +50,9 @@ class Component(Base, ABC):
|
|||||||
# The class name for the component.
|
# The class name for the component.
|
||||||
class_name: Any = None
|
class_name: Any = None
|
||||||
|
|
||||||
|
# Special component props.
|
||||||
|
special_props: Set[Var] = set()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
"""Set default properties.
|
"""Set default properties.
|
||||||
@ -290,7 +293,7 @@ class Component(Base, ABC):
|
|||||||
# Create the base tag.
|
# Create the base tag.
|
||||||
alias = self.get_alias()
|
alias = self.get_alias()
|
||||||
name = alias if alias is not None else self.tag
|
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.
|
# Add component props to the tag.
|
||||||
props = {attr: getattr(self, attr) for attr in self.get_props()}
|
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 .slider import Slider, SliderFilledTrack, SliderMark, SliderThumb, SliderTrack
|
||||||
from .switch import Switch
|
from .switch import Switch
|
||||||
from .textarea import TextArea
|
from .textarea import TextArea
|
||||||
|
from .upload import Upload
|
||||||
|
|
||||||
__all__ = [f for f in dir() if f[0].isupper()] # type: ignore
|
__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 import utils
|
||||||
from pynecone.components.component import Component
|
from pynecone.components.component import Component
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
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.graph_objects import Figure
|
||||||
from plotly.io import to_json
|
from plotly.io import to_json
|
||||||
@ -31,6 +31,12 @@ class Tag(Base):
|
|||||||
# The inner contents of the tag.
|
# The inner contents of the tag.
|
||||||
contents: str = ""
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the tag.
|
"""Initialize the tag.
|
||||||
|
|
||||||
@ -68,8 +74,15 @@ class Tag(Base):
|
|||||||
# Handle event props.
|
# Handle event props.
|
||||||
elif isinstance(prop, EventChain):
|
elif isinstance(prop, EventChain):
|
||||||
local_args = ",".join(prop.events[0].local_args)
|
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.
|
# Handle other types.
|
||||||
elif isinstance(prop, str):
|
elif isinstance(prop, str):
|
||||||
@ -125,6 +138,11 @@ class Tag(Base):
|
|||||||
"""
|
"""
|
||||||
# Get the tag props.
|
# Get the tag props.
|
||||||
props_str = self.format_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:
|
if len(props_str) > 0:
|
||||||
props_str = " " + props_str
|
props_str = " " + props_str
|
||||||
|
|
||||||
@ -132,10 +150,16 @@ class Tag(Base):
|
|||||||
# If there is no inner content, we don't need a closing tag.
|
# If there is no inner content, we don't need a closing tag.
|
||||||
tag_str = utils.wrap(f"{self.name}{props_str}/", "<")
|
tag_str = utils.wrap(f"{self.name}{props_str}/", "<")
|
||||||
else:
|
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.
|
# Otherwise wrap it in opening and closing tags.
|
||||||
open = utils.wrap(f"{self.name}{props_str}", "<")
|
open = utils.wrap(f"{self.name}{props_str}", "<")
|
||||||
close = utils.wrap(f"/{self.name}", "<")
|
close = utils.wrap(f"/{self.name}", "<")
|
||||||
tag_str = utils.wrap(self.contents, open, close)
|
tag_str = utils.wrap(contents, open, close)
|
||||||
|
|
||||||
return tag_str
|
return tag_str
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ class Endpoint(Enum):
|
|||||||
|
|
||||||
PING = "ping"
|
PING = "ping"
|
||||||
EVENT = "event"
|
EVENT = "event"
|
||||||
|
UPLOAD = "upload"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Get the string representation of the endpoint.
|
"""Get the string representation of the endpoint.
|
||||||
|
@ -62,6 +62,9 @@ class EventHandler(Base):
|
|||||||
values.append(arg.full_name)
|
values.append(arg.full_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if isinstance(arg, FileUpload):
|
||||||
|
return EventSpec(handler=self, upload=True)
|
||||||
|
|
||||||
# Otherwise, convert to JSON.
|
# Otherwise, convert to JSON.
|
||||||
try:
|
try:
|
||||||
values.append(json.dumps(arg, ensure_ascii=False))
|
values.append(json.dumps(arg, ensure_ascii=False))
|
||||||
@ -91,6 +94,9 @@ class EventSpec(Base):
|
|||||||
# The arguments to pass to the function.
|
# The arguments to pass to the function.
|
||||||
args: Tuple[Any, ...] = ()
|
args: Tuple[Any, ...] = ()
|
||||||
|
|
||||||
|
# Whether to upload files.
|
||||||
|
upload: bool = False
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""The Pydantic config."""
|
"""The Pydantic config."""
|
||||||
|
|
||||||
@ -122,6 +128,12 @@ class FrontendEvent(Base):
|
|||||||
EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
|
EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FileUpload(Base):
|
||||||
|
"""Class to represent a file upload."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Special server-side events.
|
# Special server-side events.
|
||||||
def redirect(path: str) -> EventSpec:
|
def redirect(path: str) -> EventSpec:
|
||||||
"""Redirect to a new path.
|
"""Redirect to a new path.
|
||||||
|
@ -1138,21 +1138,21 @@ def format_cond(
|
|||||||
return expr
|
return expr
|
||||||
|
|
||||||
|
|
||||||
def format_event_handler(handler: EventHandler) -> str:
|
def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
|
||||||
"""Format an event handler.
|
"""Get the state and function name of an event handler.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handler: The event handler to format.
|
handler: The event handler to get the parts of.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The formatted function.
|
The state and function name.
|
||||||
"""
|
"""
|
||||||
# Get the class that defines the event handler.
|
# Get the class that defines the event handler.
|
||||||
parts = handler.fn.__qualname__.split(".")
|
parts = handler.fn.__qualname__.split(".")
|
||||||
|
|
||||||
# If there's no enclosing class, just return the function name.
|
# If there's no enclosing class, just return the function name.
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return parts[-1]
|
return ("", parts[-1])
|
||||||
|
|
||||||
# Get the state and the function name.
|
# Get the state and the function name.
|
||||||
state_name, name = parts[-2:]
|
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]
|
state = vars(sys.modules[handler.fn.__module__])[state_name]
|
||||||
except Exception:
|
except Exception:
|
||||||
# If the state isn't in the module, just return the function name.
|
# If the state isn't in the module, just return the function name.
|
||||||
return handler.fn.__qualname__
|
return ("", handler.fn.__qualname__)
|
||||||
return ".".join([state.get_full_name(), name])
|
|
||||||
|
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:
|
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, '{')})"
|
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]:
|
def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
|
||||||
"""Convert back query params name to python-friendly case.
|
"""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()}
|
return {k.replace("-", "_"): v for k, v in params.items()}
|
||||||
|
|
||||||
|
|
||||||
|
# Set of unique variable names.
|
||||||
USED_VARIABLES = set()
|
USED_VARIABLES = set()
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ python-socketio = "^5.7.2"
|
|||||||
psutil = "^5.9.4"
|
psutil = "^5.9.4"
|
||||||
websockets = "^10.4"
|
websockets = "^10.4"
|
||||||
cloudpickle = "^2.2.1"
|
cloudpickle = "^2.2.1"
|
||||||
|
python-multipart = "^0.0.5"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^7.1.2"
|
pytest = "^7.1.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user