Compare commits
20 Commits
main
...
release/re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
49caf9d507 | ||
![]() |
9d9dc5291b | ||
![]() |
7c81dcaa8b | ||
![]() |
5f0aa0c4e0 | ||
![]() |
dc997786aa | ||
![]() |
ecbf63406a | ||
![]() |
da4599fdde | ||
![]() |
3219157476 | ||
![]() |
0c8192222f | ||
![]() |
9c9602363c | ||
![]() |
27bad2575e | ||
![]() |
3a7497c852 | ||
![]() |
77594bd0ea | ||
![]() |
e4ccba7aee | ||
![]() |
09b2d92466 | ||
![]() |
cb087acbeb | ||
![]() |
7129bfb513 | ||
![]() |
b41b1f364a | ||
![]() |
9ebf16c140 | ||
![]() |
6d0fae36e6 |
29
.github/workflows/integration_tests.yml
vendored
29
.github/workflows/integration_tests.yml
vendored
@ -162,7 +162,36 @@ jobs:
|
|||||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||||
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
|
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
|
||||||
--app-name "reflex-web" --path ./reflex-web/.web
|
--app-name "reflex-web" --path ./reflex-web/.web
|
||||||
|
|
||||||
|
rx-shout-from-template:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/setup_build_env
|
||||||
|
with:
|
||||||
|
python-version: '3.11.4'
|
||||||
|
run-poetry-install: true
|
||||||
|
create-venv-at-path: .venv
|
||||||
|
- name: Create app directory
|
||||||
|
run: mkdir rx-shout-from-template
|
||||||
|
- name: Init reflex-web from template
|
||||||
|
run: poetry run reflex init --template https://github.com/masenf/rx_shout
|
||||||
|
working-directory: ./rx-shout-from-template
|
||||||
|
- name: ignore reflex pin in requirements
|
||||||
|
run: sed -i -e '/reflex==/d' requirements.txt
|
||||||
|
working-directory: ./rx-shout-from-template
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: poetry run uv pip install -r requirements.txt
|
||||||
|
working-directory: ./rx-shout-from-template
|
||||||
|
- name: Run Website and Check for errors
|
||||||
|
run: |
|
||||||
|
# Check that npm is home
|
||||||
|
npm -v
|
||||||
|
poetry run bash scripts/integration.sh ./rx-shout-from-template prod
|
||||||
|
|
||||||
|
|
||||||
reflex-web-macos:
|
reflex-web-macos:
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
strategy:
|
strategy:
|
||||||
|
16
poetry.lock
generated
16
poetry.lock
generated
@ -2206,13 +2206,13 @@ reflex = ">=0.6.0a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reflex-hosting-cli"
|
name = "reflex-hosting-cli"
|
||||||
version = "0.1.17"
|
version = "0.1.29"
|
||||||
description = "Reflex Hosting CLI"
|
description = "Reflex Hosting CLI"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.8"
|
python-versions = "<4.0,>=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "reflex_hosting_cli-0.1.17-py3-none-any.whl", hash = "sha256:cf1accec70745557a40125ffa2a8929e6ef9834808afe78e4f4a01933ac0cb67"},
|
{file = "reflex_hosting_cli-0.1.29-py3-none-any.whl", hash = "sha256:fcbdad829762287f32397cd8a5d46536ab0db396e7fdb8a23c7f9343d7dc8de0"},
|
||||||
{file = "reflex_hosting_cli-0.1.17.tar.gz", hash = "sha256:263d8dc217eb24d4198ac0bcfd710980bd7795d9818a5e522027657f94752710"},
|
{file = "reflex_hosting_cli-0.1.29.tar.gz", hash = "sha256:7b421fec6936c26549c8c65c9dda34fc042eaaec79b238dce6b9c020f848563b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2224,7 +2224,7 @@ pydantic = ">=1.10.2,<3.0"
|
|||||||
python-dateutil = ">=2.8.1"
|
python-dateutil = ">=2.8.1"
|
||||||
rich = ">=13.0.0,<14.0"
|
rich = ">=13.0.0,<14.0"
|
||||||
tabulate = ">=0.9.0,<0.10.0"
|
tabulate = ">=0.9.0,<0.10.0"
|
||||||
typer = ">=0.4.2,<1"
|
typer = ">=0.15.0,<1"
|
||||||
websockets = ">=10.4"
|
websockets = ">=10.4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2722,13 +2722,13 @@ urllib3 = ">=1.26.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.13.1"
|
version = "0.15.1"
|
||||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "typer-0.13.1-py3-none-any.whl", hash = "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157"},
|
{file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"},
|
||||||
{file = "typer-0.13.1.tar.gz", hash = "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c"},
|
{file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -3041,4 +3041,4 @@ type = ["pytest-mypy"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "8000601d48cfc1b10d0ae18c6046cc59a50cb6c45e6d3ef4775a3203769f2154"
|
content-hash = "3810e99ff4d09952e62d88b2c26651a0d8e0ffe4007bc3274c2fb83b68243951"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "reflex"
|
name = "reflex"
|
||||||
version = "0.6.6dev1"
|
version = "0.6.6.post3"
|
||||||
description = "Web apps in pure Python."
|
description = "Web apps in pure Python."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
authors = [
|
authors = [
|
||||||
@ -49,7 +49,7 @@ wrapt = [
|
|||||||
{version = ">=1.11.0,<2.0", python = "<3.11"},
|
{version = ">=1.11.0,<2.0", python = "<3.11"},
|
||||||
]
|
]
|
||||||
packaging = ">=23.1,<25.0"
|
packaging = ">=23.1,<25.0"
|
||||||
reflex-hosting-cli = ">=0.1.17,<2.0"
|
reflex-hosting-cli = ">=0.1.29,<2.0"
|
||||||
charset-normalizer = ">=3.3.2,<4.0"
|
charset-normalizer = ">=3.3.2,<4.0"
|
||||||
wheel = ">=0.42.0,<1.0"
|
wheel = ">=0.42.0,<1.0"
|
||||||
build = ">=1.0.3,<2.0"
|
build = ">=1.0.3,<2.0"
|
||||||
|
@ -454,6 +454,10 @@ export const connect = async (
|
|||||||
queueEvents(update.events, socket);
|
queueEvents(update.events, socket);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
socket.current.on("reload", async (event) => {
|
||||||
|
event_processing = false;
|
||||||
|
queueEvents([...initialEvents(), JSON5.parse(event)], socket);
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", checkVisibility);
|
document.addEventListener("visibilitychange", checkVisibility);
|
||||||
};
|
};
|
||||||
@ -486,23 +490,30 @@ export const uploadFiles = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track how many partial updates have been processed for this upload.
|
||||||
let resp_idx = 0;
|
let resp_idx = 0;
|
||||||
const eventHandler = (progressEvent) => {
|
const eventHandler = (progressEvent) => {
|
||||||
// handle any delta / event streamed from the upload event handler
|
const event_callbacks = socket._callbacks.$event;
|
||||||
|
// Whenever called, responseText will contain the entire response so far.
|
||||||
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
||||||
|
// So only process _new_ chunks beyond resp_idx.
|
||||||
chunks.slice(resp_idx).map((chunk) => {
|
chunks.slice(resp_idx).map((chunk) => {
|
||||||
try {
|
event_callbacks.map((f, ix) => {
|
||||||
socket._callbacks.$event.map((f) => {
|
f(chunk)
|
||||||
f(chunk);
|
.then(() => {
|
||||||
});
|
if (ix === event_callbacks.length - 1) {
|
||||||
resp_idx += 1;
|
// Mark this chunk as processed.
|
||||||
} catch (e) {
|
resp_idx += 1;
|
||||||
if (progressEvent.progress === 1) {
|
}
|
||||||
// Chunk may be incomplete, so only report errors when full response is available.
|
})
|
||||||
console.log("Error parsing chunk", chunk, e);
|
.catch((e) => {
|
||||||
}
|
if (progressEvent.progress === 1) {
|
||||||
return;
|
// Chunk may be incomplete, so only report errors when full response is available.
|
||||||
}
|
console.log("Error parsing chunk", chunk, e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -707,7 +718,7 @@ export const useEventLoop = (
|
|||||||
const combined_name = events.map((e) => e.name).join("+++");
|
const combined_name = events.map((e) => e.name).join("+++");
|
||||||
if (event_actions?.temporal) {
|
if (event_actions?.temporal) {
|
||||||
if (!socket.current || !socket.current.connected) {
|
if (!socket.current || !socket.current.connected) {
|
||||||
return; // don't queue when the backend is not connected
|
return; // don't queue when the backend is not connected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event_actions?.throttle) {
|
if (event_actions?.throttle) {
|
||||||
@ -848,7 +859,7 @@ export const useEventLoop = (
|
|||||||
if (router.components[router.pathname].error) {
|
if (router.components[router.pathname].error) {
|
||||||
delete router.components[router.pathname].error;
|
delete router.components[router.pathname].error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
router.events.on("routeChangeStart", change_start);
|
router.events.on("routeChangeStart", change_start);
|
||||||
router.events.on("routeChangeComplete", change_complete);
|
router.events.on("routeChangeComplete", change_complete);
|
||||||
router.events.on("routeChangeError", change_error);
|
router.events.on("routeChangeError", change_error);
|
||||||
|
@ -73,6 +73,7 @@ from reflex.event import (
|
|||||||
EventSpec,
|
EventSpec,
|
||||||
EventType,
|
EventType,
|
||||||
IndividualEventType,
|
IndividualEventType,
|
||||||
|
get_hydrate_event,
|
||||||
window_alert,
|
window_alert,
|
||||||
)
|
)
|
||||||
from reflex.model import Model, get_db_status
|
from reflex.model import Model, get_db_status
|
||||||
@ -1259,6 +1260,21 @@ async def process(
|
|||||||
)
|
)
|
||||||
# Get the state for the session exclusively.
|
# Get the state for the session exclusively.
|
||||||
async with app.state_manager.modify_state(event.substate_token) as state:
|
async with app.state_manager.modify_state(event.substate_token) as state:
|
||||||
|
# When this is a brand new instance of the state, signal the
|
||||||
|
# frontend to reload before processing it.
|
||||||
|
if (
|
||||||
|
not state.router_data
|
||||||
|
and event.name != get_hydrate_event(state)
|
||||||
|
and app.event_namespace is not None
|
||||||
|
):
|
||||||
|
await asyncio.create_task(
|
||||||
|
app.event_namespace.emit(
|
||||||
|
"reload",
|
||||||
|
data=format.json_dumps(event),
|
||||||
|
to=sid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
# re-assign only when the value is different
|
# re-assign only when the value is different
|
||||||
if state.router_data != router_data:
|
if state.router_data != router_data:
|
||||||
# assignment will recurse into substates and force recalculation of
|
# assignment will recurse into substates and force recalculation of
|
||||||
@ -1462,10 +1478,10 @@ class EventNamespace(AsyncNamespace):
|
|||||||
app: App
|
app: App
|
||||||
|
|
||||||
# Keep a mapping between socket ID and client token.
|
# Keep a mapping between socket ID and client token.
|
||||||
token_to_sid: dict[str, str] = {}
|
token_to_sid: dict[str, str]
|
||||||
|
|
||||||
# Keep a mapping between client token and socket ID.
|
# Keep a mapping between client token and socket ID.
|
||||||
sid_to_token: dict[str, str] = {}
|
sid_to_token: dict[str, str]
|
||||||
|
|
||||||
def __init__(self, namespace: str, app: App):
|
def __init__(self, namespace: str, app: App):
|
||||||
"""Initialize the event namespace.
|
"""Initialize the event namespace.
|
||||||
@ -1475,6 +1491,8 @@ class EventNamespace(AsyncNamespace):
|
|||||||
app: The application object.
|
app: The application object.
|
||||||
"""
|
"""
|
||||||
super().__init__(namespace)
|
super().__init__(namespace)
|
||||||
|
self.token_to_sid = {}
|
||||||
|
self.sid_to_token = {}
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def on_connect(self, sid, environ):
|
def on_connect(self, sid, environ):
|
||||||
|
@ -293,13 +293,15 @@ class Upload(MemoizationLeaf):
|
|||||||
format.to_camel_case(key): value for key, value in upload_props.items()
|
format.to_camel_case(key): value for key, value in upload_props.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
use_dropzone_arguments = {
|
use_dropzone_arguments = Var.create(
|
||||||
"onDrop": event_var,
|
{
|
||||||
**upload_props,
|
"onDrop": event_var,
|
||||||
}
|
**upload_props,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
|
left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
|
||||||
right_side = f"useDropzone({str(Var.create(use_dropzone_arguments))})"
|
right_side = f"useDropzone({str(use_dropzone_arguments)})"
|
||||||
|
|
||||||
var_data = VarData.merge(
|
var_data = VarData.merge(
|
||||||
VarData(
|
VarData(
|
||||||
@ -307,6 +309,7 @@ class Upload(MemoizationLeaf):
|
|||||||
hooks={Hooks.EVENTS: None},
|
hooks={Hooks.EVENTS: None},
|
||||||
),
|
),
|
||||||
event_var._get_all_var_data(),
|
event_var._get_all_var_data(),
|
||||||
|
use_dropzone_arguments._get_all_var_data(),
|
||||||
VarData(
|
VarData(
|
||||||
hooks={
|
hooks={
|
||||||
callback_str: None,
|
callback_str: None,
|
||||||
|
@ -255,7 +255,7 @@ const extractPoints = (points) => {
|
|||||||
|
|
||||||
def _render(self):
|
def _render(self):
|
||||||
tag = super()._render()
|
tag = super()._render()
|
||||||
figure = self.data.to(dict)
|
figure = self.data.to(dict) if self.data is not None else Var.create({})
|
||||||
merge_dicts = [] # Data will be merged and spread from these dict Vars
|
merge_dicts = [] # Data will be merged and spread from these dict Vars
|
||||||
if self.layout is not None:
|
if self.layout is not None:
|
||||||
# Why is this not a literal dict? Great question... it didn't work
|
# Why is this not a literal dict? Great question... it didn't work
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from typing import Dict, Literal
|
from typing import Dict, Literal
|
||||||
|
|
||||||
from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
|
from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
|
||||||
from reflex.utils import console
|
|
||||||
|
|
||||||
|
|
||||||
class Recharts(Component):
|
class Recharts(Component):
|
||||||
@ -11,19 +10,8 @@ class Recharts(Component):
|
|||||||
|
|
||||||
library = "recharts@2.13.0"
|
library = "recharts@2.13.0"
|
||||||
|
|
||||||
def render(self) -> Dict:
|
def _get_style(self) -> Dict:
|
||||||
"""Render the tag.
|
return {"wrapperStyle": self.style}
|
||||||
|
|
||||||
Returns:
|
|
||||||
The rendered tag.
|
|
||||||
"""
|
|
||||||
tag = super().render()
|
|
||||||
if any(p.startswith("css") for p in tag["props"]):
|
|
||||||
console.warn(
|
|
||||||
f"CSS props do not work for {self.__class__.__name__}. Consult docs to style it with its own prop."
|
|
||||||
)
|
|
||||||
tag["props"] = [p for p in tag["props"] if not p.startswith("css")]
|
|
||||||
return tag
|
|
||||||
|
|
||||||
|
|
||||||
class RechartsCharts(NoSSRComponent, MemoizationLeaf):
|
class RechartsCharts(NoSSRComponent, MemoizationLeaf):
|
||||||
|
@ -11,7 +11,6 @@ from reflex.style import Style
|
|||||||
from reflex.vars.base import Var
|
from reflex.vars.base import Var
|
||||||
|
|
||||||
class Recharts(Component):
|
class Recharts(Component):
|
||||||
def render(self) -> Dict: ...
|
|
||||||
@overload
|
@overload
|
||||||
@classmethod
|
@classmethod
|
||||||
def create( # type: ignore
|
def create( # type: ignore
|
||||||
|
@ -652,9 +652,9 @@ class Config(Base):
|
|||||||
frontend_packages: List[str] = []
|
frontend_packages: List[str] = []
|
||||||
|
|
||||||
# The hosting service backend URL.
|
# The hosting service backend URL.
|
||||||
cp_backend_url: str = Hosting.CP_BACKEND_URL
|
cp_backend_url: str = Hosting.HOSTING_SERVICE
|
||||||
# The hosting service frontend URL.
|
# The hosting service frontend URL.
|
||||||
cp_web_url: str = Hosting.CP_WEB_URL
|
cp_web_url: str = Hosting.HOSTING_SERVICE_UI
|
||||||
|
|
||||||
# The worker class used in production mode
|
# The worker class used in production mode
|
||||||
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
|
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
|
||||||
|
@ -827,11 +827,11 @@ def _collect_details_for_gallery():
|
|||||||
Raises:
|
Raises:
|
||||||
Exit: If pyproject.toml file is ill-formed or the request to the backend services fails.
|
Exit: If pyproject.toml file is ill-formed or the request to the backend services fails.
|
||||||
"""
|
"""
|
||||||
from reflex.reflex import _login
|
from reflex_cli.utils import hosting
|
||||||
|
|
||||||
console.rule("[bold]Authentication with Reflex Services")
|
console.rule("[bold]Authentication with Reflex Services")
|
||||||
console.print("First let's log in to Reflex backend services.")
|
console.print("First let's log in to Reflex backend services.")
|
||||||
access_token = _login()
|
access_token, _ = hosting.authenticated_token()
|
||||||
|
|
||||||
console.rule("[bold]Custom Component Information")
|
console.rule("[bold]Custom Component Information")
|
||||||
params = {}
|
params = {}
|
||||||
|
205
reflex/reflex.py
205
reflex/reflex.py
@ -9,8 +9,6 @@ from typing import List, Optional
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
import typer.core
|
import typer.core
|
||||||
from reflex_cli.deployments import deployments_cli
|
|
||||||
from reflex_cli.utils import dependency
|
|
||||||
from reflex_cli.v2.deployments import check_version, hosting_cli
|
from reflex_cli.v2.deployments import check_version, hosting_cli
|
||||||
|
|
||||||
from reflex import constants
|
from reflex import constants
|
||||||
@ -330,47 +328,16 @@ def export(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _login() -> str:
|
|
||||||
"""Helper function to authenticate with Reflex hosting service."""
|
|
||||||
from reflex_cli.utils import hosting
|
|
||||||
|
|
||||||
access_token, invitation_code = hosting.authenticated_token()
|
|
||||||
if access_token:
|
|
||||||
console.print("You already logged in.")
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
# If not already logged in, open a browser window/tab to the login page.
|
|
||||||
access_token = hosting.authenticate_on_browser(invitation_code)
|
|
||||||
|
|
||||||
if not access_token:
|
|
||||||
console.error("Unable to authenticate. Please try again or contact support.")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
console.print("Successfully logged in.")
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def login(
|
def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
|
||||||
loglevel: constants.LogLevel = typer.Option(
|
|
||||||
config.loglevel, help="The log level to use."
|
|
||||||
),
|
|
||||||
):
|
|
||||||
"""Authenticate with Reflex hosting service."""
|
|
||||||
# Set the log level.
|
|
||||||
console.set_log_level(loglevel)
|
|
||||||
|
|
||||||
_login()
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def loginv2(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
|
|
||||||
"""Authenicate with experimental Reflex hosting service."""
|
"""Authenicate with experimental Reflex hosting service."""
|
||||||
from reflex_cli.v2 import cli as hosting_cli
|
from reflex_cli.v2 import cli as hosting_cli
|
||||||
|
|
||||||
check_version()
|
check_version()
|
||||||
|
|
||||||
hosting_cli.login()
|
validated_info = hosting_cli.login()
|
||||||
|
if validated_info is not None:
|
||||||
|
telemetry.send("login", user_uuid=validated_info.get("user_id"))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -380,31 +347,11 @@ def logout(
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""Log out of access to Reflex hosting service."""
|
"""Log out of access to Reflex hosting service."""
|
||||||
from reflex_cli.utils import hosting
|
from reflex_cli.v2.cli import logout
|
||||||
|
|
||||||
console.set_log_level(loglevel)
|
|
||||||
|
|
||||||
hosting.log_out_on_browser()
|
|
||||||
console.debug("Deleting access token from config locally")
|
|
||||||
hosting.delete_token_from_config(include_invitation_code=True)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def logoutv2(
|
|
||||||
loglevel: constants.LogLevel = typer.Option(
|
|
||||||
config.loglevel, help="The log level to use."
|
|
||||||
),
|
|
||||||
):
|
|
||||||
"""Log out of access to Reflex hosting service."""
|
|
||||||
from reflex_cli.v2.utils import hosting
|
|
||||||
|
|
||||||
check_version()
|
check_version()
|
||||||
|
|
||||||
console.set_log_level(loglevel)
|
logout(loglevel) # type: ignore
|
||||||
|
|
||||||
hosting.log_out_on_browser()
|
|
||||||
console.debug("Deleting access token from config locally")
|
|
||||||
hosting.delete_token_from_config(include_invitation_code=True)
|
|
||||||
|
|
||||||
|
|
||||||
db_cli = typer.Typer()
|
db_cli = typer.Typer()
|
||||||
@ -489,126 +436,6 @@ def makemigrations(
|
|||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def deploy(
|
def deploy(
|
||||||
key: Optional[str] = typer.Option(
|
|
||||||
None,
|
|
||||||
"-k",
|
|
||||||
"--deployment-key",
|
|
||||||
help="The name of the deployment. Domain name safe characters only.",
|
|
||||||
),
|
|
||||||
app_name: str = typer.Option(
|
|
||||||
config.app_name,
|
|
||||||
"--app-name",
|
|
||||||
help="The name of the App to deploy under.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
regions: List[str] = typer.Option(
|
|
||||||
list(),
|
|
||||||
"-r",
|
|
||||||
"--region",
|
|
||||||
help="The regions to deploy to.",
|
|
||||||
),
|
|
||||||
envs: List[str] = typer.Option(
|
|
||||||
list(),
|
|
||||||
"--env",
|
|
||||||
help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
|
|
||||||
),
|
|
||||||
cpus: Optional[int] = typer.Option(
|
|
||||||
None, help="The number of CPUs to allocate.", hidden=True
|
|
||||||
),
|
|
||||||
memory_mb: Optional[int] = typer.Option(
|
|
||||||
None, help="The amount of memory to allocate.", hidden=True
|
|
||||||
),
|
|
||||||
auto_start: Optional[bool] = typer.Option(
|
|
||||||
None,
|
|
||||||
help="Whether to auto start the instance.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
auto_stop: Optional[bool] = typer.Option(
|
|
||||||
None,
|
|
||||||
help="Whether to auto stop the instance.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
frontend_hostname: Optional[str] = typer.Option(
|
|
||||||
None,
|
|
||||||
"--frontend-hostname",
|
|
||||||
help="The hostname of the frontend.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
interactive: bool = typer.Option(
|
|
||||||
True,
|
|
||||||
help="Whether to list configuration options and ask for confirmation.",
|
|
||||||
),
|
|
||||||
with_metrics: Optional[str] = typer.Option(
|
|
||||||
None,
|
|
||||||
help="Setting for metrics scraping for the deployment. Setup required in user code.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
with_tracing: Optional[str] = typer.Option(
|
|
||||||
None,
|
|
||||||
help="Setting to export tracing for the deployment. Setup required in user code.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
upload_db_file: bool = typer.Option(
|
|
||||||
False,
|
|
||||||
help="Whether to include local sqlite db files when uploading to hosting service.",
|
|
||||||
hidden=True,
|
|
||||||
),
|
|
||||||
loglevel: constants.LogLevel = typer.Option(
|
|
||||||
config.loglevel, help="The log level to use."
|
|
||||||
),
|
|
||||||
):
|
|
||||||
"""Deploy the app to the Reflex hosting service."""
|
|
||||||
from reflex_cli import cli as hosting_cli
|
|
||||||
|
|
||||||
from reflex.utils import export as export_utils
|
|
||||||
from reflex.utils import prerequisites
|
|
||||||
|
|
||||||
# Set the log level.
|
|
||||||
console.set_log_level(loglevel)
|
|
||||||
|
|
||||||
# Only check requirements if interactive. There is user interaction for requirements update.
|
|
||||||
if interactive:
|
|
||||||
dependency.check_requirements()
|
|
||||||
|
|
||||||
# Check if we are set up.
|
|
||||||
if prerequisites.needs_reinit(frontend=True):
|
|
||||||
_init(name=config.app_name, loglevel=loglevel)
|
|
||||||
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
|
|
||||||
|
|
||||||
hosting_cli.deploy(
|
|
||||||
app_name=app_name,
|
|
||||||
export_fn=lambda zip_dest_dir,
|
|
||||||
api_url,
|
|
||||||
deploy_url,
|
|
||||||
frontend,
|
|
||||||
backend,
|
|
||||||
zipping: export_utils.export(
|
|
||||||
zip_dest_dir=zip_dest_dir,
|
|
||||||
api_url=api_url,
|
|
||||||
deploy_url=deploy_url,
|
|
||||||
frontend=frontend,
|
|
||||||
backend=backend,
|
|
||||||
zipping=zipping,
|
|
||||||
loglevel=loglevel.subprocess_level(),
|
|
||||||
upload_db_file=upload_db_file,
|
|
||||||
),
|
|
||||||
key=key,
|
|
||||||
regions=regions,
|
|
||||||
envs=envs,
|
|
||||||
cpus=cpus,
|
|
||||||
memory_mb=memory_mb,
|
|
||||||
auto_start=auto_start,
|
|
||||||
auto_stop=auto_stop,
|
|
||||||
frontend_hostname=frontend_hostname,
|
|
||||||
interactive=interactive,
|
|
||||||
with_metrics=with_metrics,
|
|
||||||
with_tracing=with_tracing,
|
|
||||||
loglevel=loglevel.subprocess_level(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
def deployv2(
|
|
||||||
app_name: str = typer.Option(
|
app_name: str = typer.Option(
|
||||||
config.app_name,
|
config.app_name,
|
||||||
"--app-name",
|
"--app-name",
|
||||||
@ -660,8 +487,8 @@ def deployv2(
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""Deploy the app to the Reflex hosting service."""
|
"""Deploy the app to the Reflex hosting service."""
|
||||||
|
from reflex_cli.utils import dependency
|
||||||
from reflex_cli.v2 import cli as hosting_cli
|
from reflex_cli.v2 import cli as hosting_cli
|
||||||
from reflex_cli.v2.utils import dependency
|
|
||||||
|
|
||||||
from reflex.utils import export as export_utils
|
from reflex.utils import export as export_utils
|
||||||
from reflex.utils import prerequisites
|
from reflex.utils import prerequisites
|
||||||
@ -671,6 +498,13 @@ def deployv2(
|
|||||||
# Set the log level.
|
# Set the log level.
|
||||||
console.set_log_level(loglevel)
|
console.set_log_level(loglevel)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
# make sure user is logged in.
|
||||||
|
if interactive:
|
||||||
|
hosting_cli.login()
|
||||||
|
else:
|
||||||
|
raise SystemExit("Token is required for non-interactive mode.")
|
||||||
|
|
||||||
# Only check requirements if interactive.
|
# Only check requirements if interactive.
|
||||||
# There is user interaction for requirements update.
|
# There is user interaction for requirements update.
|
||||||
if interactive:
|
if interactive:
|
||||||
@ -703,7 +537,7 @@ def deployv2(
|
|||||||
envfile=envfile,
|
envfile=envfile,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
interactive=interactive,
|
interactive=interactive,
|
||||||
loglevel=loglevel.subprocess_level(),
|
loglevel=type(loglevel).INFO, # type: ignore
|
||||||
token=token,
|
token=token,
|
||||||
project=project,
|
project=project,
|
||||||
)
|
)
|
||||||
@ -711,15 +545,10 @@ def deployv2(
|
|||||||
|
|
||||||
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
|
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
|
||||||
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
|
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
|
||||||
cli.add_typer(
|
|
||||||
deployments_cli,
|
|
||||||
name="deployments",
|
|
||||||
help="Subcommands for managing the Deployments.",
|
|
||||||
)
|
|
||||||
cli.add_typer(
|
cli.add_typer(
|
||||||
hosting_cli,
|
hosting_cli,
|
||||||
name="apps",
|
name="cloud",
|
||||||
help="Subcommands for managing the Deployments.",
|
help="Subcommands for managing the reflex cloud.",
|
||||||
)
|
)
|
||||||
cli.add_typer(
|
cli.add_typer(
|
||||||
custom_components_cli,
|
custom_components_cli,
|
||||||
|
@ -1748,7 +1748,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
hinted_args = value_inside_optional(hinted_args)
|
hinted_args = value_inside_optional(hinted_args)
|
||||||
if isinstance(value, dict) and inspect.isclass(hinted_args):
|
if (
|
||||||
|
isinstance(value, dict)
|
||||||
|
and inspect.isclass(hinted_args)
|
||||||
|
and not types.is_generic_alias(hinted_args) # py3.9-py3.10
|
||||||
|
):
|
||||||
if issubclass(hinted_args, Model):
|
if issubclass(hinted_args, Model):
|
||||||
# Remove non-fields from the payload
|
# Remove non-fields from the payload
|
||||||
payload[arg] = hinted_args(
|
payload[arg] = hinted_args(
|
||||||
@ -1759,7 +1763,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif dataclasses.is_dataclass(hinted_args) or issubclass(
|
elif dataclasses.is_dataclass(hinted_args) or issubclass(
|
||||||
hinted_args, Base
|
hinted_args, (Base, BaseModelV1, BaseModelV2)
|
||||||
):
|
):
|
||||||
payload[arg] = hinted_args(**value)
|
payload[arg] = hinted_args(**value)
|
||||||
if isinstance(value, list) and (hinted_args is set or hinted_args is Set):
|
if isinstance(value, list) and (hinted_args is set or hinted_args is Set):
|
||||||
@ -1955,6 +1959,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|||||||
if var in self.base_vars or var in self._backend_vars:
|
if var in self.base_vars or var in self._backend_vars:
|
||||||
self._was_touched = True
|
self._was_touched = True
|
||||||
break
|
break
|
||||||
|
if var == constants.ROUTER_DATA and self.parent_state is None:
|
||||||
|
self._was_touched = True
|
||||||
|
break
|
||||||
|
|
||||||
def _get_was_touched(self) -> bool:
|
def _get_was_touched(self) -> bool:
|
||||||
"""Check current dirty_vars and flag to determine if state instance was modified.
|
"""Check current dirty_vars and flag to determine if state instance was modified.
|
||||||
|
@ -1408,13 +1408,22 @@ def validate_and_create_app_using_remote_template(app_name, template, templates)
|
|||||||
"""
|
"""
|
||||||
# If user selects a template, it needs to exist.
|
# If user selects a template, it needs to exist.
|
||||||
if template in templates:
|
if template in templates:
|
||||||
|
from reflex_cli.v2.utils import hosting
|
||||||
|
|
||||||
|
authenticated_token = hosting.authenticated_token()
|
||||||
|
if not authenticated_token or not authenticated_token[0]:
|
||||||
|
console.print(
|
||||||
|
f"Please use `reflex login` to access the '{template}' template."
|
||||||
|
)
|
||||||
|
raise typer.Exit(3)
|
||||||
|
|
||||||
template_url = templates[template].code_url
|
template_url = templates[template].code_url
|
||||||
else:
|
else:
|
||||||
# Check if the template is a github repo.
|
# Check if the template is a github repo.
|
||||||
if template.startswith("https://github.com"):
|
if template.startswith("https://github.com"):
|
||||||
template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip"
|
template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip"
|
||||||
else:
|
else:
|
||||||
console.error(f"Template `{template}` not found.")
|
console.error(f"Template `{template}` not found or invalid.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if template_url is None:
|
if template_url is None:
|
||||||
@ -1451,7 +1460,7 @@ def generate_template_using_ai(template: str | None = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def fetch_remote_templates(
|
def fetch_remote_templates(
|
||||||
template: Optional[str] = None,
|
template: str,
|
||||||
) -> tuple[str, dict[str, Template]]:
|
) -> tuple[str, dict[str, Template]]:
|
||||||
"""Fetch the available remote templates.
|
"""Fetch the available remote templates.
|
||||||
|
|
||||||
@ -1460,9 +1469,6 @@ def fetch_remote_templates(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The selected template and the available templates.
|
The selected template and the available templates.
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exit: If the template is not valid or if the template is not specified.
|
|
||||||
"""
|
"""
|
||||||
available_templates = {}
|
available_templates = {}
|
||||||
|
|
||||||
@ -1474,19 +1480,7 @@ def fetch_remote_templates(
|
|||||||
console.debug(f"Error while fetching templates: {e}")
|
console.debug(f"Error while fetching templates: {e}")
|
||||||
template = constants.Templates.DEFAULT
|
template = constants.Templates.DEFAULT
|
||||||
|
|
||||||
if template == constants.Templates.DEFAULT:
|
return template, available_templates
|
||||||
return template, available_templates
|
|
||||||
|
|
||||||
if template in available_templates:
|
|
||||||
return template, available_templates
|
|
||||||
|
|
||||||
else:
|
|
||||||
if template is not None:
|
|
||||||
console.error(f"{template!r} is not a valid template name.")
|
|
||||||
console.print(
|
|
||||||
f"Go to the templates page ({constants.Templates.REFLEX_TEMPLATES_URL}) and copy the command to init with a template."
|
|
||||||
)
|
|
||||||
raise typer.Exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_app(
|
def initialize_app(
|
||||||
@ -1501,6 +1495,9 @@ def initialize_app(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The name of the template.
|
The name of the template.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exit: If the template is not valid or unspecified.
|
||||||
"""
|
"""
|
||||||
# Local imports to avoid circular imports.
|
# Local imports to avoid circular imports.
|
||||||
from reflex.utils import telemetry
|
from reflex.utils import telemetry
|
||||||
@ -1528,7 +1525,10 @@ def initialize_app(
|
|||||||
# change to the default to allow creation of default app
|
# change to the default to allow creation of default app
|
||||||
template = constants.Templates.DEFAULT
|
template = constants.Templates.DEFAULT
|
||||||
elif template == constants.Templates.CHOOSE_TEMPLATES:
|
elif template == constants.Templates.CHOOSE_TEMPLATES:
|
||||||
template, templates = fetch_remote_templates()
|
console.print(
|
||||||
|
f"Go to the templates page ({constants.Templates.REFLEX_TEMPLATES_URL}) and copy the command to init with a template."
|
||||||
|
)
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
# If the blank template is selected, create a blank app.
|
# If the blank template is selected, create a blank app.
|
||||||
if template in (constants.Templates.DEFAULT,):
|
if template in (constants.Templates.DEFAULT,):
|
||||||
|
@ -129,7 +129,7 @@ def _prepare_event(event: str, **kwargs) -> dict:
|
|||||||
|
|
||||||
cpuinfo = get_cpu_info()
|
cpuinfo = get_cpu_info()
|
||||||
|
|
||||||
additional_keys = ["template", "context", "detail"]
|
additional_keys = ["template", "context", "detail", "user_uuid"]
|
||||||
additional_fields = {
|
additional_fields = {
|
||||||
key: value for key in additional_keys if (value := kwargs.get(key)) is not None
|
key: value for key in additional_keys if (value := kwargs.get(key)) is not None
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,13 @@ from selenium.webdriver import Firefox
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.remote.webdriver import WebDriver
|
from selenium.webdriver.remote.webdriver import WebDriver
|
||||||
|
|
||||||
|
from reflex.state import (
|
||||||
|
State,
|
||||||
|
StateManagerDisk,
|
||||||
|
StateManagerMemory,
|
||||||
|
StateManagerRedis,
|
||||||
|
_substate_key,
|
||||||
|
)
|
||||||
from reflex.testing import AppHarness
|
from reflex.testing import AppHarness
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
@ -74,7 +81,7 @@ def ClientSide():
|
|||||||
return rx.fragment(
|
return rx.fragment(
|
||||||
rx.input(
|
rx.input(
|
||||||
value=ClientSideState.router.session.client_token,
|
value=ClientSideState.router.session.client_token,
|
||||||
is_read_only=True,
|
read_only=True,
|
||||||
id="token",
|
id="token",
|
||||||
),
|
),
|
||||||
rx.input(
|
rx.input(
|
||||||
@ -604,6 +611,110 @@ async def test_client_side_state(
|
|||||||
assert s2.text == "s2 value"
|
assert s2.text == "s2 value"
|
||||||
assert s3.text == "s3 value"
|
assert s3.text == "s3 value"
|
||||||
|
|
||||||
|
# Simulate state expiration
|
||||||
|
if isinstance(client_side.state_manager, StateManagerRedis):
|
||||||
|
await client_side.state_manager.redis.delete(
|
||||||
|
_substate_key(token, State.get_full_name())
|
||||||
|
)
|
||||||
|
await client_side.state_manager.redis.delete(_substate_key(token, state_name))
|
||||||
|
await client_side.state_manager.redis.delete(
|
||||||
|
_substate_key(token, sub_state_name)
|
||||||
|
)
|
||||||
|
await client_side.state_manager.redis.delete(
|
||||||
|
_substate_key(token, sub_sub_state_name)
|
||||||
|
)
|
||||||
|
elif isinstance(client_side.state_manager, (StateManagerMemory, StateManagerDisk)):
|
||||||
|
del client_side.state_manager.states[token]
|
||||||
|
if isinstance(client_side.state_manager, StateManagerDisk):
|
||||||
|
client_side.state_manager.token_expiration = 0
|
||||||
|
client_side.state_manager._purge_expired_states()
|
||||||
|
|
||||||
|
# Ensure the state is gone (not hydrated)
|
||||||
|
async def poll_for_not_hydrated():
|
||||||
|
state = await client_side.get_state(_substate_key(token or "", state_name))
|
||||||
|
return not state.is_hydrated
|
||||||
|
|
||||||
|
assert await AppHarness._poll_for_async(poll_for_not_hydrated)
|
||||||
|
|
||||||
|
# Trigger event to get a new instance of the state since the old was expired.
|
||||||
|
state_var_input = driver.find_element(By.ID, "state_var")
|
||||||
|
state_var_input.send_keys("re-triggering")
|
||||||
|
|
||||||
|
# get new references to all cookie and local storage elements (again)
|
||||||
|
c1 = driver.find_element(By.ID, "c1")
|
||||||
|
c2 = driver.find_element(By.ID, "c2")
|
||||||
|
c3 = driver.find_element(By.ID, "c3")
|
||||||
|
c4 = driver.find_element(By.ID, "c4")
|
||||||
|
c5 = driver.find_element(By.ID, "c5")
|
||||||
|
c6 = driver.find_element(By.ID, "c6")
|
||||||
|
c7 = driver.find_element(By.ID, "c7")
|
||||||
|
l1 = driver.find_element(By.ID, "l1")
|
||||||
|
l2 = driver.find_element(By.ID, "l2")
|
||||||
|
l3 = driver.find_element(By.ID, "l3")
|
||||||
|
l4 = driver.find_element(By.ID, "l4")
|
||||||
|
s1 = driver.find_element(By.ID, "s1")
|
||||||
|
s2 = driver.find_element(By.ID, "s2")
|
||||||
|
s3 = driver.find_element(By.ID, "s3")
|
||||||
|
c1s = driver.find_element(By.ID, "c1s")
|
||||||
|
l1s = driver.find_element(By.ID, "l1s")
|
||||||
|
s1s = driver.find_element(By.ID, "s1s")
|
||||||
|
|
||||||
|
assert c1.text == "c1 value"
|
||||||
|
assert c2.text == "c2 value"
|
||||||
|
assert c3.text == "" # temporary cookie expired after reset state!
|
||||||
|
assert c4.text == "c4 value"
|
||||||
|
assert c5.text == "c5 value"
|
||||||
|
assert c6.text == "c6 value"
|
||||||
|
assert c7.text == "c7 value"
|
||||||
|
assert l1.text == "l1 value"
|
||||||
|
assert l2.text == "l2 value"
|
||||||
|
assert l3.text == "l3 value"
|
||||||
|
assert l4.text == "l4 value"
|
||||||
|
assert s1.text == "s1 value"
|
||||||
|
assert s2.text == "s2 value"
|
||||||
|
assert s3.text == "s3 value"
|
||||||
|
assert c1s.text == "c1s value"
|
||||||
|
assert l1s.text == "l1s value"
|
||||||
|
assert s1s.text == "s1s value"
|
||||||
|
|
||||||
|
# Get the backend state and ensure the values are still set
|
||||||
|
async def get_sub_state():
|
||||||
|
root_state = await client_side.get_state(
|
||||||
|
_substate_key(token or "", sub_state_name)
|
||||||
|
)
|
||||||
|
state = root_state.substates[client_side.get_state_name("_client_side_state")]
|
||||||
|
sub_state = state.substates[
|
||||||
|
client_side.get_state_name("_client_side_sub_state")
|
||||||
|
]
|
||||||
|
return sub_state
|
||||||
|
|
||||||
|
async def poll_for_c1_set():
|
||||||
|
sub_state = await get_sub_state()
|
||||||
|
return sub_state.c1 == "c1 value"
|
||||||
|
|
||||||
|
assert await AppHarness._poll_for_async(poll_for_c1_set)
|
||||||
|
sub_state = await get_sub_state()
|
||||||
|
assert sub_state.c1 == "c1 value"
|
||||||
|
assert sub_state.c2 == "c2 value"
|
||||||
|
assert sub_state.c3 == ""
|
||||||
|
assert sub_state.c4 == "c4 value"
|
||||||
|
assert sub_state.c5 == "c5 value"
|
||||||
|
assert sub_state.c6 == "c6 value"
|
||||||
|
assert sub_state.c7 == "c7 value"
|
||||||
|
assert sub_state.l1 == "l1 value"
|
||||||
|
assert sub_state.l2 == "l2 value"
|
||||||
|
assert sub_state.l3 == "l3 value"
|
||||||
|
assert sub_state.l4 == "l4 value"
|
||||||
|
assert sub_state.s1 == "s1 value"
|
||||||
|
assert sub_state.s2 == "s2 value"
|
||||||
|
assert sub_state.s3 == "s3 value"
|
||||||
|
sub_sub_state = sub_state.substates[
|
||||||
|
client_side.get_state_name("_client_side_sub_sub_state")
|
||||||
|
]
|
||||||
|
assert sub_sub_state.c1s == "c1s value"
|
||||||
|
assert sub_sub_state.l1s == "l1s value"
|
||||||
|
assert sub_sub_state.s1s == "s1s value"
|
||||||
|
|
||||||
# clear the cookie jar and local storage, ensure state reset to default
|
# clear the cookie jar and local storage, ensure state reset to default
|
||||||
driver.delete_all_cookies()
|
driver.delete_all_cookies()
|
||||||
local_storage.clear()
|
local_storage.clear()
|
||||||
|
@ -89,6 +89,11 @@ def DynamicRoute():
|
|||||||
@rx.page(route="/arg/[arg_str]")
|
@rx.page(route="/arg/[arg_str]")
|
||||||
def arg() -> rx.Component:
|
def arg() -> rx.Component:
|
||||||
return rx.vstack(
|
return rx.vstack(
|
||||||
|
rx.input(
|
||||||
|
value=DynamicState.router.session.client_token,
|
||||||
|
read_only=True,
|
||||||
|
id="token",
|
||||||
|
),
|
||||||
rx.data_list.root(
|
rx.data_list.root(
|
||||||
rx.data_list.item(
|
rx.data_list.item(
|
||||||
rx.data_list.label("rx.State.arg_str (dynamic)"),
|
rx.data_list.label("rx.State.arg_str (dynamic)"),
|
||||||
@ -373,12 +378,14 @@ async def test_on_load_navigate_non_dynamic(
|
|||||||
async def test_render_dynamic_arg(
|
async def test_render_dynamic_arg(
|
||||||
dynamic_route: AppHarness,
|
dynamic_route: AppHarness,
|
||||||
driver: WebDriver,
|
driver: WebDriver,
|
||||||
|
token: str,
|
||||||
):
|
):
|
||||||
"""Assert that dynamic arg var is rendered correctly in different contexts.
|
"""Assert that dynamic arg var is rendered correctly in different contexts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dynamic_route: harness for DynamicRoute app.
|
dynamic_route: harness for DynamicRoute app.
|
||||||
driver: WebDriver instance.
|
driver: WebDriver instance.
|
||||||
|
token: The token visible in the driver browser.
|
||||||
"""
|
"""
|
||||||
assert dynamic_route.app_instance is not None
|
assert dynamic_route.app_instance is not None
|
||||||
with poll_for_navigation(driver):
|
with poll_for_navigation(driver):
|
||||||
@ -398,7 +405,8 @@ async def test_render_dynamic_arg(
|
|||||||
el = driver.find_element(By.ID, id)
|
el = driver.find_element(By.ID, id)
|
||||||
assert el
|
assert el
|
||||||
assert (
|
assert (
|
||||||
dynamic_route.poll_for_content(el, exp_not_equal=expect_not) == expected
|
dynamic_route.poll_for_content(el, timeout=30, exp_not_equal=expect_not)
|
||||||
|
== expected
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_content("0", "")
|
assert_content("0", "")
|
||||||
|
@ -19,10 +19,14 @@ def UploadFile():
|
|||||||
|
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
|
LARGE_DATA = "DUMMY" * 1024 * 512
|
||||||
|
|
||||||
class UploadState(rx.State):
|
class UploadState(rx.State):
|
||||||
_file_data: Dict[str, str] = {}
|
_file_data: Dict[str, str] = {}
|
||||||
event_order: List[str] = []
|
event_order: List[str] = []
|
||||||
progress_dicts: List[dict] = []
|
progress_dicts: List[dict] = []
|
||||||
|
disabled: bool = False
|
||||||
|
large_data: str = ""
|
||||||
|
|
||||||
async def handle_upload(self, files: List[rx.UploadFile]):
|
async def handle_upload(self, files: List[rx.UploadFile]):
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -33,6 +37,7 @@ def UploadFile():
|
|||||||
for file in files:
|
for file in files:
|
||||||
upload_data = await file.read()
|
upload_data = await file.read()
|
||||||
self._file_data[file.filename or ""] = upload_data.decode("utf-8")
|
self._file_data[file.filename or ""] = upload_data.decode("utf-8")
|
||||||
|
self.large_data = LARGE_DATA
|
||||||
yield UploadState.chain_event
|
yield UploadState.chain_event
|
||||||
|
|
||||||
def upload_progress(self, progress):
|
def upload_progress(self, progress):
|
||||||
@ -41,13 +46,15 @@ def UploadFile():
|
|||||||
self.progress_dicts.append(progress)
|
self.progress_dicts.append(progress)
|
||||||
|
|
||||||
def chain_event(self):
|
def chain_event(self):
|
||||||
|
assert self.large_data == LARGE_DATA
|
||||||
|
self.large_data = ""
|
||||||
self.event_order.append("chain_event")
|
self.event_order.append("chain_event")
|
||||||
|
|
||||||
def index():
|
def index():
|
||||||
return rx.vstack(
|
return rx.vstack(
|
||||||
rx.input(
|
rx.input(
|
||||||
value=UploadState.router.session.client_token,
|
value=UploadState.router.session.client_token,
|
||||||
is_read_only=True,
|
read_only=True,
|
||||||
id="token",
|
id="token",
|
||||||
),
|
),
|
||||||
rx.heading("Default Upload"),
|
rx.heading("Default Upload"),
|
||||||
@ -56,6 +63,7 @@ def UploadFile():
|
|||||||
rx.button("Select File"),
|
rx.button("Select File"),
|
||||||
rx.text("Drag and drop files here or click to select files"),
|
rx.text("Drag and drop files here or click to select files"),
|
||||||
),
|
),
|
||||||
|
disabled=UploadState.disabled,
|
||||||
),
|
),
|
||||||
rx.button(
|
rx.button(
|
||||||
"Upload",
|
"Upload",
|
||||||
|
@ -1007,8 +1007,9 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
|||||||
substate_token = _substate_key(token, DynamicState)
|
substate_token = _substate_key(token, DynamicState)
|
||||||
sid = "mock_sid"
|
sid = "mock_sid"
|
||||||
client_ip = "127.0.0.1"
|
client_ip = "127.0.0.1"
|
||||||
state = await app.state_manager.get_state(substate_token)
|
async with app.state_manager.modify_state(substate_token) as state:
|
||||||
assert state.dynamic == ""
|
state.router_data = {"simulate": "hydrated"}
|
||||||
|
assert state.dynamic == ""
|
||||||
exp_vals = ["foo", "foobar", "baz"]
|
exp_vals = ["foo", "foobar", "baz"]
|
||||||
|
|
||||||
def _event(name, val, **kwargs):
|
def _event(name, val, **kwargs):
|
||||||
@ -1180,6 +1181,7 @@ async def test_process_events(mocker, token: str):
|
|||||||
"ip": "127.0.0.1",
|
"ip": "127.0.0.1",
|
||||||
}
|
}
|
||||||
app = App(state=GenState)
|
app = App(state=GenState)
|
||||||
|
|
||||||
mocker.patch.object(app, "_postprocess", AsyncMock())
|
mocker.patch.object(app, "_postprocess", AsyncMock())
|
||||||
event = Event(
|
event = Event(
|
||||||
token=token,
|
token=token,
|
||||||
@ -1187,6 +1189,8 @@ async def test_process_events(mocker, token: str):
|
|||||||
payload={"c": 5},
|
payload={"c": 5},
|
||||||
router_data=router_data,
|
router_data=router_data,
|
||||||
)
|
)
|
||||||
|
async with app.state_manager.modify_state(event.substate_token) as state:
|
||||||
|
state.router_data = {"simulate": "hydrated"}
|
||||||
|
|
||||||
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
|
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
|
||||||
pass
|
pass
|
||||||
|
@ -10,7 +10,17 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
|
from typing import (
|
||||||
|
Any,
|
||||||
|
AsyncGenerator,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -1828,12 +1838,11 @@ async def test_state_manager_lock_expire_contend(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def mock_app(monkeypatch, state_manager: StateManager) -> rx.App:
|
def mock_app_simple(monkeypatch) -> rx.App:
|
||||||
"""Mock app fixture.
|
"""Simple Mock app fixture.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
monkeypatch: Pytest monkeypatch object.
|
monkeypatch: Pytest monkeypatch object.
|
||||||
state_manager: A state manager.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The app, after mocking out prerequisites.get_app()
|
The app, after mocking out prerequisites.get_app()
|
||||||
@ -1844,7 +1853,6 @@ def mock_app(monkeypatch, state_manager: StateManager) -> rx.App:
|
|||||||
|
|
||||||
setattr(app_module, CompileVars.APP, app)
|
setattr(app_module, CompileVars.APP, app)
|
||||||
app.state = TestState
|
app.state = TestState
|
||||||
app._state_manager = state_manager
|
|
||||||
app.event_namespace.emit = AsyncMock() # type: ignore
|
app.event_namespace.emit = AsyncMock() # type: ignore
|
||||||
|
|
||||||
def _mock_get_app(*args, **kwargs):
|
def _mock_get_app(*args, **kwargs):
|
||||||
@ -1854,6 +1862,21 @@ def mock_app(monkeypatch, state_manager: StateManager) -> rx.App:
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def mock_app(mock_app_simple: rx.App, state_manager: StateManager) -> rx.App:
|
||||||
|
"""Mock app fixture.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mock_app_simple: A simple mock app.
|
||||||
|
state_manager: A state manager.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The app, after mocking out prerequisites.get_app()
|
||||||
|
"""
|
||||||
|
mock_app_simple._state_manager = state_manager
|
||||||
|
return mock_app_simple
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
|
async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
|
||||||
"""Test that the state proxy works.
|
"""Test that the state proxy works.
|
||||||
@ -1959,6 +1982,10 @@ class BackgroundTaskState(BaseState):
|
|||||||
order: List[str] = []
|
order: List[str] = []
|
||||||
dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]}
|
dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs): # noqa: D107
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.router_data = {"simulate": "hydrate"}
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def computed_order(self) -> List[str]:
|
def computed_order(self) -> List[str]:
|
||||||
"""Get the order as a computed var.
|
"""Get the order as a computed var.
|
||||||
@ -2709,7 +2736,7 @@ def test_set_base_field_via_setter():
|
|||||||
assert "c2" in bfss.dirty_vars
|
assert "c2" in bfss.dirty_vars
|
||||||
|
|
||||||
|
|
||||||
def exp_is_hydrated(state: State, is_hydrated: bool = True) -> Dict[str, Any]:
|
def exp_is_hydrated(state: BaseState, is_hydrated: bool = True) -> Dict[str, Any]:
|
||||||
"""Expected IS_HYDRATED delta that would be emitted by HydrateMiddleware.
|
"""Expected IS_HYDRATED delta that would be emitted by HydrateMiddleware.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -2788,7 +2815,8 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker):
|
|||||||
app = app_module_mock.app = App(
|
app = app_module_mock.app = App(
|
||||||
state=State, load_events={"index": [test_state.test_handler]}
|
state=State, load_events={"index": [test_state.test_handler]}
|
||||||
)
|
)
|
||||||
state = State()
|
async with app.state_manager.modify_state(_substate_key(token, State)) as state:
|
||||||
|
state.router_data = {"simulate": "hydrate"}
|
||||||
|
|
||||||
updates = []
|
updates = []
|
||||||
async for update in rx.app.process(
|
async for update in rx.app.process(
|
||||||
@ -2835,7 +2863,8 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker):
|
|||||||
state=State,
|
state=State,
|
||||||
load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]},
|
load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]},
|
||||||
)
|
)
|
||||||
state = State()
|
async with app.state_manager.modify_state(_substate_key(token, State)) as state:
|
||||||
|
state.router_data = {"simulate": "hydrate"}
|
||||||
|
|
||||||
updates = []
|
updates = []
|
||||||
async for update in rx.app.process(
|
async for update in rx.app.process(
|
||||||
@ -3506,3 +3535,106 @@ def test_init_mixin() -> None:
|
|||||||
|
|
||||||
with pytest.raises(ReflexRuntimeError):
|
with pytest.raises(ReflexRuntimeError):
|
||||||
SubMixin()
|
SubMixin()
|
||||||
|
|
||||||
|
|
||||||
|
class ReflexModel(rx.Model):
|
||||||
|
"""A model for testing."""
|
||||||
|
|
||||||
|
foo: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpcastState(rx.State):
|
||||||
|
"""A state for testing upcasting."""
|
||||||
|
|
||||||
|
passed: bool = False
|
||||||
|
|
||||||
|
def rx_model(self, m: ReflexModel): # noqa: D102
|
||||||
|
assert isinstance(m, ReflexModel)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def rx_base(self, o: Object): # noqa: D102
|
||||||
|
assert isinstance(o, Object)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def rx_base_or_none(self, o: Optional[Object]): # noqa: D102
|
||||||
|
if o is not None:
|
||||||
|
assert isinstance(o, Object)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def rx_basemodelv1(self, m: ModelV1): # noqa: D102
|
||||||
|
assert isinstance(m, ModelV1)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def rx_basemodelv2(self, m: ModelV2): # noqa: D102
|
||||||
|
assert isinstance(m, ModelV2)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def rx_dataclass(self, dc: ModelDC): # noqa: D102
|
||||||
|
assert isinstance(dc, ModelDC)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_set(self, s: set): # noqa: D102
|
||||||
|
assert isinstance(s, set)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_Set(self, s: Set): # noqa: D102
|
||||||
|
assert isinstance(s, Set)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_tuple(self, t: tuple): # noqa: D102
|
||||||
|
assert isinstance(t, tuple)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_Tuple(self, t: Tuple): # noqa: D102
|
||||||
|
assert isinstance(t, tuple)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_dict(self, d: dict[str, str]): # noqa: D102
|
||||||
|
assert isinstance(d, dict)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_list(self, ls: list[str]): # noqa: D102
|
||||||
|
assert isinstance(ls, list)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_Any(self, a: Any): # noqa: D102
|
||||||
|
assert isinstance(a, list)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
def py_unresolvable(self, u: "Unresolvable"): # noqa: D102, F821 # type: ignore
|
||||||
|
assert isinstance(u, list)
|
||||||
|
self.passed = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_app_simple")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("handler", "payload"),
|
||||||
|
[
|
||||||
|
(UpcastState.rx_model, {"m": {"foo": "bar"}}),
|
||||||
|
(UpcastState.rx_base, {"o": {"foo": "bar"}}),
|
||||||
|
(UpcastState.rx_base_or_none, {"o": {"foo": "bar"}}),
|
||||||
|
(UpcastState.rx_base_or_none, {"o": None}),
|
||||||
|
(UpcastState.rx_basemodelv1, {"m": {"foo": "bar"}}),
|
||||||
|
(UpcastState.rx_basemodelv2, {"m": {"foo": "bar"}}),
|
||||||
|
(UpcastState.rx_dataclass, {"dc": {"foo": "bar"}}),
|
||||||
|
(UpcastState.py_set, {"s": ["foo", "foo"]}),
|
||||||
|
(UpcastState.py_Set, {"s": ["foo", "foo"]}),
|
||||||
|
(UpcastState.py_tuple, {"t": ["foo", "foo"]}),
|
||||||
|
(UpcastState.py_Tuple, {"t": ["foo", "foo"]}),
|
||||||
|
(UpcastState.py_dict, {"d": {"foo": "bar"}}),
|
||||||
|
(UpcastState.py_list, {"ls": ["foo", "foo"]}),
|
||||||
|
(UpcastState.py_Any, {"a": ["foo"]}),
|
||||||
|
(UpcastState.py_unresolvable, {"u": ["foo"]}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_upcast_event_handler_arg(handler, payload):
|
||||||
|
"""Test that upcast event handler args work correctly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handler: The handler to test.
|
||||||
|
payload: The payload to test.
|
||||||
|
"""
|
||||||
|
state = UpcastState()
|
||||||
|
async for update in state._process_event(handler, state, payload):
|
||||||
|
assert update.delta == {UpcastState.get_full_name(): {"passed": True}}
|
||||||
|
Loading…
Reference in New Issue
Block a user