diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml new file mode 100644 index 000000000..f4091ea39 --- /dev/null +++ b/.github/codeql-config.yml @@ -0,0 +1,2 @@ +paths-ignore: + - "**/tests/**" diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6da40ef6f..b794106e1 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -70,56 +70,6 @@ jobs: env: GITHUB_SHA: ${{ github.sha }} - simple-apps-benchmarks: # This app tests the compile times of various compoonents and pages - if: github.event.pull_request.merged == true - env: - OUTPUT_FILE: benchmarks.json - timeout-minutes: 50 - strategy: - # Prioritize getting more information out of the workflow (even if something fails) - fail-fast: false - matrix: - # Show OS combos first in GUI - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10.16", "3.11.11", "3.12.8"] - exclude: - - os: windows-latest - python-version: "3.10.16" - - os: windows-latest - python-version: "3.11.11" - # keep only one python version for MacOS - - os: macos-latest - python-version: "3.10.16" - - os: macos-latest - python-version: "3.11.11" - include: - - os: windows-latest - python-version: "3.10.11" - - os: windows-latest - python-version: "3.11.9" - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup_build_env - with: - python-version: ${{ matrix.python-version }} - run-poetry-install: true - create-venv-at-path: .venv - - name: Run benchmark tests - env: - APP_HARNESS_HEADLESS: 1 - PYTHONUNBUFFERED: 1 - run: | - poetry run pytest -v benchmarks/ --benchmark-json=${{ env.OUTPUT_FILE }} -s - - name: Upload benchmark results - # Only run if the database creds are available in this context. - run: - poetry run python benchmarks/benchmark_compile_times.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "${{ env.OUTPUT_FILE }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}" - reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) if: github.event.pull_request.merged == true timeout-minutes: 30 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..6b2a54dd5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: "36 7 * * 4" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + - language: actions + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + config-file: .github/codeql-config.yml + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index b02604fd6..dc5a14d88 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -94,26 +94,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-examples/counter dev - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" - --branch-name "${{ github.head_ref || github.ref_name }}" - --path ./reflex-examples/counter/.web - --app-name "counter" - - name: Install hyperfine - run: cargo install hyperfine - - name: Benchmark imports - working-directory: ./reflex-examples/counter - run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash - - name: Upload Benchmarks - run: - poetry run python benchmarks/benchmark_imports.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}" - --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" - --app-name "counter" - name: Install requirements for nba proxy example working-directory: ./reflex-examples/nba-proxy run: | @@ -174,12 +154,6 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web rx-shout-from-template: strategy: @@ -243,9 +217,3 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./reflex-web prod - - name: Measure and upload .web size - run: - poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}" - --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" - --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" - --app-name "reflex-web" --path ./reflex-web/.web diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bad7b996..743f5f31a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ fail_fast: true repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.9.3 + rev: v0.9.6 hooks: - id: ruff-format args: [reflex, tests] diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py deleted file mode 100644 index 9bcfbf85b..000000000 --- a/benchmarks/test_benchmark_compile_components.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Benchmark tests for apps with varying component numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_component(num: int): - """Generate a number of components based on num. - - Args: - num: number of components to produce. - - Returns: - The rendered number of components. - """ - import reflex as rx - - return [ - rx.fragment( - rx.box( - rx.accordion.root( - rx.accordion.item( - header="Full Ingredients", - content="Yes. It's built with accessibility in mind.", - font_size="3em", - ), - rx.accordion.item( - header="Applications", - content="Yes. It's unstyled by default, giving you freedom over the look and feel.", - ), - collapsible=True, - variant="ghost", - width="25rem", - ), - padding_top="20px", - ), - rx.box( - rx.drawer.root( - rx.drawer.trigger( - rx.button("Open Drawer with snap points"), as_child=True - ), - rx.drawer.overlay(), - rx.drawer.portal( - rx.drawer.content( - rx.flex( - rx.drawer.title("Drawer Content"), - rx.drawer.description("Drawer description"), - rx.drawer.close( - rx.button("Close Button"), - as_child=True, - ), - direction="column", - margin="5em", - align_items="center", - ), - top="auto", - height="100%", - flex_direction="column", - background_color="var(--green-3)", - ), - ), - snap_points=["148px", "355px", 1], - ), - ), - rx.box( - rx.callout( - "You will need admin privileges to install and access this application.", - icon="info", - size="3", - ), - ), - rx.box( - rx.table.root( - rx.table.header( - rx.table.row( - rx.table.column_header_cell("Full name"), - rx.table.column_header_cell("Email"), - rx.table.column_header_cell("Group"), - ), - ), - rx.table.body( - rx.table.row( - rx.table.row_header_cell("Danilo Sousa"), - rx.table.cell("danilo@example.com"), - rx.table.cell("Developer"), - ), - rx.table.row( - rx.table.row_header_cell("Zahra Ambessa"), - rx.table.cell("zahra@example.com"), - rx.table.cell("Admin"), - ), - rx.table.row( - rx.table.row_header_cell("Jasper Eriksson"), - rx.table.cell("jasper@example.com"), - rx.table.cell("Developer"), - ), - ), - ) - ), - ) - ] * num - - -def AppWithTenComponentsOnePage(): - """A reflex app with roughly 10 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithHundredComponentOnePage(): - """A reflex app with roughly 100 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(100))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithThousandComponentsOnePage(): - """A reflex app with roughly 1000 components on one page.""" - import reflex as rx - - def index() -> rx.Component: - return rx.center(rx.vstack(*render_component(1000))) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -@pytest.fixture(scope="session") -def app_with_10_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_100_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Start Blank Template app at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app100components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredComponentOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_1000_components( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 components at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000components") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandComponentsOnePage, - render_component=render_component, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_10_components): - """Test the compile time on a cold start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - - def setup(): - with chdir(app_with_10_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=10) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_10_components): - """Test the compile time on a warm start for an app with roughly 10 components. - - Args: - benchmark: The benchmark fixture. - app_with_10_components: The app harness. - """ - with chdir(app_with_10_components.app_path): - app_with_10_components._initialize_app() - build.setup_frontend(app_with_10_components.app_path) - - def benchmark_fn(): - with chdir(app_with_10_components.app_path): - app_with_10_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_100_components): - """Test the compile time on a cold start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - - def setup(): - with chdir(app_with_100_components.app_path): - utils.empty_dir(web_pages, ["_app.js"]) - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_100_components): - """Test the compile time on a warm start for an app with roughly 100 components. - - Args: - benchmark: The benchmark fixture. - app_with_100_components: The app harness. - """ - with chdir(app_with_100_components.app_path): - app_with_100_components._initialize_app() - build.setup_frontend(app_with_100_components.app_path) - - def benchmark_fn(): - with chdir(app_with_100_components.app_path): - app_with_100_components.app_instance._compile() - - benchmark(benchmark_fn) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_1000_components): - """Test the compile time on a cold start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - - def setup(): - with chdir(app_with_1000_components.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - - -@pytest.mark.benchmark( - group="Compile time of varying component numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_1000_components): - """Test the compile time on a warm start for an app with roughly 1000 components. - - Args: - benchmark: The benchmark fixture. - app_with_1000_components: The app harness. - """ - with chdir(app_with_1000_components.app_path): - app_with_1000_components._initialize_app() - build.setup_frontend(app_with_1000_components.app_path) - - def benchmark_fn(): - with chdir(app_with_1000_components.app_path): - app_with_1000_components.app_instance._compile() - - benchmark(benchmark_fn) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py deleted file mode 100644 index 149fc6130..000000000 --- a/benchmarks/test_benchmark_compile_pages.py +++ /dev/null @@ -1,579 +0,0 @@ -"""Benchmark tests for apps with varying page numbers.""" - -from __future__ import annotations - -import functools -import time -from typing import Generator - -import pytest - -from benchmarks import WINDOWS_SKIP_REASON -from reflex import constants -from reflex.compiler import utils -from reflex.testing import AppHarness, chdir -from reflex.utils import build -from reflex.utils.prerequisites import get_web_dir - -web_pages = get_web_dir() / constants.Dirs.PAGES - - -def render_multiple_pages(app, num: int): - """Add multiple pages based on num. - - Args: - app: The App object. - num: number of pages to render. - - """ - from typing import Tuple - - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - college = [ - "Stanford University", - "Arizona", - "Arizona state", - "Baylor", - "Boston College", - "Boston University", - ] - - class State(rx.State): - """The app state.""" - - position: str - college: str - age: Tuple[int, int] = (18, 50) - salary: Tuple[int, int] = (0, 25000000) - - comp1 = rx.center( - rx.theme_panel(), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - comp2 = rx.vstack( - rx.hstack( - rx.vstack( - rx.select( - ["C", "PF", "SF", "PG", "SG"], - placeholder="Select a position. (All)", - on_change=State.set_position, # pyright: ignore [reportAttributeAccessIssue] - size="3", - ), - rx.select( - college, - placeholder="Select a college. (All)", - on_change=State.set_college, # pyright: ignore [reportAttributeAccessIssue] - size="3", - ), - ), - rx.vstack( - rx.vstack( - rx.hstack( - rx.badge("Min Age: ", State.age[0]), - rx.divider(orientation="vertical"), - rx.badge("Max Age: ", State.age[1]), - ), - rx.slider( - default_value=[18, 50], - min=18, - max=50, - on_value_commit=State.set_age, # pyright: ignore [reportAttributeAccessIssue] - ), - align_items="left", - width="100%", - ), - rx.vstack( - rx.hstack( - rx.badge("Min Sal: ", State.salary[0] // 1000000, "M"), - rx.divider(orientation="vertical"), - rx.badge("Max Sal: ", State.salary[1] // 1000000, "M"), - ), - rx.slider( - default_value=[0, 25000000], - min=0, - max=25000000, - on_value_commit=State.set_salary, # pyright: ignore [reportAttributeAccessIssue] - ), - align_items="left", - width="100%", - ), - ), - spacing="4", - ), - width="100%", - ) - - for i in range(1, num + 1): - if i % 2 == 1: - app.add_page(comp1, route=f"page{i}") - else: - app.add_page(comp2, route=f"page{i}") - - -def AppWithOnePage(): - """A reflex app with one page.""" - from rxconfig import config # pyright: ignore [reportMissingImports] - - import reflex as rx - - docs_url = "https://reflex.dev/docs/getting-started/introduction/" - filename = f"{config.app_name}/{config.app_name}.py" - - class State(rx.State): - """The app state.""" - - pass - - def index() -> rx.Component: - return rx.center( - rx.input( - id="token", value=State.router.session.client_token, is_read_only=True - ), - rx.vstack( - rx.heading("Welcome to Reflex!", size="9"), - rx.text("Get started by editing ", rx.code(filename)), - rx.button( - "Check out our docs!", - on_click=lambda: rx.redirect(docs_url), - size="4", - ), - align="center", - spacing="7", - font_size="2em", - ), - height="100vh", - ) - - app = rx.App(_state=rx.State) - app.add_page(index) - - -def AppWithTenPages(): - """A reflex app with 10 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10) - - -def AppWithHundredPages(): - """A reflex app with 100 pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 100) - - -def AppWithThousandPages(): - """A reflex app with Thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 1000) - - -def AppWithTenThousandPages(): - """A reflex app with ten thousand pages.""" - import reflex as rx - - app = rx.App(_state=rx.State) - render_multiple_pages(app, 10000) - - -@pytest.fixture(scope="session") -def app_with_one_page( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1") - - yield AppHarness.create(root=root, app_source=AppWithOnePage) - - -@pytest.fixture(scope="session") -def app_with_ten_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app10") - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_hundred_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 100 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app100") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithHundredPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 1000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - an AppHarness instance - """ - root = tmp_path_factory.mktemp("app1000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.fixture(scope="session") -def app_with_ten_thousand_pages( - tmp_path_factory, -) -> Generator[AppHarness, None, None]: - """Create an app with 10000 pages at tmp_path via AppHarness. - - Args: - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - running AppHarness instance - """ - root = tmp_path_factory.mktemp("app10000") - - yield AppHarness.create( - root=root, - app_source=functools.partial( - AppWithTenThousandPages, - render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] - ), - ) - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_cold(benchmark, app_with_one_page): - """Test the compile time on a cold start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - - def setup(): - with chdir(app_with_one_page.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_one_page._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1_compile_time_warm(benchmark, app_with_one_page): - """Test the compile time on a warm start for an app with 1 page. - - Args: - benchmark: The benchmark fixture. - app_with_one_page: The app harness. - """ - with chdir(app_with_one_page.app_path): - app_with_one_page._initialize_app() - build.setup_frontend(app_with_one_page.app_path) - - def benchmark_fn(): - with chdir(app_with_one_page.app_path): - app_with_one_page.app_instance._compile() - - benchmark(benchmark_fn) - app_with_one_page._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_cold(benchmark, app_with_ten_pages): - """Test the compile time on a cold start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10_compile_time_warm(benchmark, app_with_ten_pages): - """Test the compile time on a warm start for an app with 10 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_pages: The app harness. - """ - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages._initialize_app() - build.setup_frontend(app_with_ten_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_pages.app_path): - app_with_ten_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages): - """Test the compile time on a cold start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - - def setup(): - with chdir(app_with_hundred_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages): - """Test the compile time on a warm start for an app with 100 page. - - Args: - benchmark: The benchmark fixture. - app_with_hundred_pages: The app harness. - """ - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages._initialize_app() - build.setup_frontend(app_with_hundred_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_hundred_pages.app_path): - app_with_hundred_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_hundred_pages._reload_state_module() - - -@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages): - """Test the compile time on a cold start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages): - """Test the compile time on a warm start for an app with 1000 page. - - Args: - benchmark: The benchmark fixture. - app_with_thousand_pages: The app harness. - """ - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages._initialize_app() - build.setup_frontend(app_with_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_thousand_pages.app_path): - app_with_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a cold start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def setup(): - with chdir(app_with_ten_thousand_pages.app_path): - utils.empty_dir(web_pages, keep_files=["_app.js"]) - app_with_ten_thousand_pages._initialize_app() - build.setup_frontend(app_with_ten_thousand_pages.app_path) - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark.pedantic(benchmark_fn, setup=setup, rounds=5) - app_with_ten_thousand_pages._reload_state_module() - - -@pytest.mark.skip -@pytest.mark.benchmark( - group="Compile time of varying page numbers", - min_rounds=5, - timer=time.perf_counter, - disable_gc=True, - warmup=False, -) -def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages): - """Test the compile time on a warm start for an app with 10000 page. - - Args: - benchmark: The benchmark fixture. - app_with_ten_thousand_pages: The app harness. - """ - - def benchmark_fn(): - with chdir(app_with_ten_thousand_pages.app_path): - app_with_ten_thousand_pages.app_instance._compile() - - benchmark(benchmark_fn) - app_with_ten_thousand_pages._reload_state_module() diff --git a/poetry.lock b/poetry.lock index f5007ee07..032bd2d4a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -403,75 +403,76 @@ markers = {main = "(platform_system == \"Windows\" or os_name == \"nt\") and (py [[package]] name = "coverage" -version = "7.6.10" +version = "7.6.12" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] [package.dependencies] @@ -482,40 +483,44 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "44.0.0" +version = "44.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ - {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, - {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, - {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, - {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, - {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, - {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, - {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, + {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, + {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, + {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, + {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, + {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, + {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, + {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, ] [package.dependencies] @@ -528,7 +533,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -852,15 +857,15 @@ test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] [[package]] name = "identify" -version = "2.6.6" +version = "2.6.7" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, - {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, + {file = "identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0"}, + {file = "identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684"}, ] [package.extras] @@ -1074,15 +1079,15 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "mako" -version = "1.3.8" +version = "1.3.9" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, - {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, + {file = "Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1"}, + {file = "mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac"}, ] [package.dependencies] @@ -1558,25 +1563,25 @@ type = ["mypy (>=1.11.2)"] [[package]] name = "playwright" -version = "1.49.1" +version = "1.50.0" description = "A high-level API to automate web browsers" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"}, - {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"}, - {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"}, - {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"}, - {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"}, - {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"}, + {file = "playwright-1.50.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f36d754a6c5bd9bf7f14e8f57a2aea6fd08f39ca4c8476481b9c83e299531148"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40f274384591dfd27f2b014596250b2250c843ed1f7f4ef5d2960ecb91b4961e"}, + {file = "playwright-1.50.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9922ef9bcd316995f01e220acffd2d37a463b4ad10fd73e388add03841dfa230"}, + {file = "playwright-1.50.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8fc628c492d12b13d1f347137b2ac6c04f98197ff0985ef0403a9a9ee0d39131"}, + {file = "playwright-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcff35f72db2689a79007aee78f1b0621a22e6e3d6c1f58aaa9ac805bf4497c"}, + {file = "playwright-1.50.0-py3-none-win32.whl", hash = "sha256:3b906f4d351260016a8c5cc1e003bb341651ae682f62213b50168ed581c7558a"}, + {file = "playwright-1.50.0-py3-none-win_amd64.whl", hash = "sha256:1859423da82de631704d5e3d88602d755462b0906824c1debe140979397d2e8d"}, ] [package.dependencies] -greenlet = "3.1.1" -pyee = "12.0.0" +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=12,<13" [[package]] name = "plotly" @@ -1828,15 +1833,15 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyee" -version = "12.0.0" +version = "12.1.1" description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, - {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, + {file = "pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef"}, + {file = "pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3"}, ] [package.dependencies] @@ -2311,15 +2316,15 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)" [[package]] name = "reflex-hosting-cli" -version = "0.1.34" +version = "0.1.35" description = "Reflex Hosting CLI" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "reflex_hosting_cli-0.1.34-py3-none-any.whl", hash = "sha256:eabc4dc7bf68e022a9388614c1a35b5ab36b01021df063d0c3356eda0e245264"}, - {file = "reflex_hosting_cli-0.1.34.tar.gz", hash = "sha256:07be37fda6dcede0a5d4bc1fd1786d9a3df5ad4e49dc1b6ba335418563cfecec"}, + {file = "reflex_hosting_cli-0.1.35-py3-none-any.whl", hash = "sha256:619687be27e6691cb54f6cf038e98d4d622fcf25a85bc9986f8daf52b48e6744"}, + {file = "reflex_hosting_cli-0.1.35.tar.gz", hash = "sha256:9a5d02978b900045464a1a5581f3adc6260daaa09e8acf95fd05024cda926ae7"}, ] [package.dependencies] @@ -2410,31 +2415,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.9.3" +version = "0.9.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"}, - {file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"}, - {file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"}, - {file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"}, - {file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"}, - {file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"}, - {file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"}, - {file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"}, - {file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"}, + {file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"}, + {file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"}, + {file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"}, + {file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"}, + {file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"}, + {file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"}, + {file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"}, ] [[package]] @@ -2571,70 +2576,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.37" +version = "2.0.38" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, - {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, - {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, - {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, - {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-win32.whl", hash = "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb"}, - {file = "SQLAlchemy-2.0.37-cp37-cp37m-win_amd64.whl", hash = "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-win32.whl", hash = "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f"}, - {file = "SQLAlchemy-2.0.37-cp38-cp38-win_amd64.whl", hash = "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278"}, - {file = "SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b"}, - {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, - {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, + {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, + {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, ] [package.dependencies] @@ -2999,15 +3004,15 @@ standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.29.1" +version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, - {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, + {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, + {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, ] [package.dependencies] @@ -3183,4 +3188,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10, <4.0" -content-hash = "3b7e6e6e872c68f951f191d85a7d76fe1dd86caf32e2143a53a3152a3686fc7f" +content-hash = "7ae644e1c5b910f4fd0d8ab0b530818077a96e5d329b2be1269e967c6b0b3d25" diff --git a/pyproject.toml b/pyproject.toml index b54f578fd..67611a867 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "reflex" -version = "0.7.0dev1" +version = "0.7.1dev1" description = "Web apps in pure Python." license = "Apache-2.0" authors = [ @@ -61,7 +61,7 @@ dill = ">=0.3.8" toml = ">=0.10.2,<1.0" pytest-asyncio = ">=0.24.0" pytest-cov = ">=4.0.0,<7.0" -ruff = "0.9.3" +ruff = "0.9.6" pandas = ">=2.1.1,<3.0" pillow = ">=10.0.0,<12.0" plotly = ">=5.13.0,<6.0" diff --git a/reflex/app.py b/reflex/app.py index 984428375..a09c867b6 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -11,17 +11,17 @@ import functools import inspect import io import json -import multiprocessing -import platform import sys import traceback from datetime import datetime from pathlib import Path +from timeit import default_timer as timer from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, AsyncIterator, + BinaryIO, Callable, Coroutine, Dict, @@ -35,12 +35,15 @@ from typing import ( get_type_hints, ) -from fastapi import FastAPI, HTTPException, Request, UploadFile +from fastapi import FastAPI, HTTPException, Request +from fastapi import UploadFile as FastAPIUploadFile from fastapi.middleware import cors from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from socketio import ASGIApp, AsyncNamespace, AsyncServer +from starlette.datastructures import Headers +from starlette.datastructures import UploadFile as StarletteUploadFile from starlette_admin.contrib.sqla.admin import Admin from starlette_admin.contrib.sqla.view import ModelView @@ -72,7 +75,7 @@ from reflex.components.core.client_side_routing import ( from reflex.components.core.sticky import sticky from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes -from reflex.config import environment, get_config +from reflex.config import ExecutorType, environment, get_config from reflex.event import ( _EVENT_FIELDS, Event, @@ -100,7 +103,15 @@ from reflex.state import ( all_base_state_classes, code_uses_state_contexts, ) -from reflex.utils import codespaces, console, exceptions, format, prerequisites, types +from reflex.utils import ( + codespaces, + console, + exceptions, + format, + path_ops, + prerequisites, + types, +) from reflex.utils.exec import is_prod_mode, is_testing_env from reflex.utils.imports import ImportVar @@ -158,11 +169,11 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: return window_alert("\n".join(error_message)) -def default_overlay_component() -> Component: - """Default overlay_component attribute for App. +def extra_overlay_function() -> Optional[Component]: + """Extra overlay function to add to the overlay component. Returns: - The default overlay_component, which is a connection_modal. + The extra overlay function. """ config = get_config() @@ -172,7 +183,8 @@ def default_overlay_component() -> Component: module, _, function_name = extra_config.rpartition(".") try: module = __import__(module) - config_overlay = getattr(module, function_name)() + config_overlay = Fragment.create(getattr(module, function_name)()) + config_overlay._get_all_imports() except Exception as e: from reflex.compiler.utils import save_error @@ -182,13 +194,27 @@ def default_overlay_component() -> Component: f"Error loading extra_overlay_function {extra_config}. Error saved to {log_path}" ) - return Fragment.create( - connection_pulser(), - connection_toaster(), - *([config_overlay] if config_overlay else []), - *([backend_disabled()] if config.is_reflex_cloud else []), - *codespaces.codespaces_auto_redirect(), - ) + return config_overlay + + +def default_overlay_component() -> Component: + """Default overlay_component attribute for App. + + Returns: + The default overlay_component, which is a connection_modal. + """ + config = get_config() + from reflex.components.component import memo + + def default_overlay_components(): + return Fragment.create( + connection_pulser(), + connection_toaster(), + *([backend_disabled()] if config.is_reflex_cloud else []), + *codespaces.codespaces_auto_redirect(), + ) + + return Fragment.create(memo(default_overlay_components)()) def default_error_boundary(*children: Component) -> Component: @@ -210,6 +236,53 @@ class OverlayFragment(Fragment): pass +@dataclasses.dataclass(frozen=True) +class UploadFile(StarletteUploadFile): + """A file uploaded to the server. + + Args: + file: The standard Python file object (non-async). + filename: The original file name. + size: The size of the file in bytes. + headers: The headers of the request. + """ + + file: BinaryIO + + path: Optional[Path] = dataclasses.field(default=None) + + _deprecated_filename: Optional[str] = dataclasses.field(default=None) + + size: Optional[int] = dataclasses.field(default=None) + + headers: Headers = dataclasses.field(default_factory=Headers) + + @property + def name(self) -> Optional[str]: + """Get the name of the uploaded file. + + Returns: + The name of the uploaded file. + """ + if self.path: + return self.path.name + + @property + def filename(self) -> Optional[str]: + """Get the filename of the uploaded file. + + Returns: + The filename of the uploaded file. + """ + console.deprecate( + feature_name="UploadFile.filename", + reason="Use UploadFile.name instead.", + deprecation_version="0.7.1", + removal_version="0.8.0", + ) + return self._deprecated_filename + + @dataclasses.dataclass( frozen=True, ) @@ -260,11 +333,26 @@ class App(MiddlewareMixin, LifespanMixin): # A component that is present on every page (defaults to the Connection Error banner). overlay_component: Optional[Union[Component, ComponentCallable]] = ( - dataclasses.field(default_factory=default_overlay_component) + dataclasses.field(default=None) ) # Error boundary component to wrap the app with. - error_boundary: Optional[ComponentCallable] = default_error_boundary + error_boundary: Optional[ComponentCallable] = dataclasses.field(default=None) + + # App wraps to be applied to the whole app. Expected to be a dictionary of (order, name) to a function that takes whether the state is enabled and optionally returns a component. + app_wraps: Dict[tuple[int, str], Callable[[bool], Optional[Component]]] = ( + dataclasses.field( + default_factory=lambda: { + (55, "ErrorBoundary"): ( + lambda stateful: default_error_boundary() if stateful else None + ), + (5, "Overlay"): ( + lambda stateful: default_overlay_component() if stateful else None + ), + (4, "ExtraOverlay"): lambda stateful: extra_overlay_function(), + } + ) + ) # Components to add to the head of every page. head_components: List[Component] = dataclasses.field(default_factory=list) @@ -322,6 +410,9 @@ class App(MiddlewareMixin, LifespanMixin): [Exception], Union[EventSpec, List[EventSpec], None] ] = default_backend_exception_handler + # Put the toast provider in the app wrap. + bundle_toaster: bool = True + @property def api(self) -> FastAPI | None: """Get the backend api. @@ -560,7 +651,9 @@ class App(MiddlewareMixin, LifespanMixin): Returns: The generated component. """ - return component if isinstance(component, Component) else component() + from reflex.compiler.compiler import into_component + + return into_component(component) def add_page( self, @@ -888,25 +981,6 @@ class App(MiddlewareMixin, LifespanMixin): for k, component in self._pages.items(): self._pages[k] = self._add_overlay_to_component(component) - def _add_error_boundary_to_component(self, component: Component) -> Component: - if self.error_boundary is None: - return component - - component = self.error_boundary(*component.children) - - return component - - def _setup_error_boundary(self): - """If a State is not used and no error_boundary is specified, do not render the error boundary.""" - if self._state is None and self.error_boundary is default_error_boundary: - self.error_boundary = None - - for k, component in self._pages.items(): - # Skip the 404 page - if k == constants.Page404.SLUG: - continue - self._pages[k] = self._add_error_boundary_to_component(component) - def _setup_sticky_badge(self): """Add the sticky badge to the app.""" for k, component in self._pages.items(): @@ -1021,9 +1095,14 @@ class App(MiddlewareMixin, LifespanMixin): should_compile = self._should_compile() if not should_compile: - for route in self._unevaluated_pages: - console.debug(f"Evaluating page: {route}") - self._compile_page(route, save_page=should_compile) + if self.bundle_toaster: + from reflex.components.sonner.toast import Toaster + + Toaster.is_used = True + with console.timing("Evaluate Pages (Backend)"): + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + self._compile_page(route, save_page=should_compile) # Add the optional endpoints (_upload) self._add_optional_endpoints() @@ -1049,24 +1128,51 @@ class App(MiddlewareMixin, LifespanMixin): + adhoc_steps_without_executor, ) - for route in self._unevaluated_pages: - console.debug(f"Evaluating page: {route}") - self._compile_page(route, save_page=should_compile) - progress.advance(task) + if self.bundle_toaster: + from reflex.components.component import memo + from reflex.components.sonner.toast import toast + + internal_toast_provider = toast.provider() + + @memo + def memoized_toast_provider(): + return internal_toast_provider + + toast_provider = Fragment.create(memoized_toast_provider()) + + app_wrappers[(1, "ToasterProvider")] = toast_provider + + with console.timing("Evaluate Pages (Frontend)"): + performance_metrics: list[tuple[str, float]] = [] + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + start = timer() + self._compile_page(route, save_page=should_compile) + end = timer() + performance_metrics.append((route, end - start)) + progress.advance(task) + console.debug( + "Slowest pages:\n" + + "\n".join( + f"{route}: {time * 1000:.1f}ms" + for route, time in sorted( + performance_metrics, key=lambda x: x[1], reverse=True + )[:10] + ) + ) # Add the optional endpoints (_upload) self._add_optional_endpoints() self._validate_var_dependencies() self._setup_overlay_component() - self._setup_error_boundary() - if config.show_built_with_reflex: + if is_prod_mode() and config.show_built_with_reflex: self._setup_sticky_badge() progress.advance(task) # Store the compile results. - compile_results = [] + compile_results: list[tuple[str, str]] = [] progress.advance(task) @@ -1086,14 +1192,32 @@ class App(MiddlewareMixin, LifespanMixin): # Add the custom components from the page to the set. custom_components |= component._get_all_custom_components() - # Perform auto-memoization of stateful components. - ( - stateful_components_path, - stateful_components_code, - page_components, - ) = compiler.compile_stateful_components(self._pages.values()) + # Add the app wraps to the app. + for key, app_wrap in self.app_wraps.items(): + component = app_wrap(self._state is not None) + if component is not None: + app_wrappers[key] = component - progress.advance(task) + for component in app_wrappers.values(): + custom_components |= component._get_all_custom_components() + + if self.error_boundary: + console.deprecate( + feature_name="App.error_boundary", + reason="Use app_wraps instead.", + deprecation_version="0.7.1", + removal_version="0.8.0", + ) + app_wrappers[(55, "ErrorBoundary")] = self.error_boundary() + + # Perform auto-memoization of stateful components. + with console.timing("Auto-memoize StatefulComponents"): + ( + stateful_components_path, + stateful_components_code, + page_components, + ) = compiler.compile_stateful_components(self._pages.values()) + progress.advance(task) # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State. if code_uses_state_contexts(stateful_components_code) and self._state is None: @@ -1116,33 +1240,30 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) - # Use a forking process pool, if possible. Much faster, especially for large sites. - # Fallback to ThreadPoolExecutor as something that will always work. - executor = None - if ( - platform.system() in ("Linux", "Darwin") - and (number_of_processes := environment.REFLEX_COMPILE_PROCESSES.get()) - is not None - ): - executor = concurrent.futures.ProcessPoolExecutor( - max_workers=number_of_processes or None, - mp_context=multiprocessing.get_context("fork"), - ) - else: - executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() or None - ) + # Copy the assets. + assets_src = Path.cwd() / constants.Dirs.APP_ASSETS + if assets_src.is_dir(): + with console.timing("Copy assets"): + path_ops.update_directory_tree( + src=assets_src, + dest=( + Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC + ), + ) + + executor = ExecutorType.get_executor_from_environment() for route, component in zip(self._pages, page_components, strict=True): ExecutorSafeFunctions.COMPONENTS[route] = component ExecutorSafeFunctions.STATE = self._state - with executor: - result_futures = [] + with console.timing("Compile to Javascript"), executor as executor: + result_futures: list[concurrent.futures.Future[tuple[str, str]]] = [] - def _submit_work(fn: Callable, *args, **kwargs): + def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs): f = executor.submit(fn, *args, **kwargs) + f.add_done_callback(lambda _: progress.advance(task)) result_futures.append(f) # Compile the pre-compiled pages. @@ -1168,9 +1289,10 @@ class App(MiddlewareMixin, LifespanMixin): _submit_work(compiler.remove_tailwind_from_postcss) # Wait for all compilation tasks to complete. - for future in concurrent.futures.as_completed(result_futures): - compile_results.append(future.result()) - progress.advance(task) + compile_results.extend( + future.result() + for future in concurrent.futures.as_completed(result_futures) + ) app_root = self._app_root(app_wrappers=app_wrappers) @@ -1195,17 +1317,20 @@ class App(MiddlewareMixin, LifespanMixin): progress.advance(task) # Compile custom components. - *custom_components_result, custom_components_imports = ( - compiler.compile_components(custom_components) - ) - compile_results.append(custom_components_result) + ( + custom_components_output, + custom_components_result, + custom_components_imports, + ) = compiler.compile_components(custom_components) + compile_results.append((custom_components_output, custom_components_result)) all_imports.update(custom_components_imports) progress.advance(task) progress.stop() # Install frontend packages. - self._get_frontend_packages(all_imports) + with console.timing("Install Frontend Packages"): + self._get_frontend_packages(all_imports) # Setup the next.config.js transpile_packages = [ @@ -1231,8 +1356,9 @@ class App(MiddlewareMixin, LifespanMixin): # Remove pages that are no longer in the app. p.unlink() - for output_path, code in compile_results: - compiler_utils.write_page(output_path, code) + with console.timing("Write to Disk"): + for output_path, code in compile_results: + compiler_utils.write_page(output_path, code) # Pickle dynamic states if self._state is not None: @@ -1549,7 +1675,7 @@ def upload(app: App): The upload function. """ - async def upload_file(request: Request, files: List[UploadFile]): + async def upload_file(request: Request, files: List[FastAPIUploadFile]): """Upload a file. Args: @@ -1625,7 +1751,8 @@ def upload(app: App): file_copies.append( UploadFile( file=content_copy, - filename=file.filename, + path=Path(file.filename.lstrip("/")) if file.filename else None, + _deprecated_filename=file.filename, size=file.size, headers=file.headers, ) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index c2a76aad3..81de50182 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Tuple, Type, Union from reflex import constants from reflex.compiler import templates, utils @@ -78,6 +78,7 @@ def _compile_app(app_root: Component) -> str: hooks=app_root._get_all_hooks(), window_libraries=window_libraries, render=app_root.render(), + dynamic_imports=app_root._get_all_dynamic_imports(), ) @@ -507,7 +508,7 @@ def compile_tailwind( The compiled Tailwind config. """ # Get the path for the output file. - output_path = get_web_dir() / constants.Tailwind.CONFIG + output_path = str((get_web_dir() / constants.Tailwind.CONFIG).absolute()) # Compile the config. code = _compile_tailwind(config) @@ -544,7 +545,47 @@ def purge_web_pages_dir(): if TYPE_CHECKING: - from reflex.app import UnevaluatedPage + from reflex.app import ComponentCallable, UnevaluatedPage + + +def _into_component_once(component: Component | ComponentCallable) -> Component | None: + """Convert a component to a Component. + + Args: + component: The component to convert. + + Returns: + The converted component. + """ + if isinstance(component, Component): + return component + if isinstance(component, (Var, int, float, str)): + return Fragment.create(component) + if isinstance(component, Sequence): + return Fragment.create(*component) + return None + + +def into_component(component: Component | ComponentCallable) -> Component: + """Convert a component to a Component. + + Args: + component: The component to convert. + + Returns: + The converted component. + + Raises: + TypeError: If the component is not a Component. + """ + if (converted := _into_component_once(component)) is not None: + return converted + if ( + callable(component) + and (converted := _into_component_once(component())) is not None + ): + return converted + raise TypeError(f"Expected a Component, got {type(component)}") def compile_unevaluated_page( @@ -567,12 +608,7 @@ def compile_unevaluated_page( The compiled component and whether state should be enabled. """ # Generate the component if it is a callable. - component = page.component - component = component if isinstance(component, Component) else component() - - # unpack components that return tuples in an rx.fragment. - if isinstance(component, tuple): - component = Fragment.create(*component) + component = into_component(page.component) component._add_style_recursive(style or {}, theme) @@ -677,10 +713,8 @@ class ExecutorSafeFunctions: The route, compiled component, and compiled page. """ component, enable_state = compile_unevaluated_page( - route, cls.UNCOMPILED_PAGES[route] + route, cls.UNCOMPILED_PAGES[route], cls.STATE, style, theme ) - component = component if isinstance(component, Component) else component() - component._add_style_recursive(style, theme) return route, component, compile_page(route, component, cls.STATE) @classmethod diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 117b655a9..e85e5fe6d 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -48,11 +48,10 @@ class ReflexJinjaEnvironment(Environment): def __init__(self) -> None: """Set default environment.""" - extensions = ["jinja2.ext.debug"] super().__init__( - extensions=extensions, trim_blocks=True, lstrip_blocks=True, + auto_reload=False, ) self.filters["json_dumps"] = json_dumps self.filters["react_setter"] = lambda state: f"set{state.capitalize()}" diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index c797a095f..c66dfe304 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -119,24 +119,34 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: validate_imports(collapsed_import_dict) import_dicts = [] for lib, fields in collapsed_import_dict.items(): - default, rest = compile_import_statement(fields) - # prevent lib from being rendered on the page if all imports are non rendered kind if not any(f.render for f in fields): continue - if not lib: - if default: - raise ValueError("No default field allowed for empty library.") - if rest is None or len(rest) == 0: - raise ValueError("No fields to import.") - import_dicts.extend(get_import_dict(module) for module in sorted(rest)) - continue + lib_paths: dict[str, list[ImportVar]] = {} - # remove the version before rendering the package imports - lib = format.format_library_name(lib) + for field in fields: + lib_paths.setdefault(field.package_path, []).append(field) - import_dicts.append(get_import_dict(lib, default, rest)) + compiled = { + path: compile_import_statement(fields) for path, fields in lib_paths.items() + } + + for path, (default, rest) in compiled.items(): + if not lib: + if default: + raise ValueError("No default field allowed for empty library.") + if rest is None or len(rest) == 0: + raise ValueError("No fields to import.") + import_dicts.extend(get_import_dict(module) for module in sorted(rest)) + continue + + # remove the version before rendering the package imports + formatted_lib = format.format_library_name(lib) + ( + path if path != "/" else "" + ) + + import_dicts.append(get_import_dict(formatted_lib, default, rest)) return import_dicts @@ -513,6 +523,8 @@ def write_page(path: str | Path, code: str): """ path = Path(path) path_ops.mkdir(path.parent) + if path.exists() and path.read_text(encoding="utf-8") == code: + return path.write_text(code, encoding="utf-8") diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index 0f0bef8b9..73b0680d3 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -7,9 +7,44 @@ from typing import Any, Iterator from reflex.components.component import Component, LiteralComponentVar from reflex.components.tags import Tag from reflex.components.tags.tagless import Tagless +from reflex.config import PerformanceMode, environment +from reflex.utils import console +from reflex.utils.decorator import once from reflex.utils.imports import ParsedImportDict from reflex.vars import BooleanVar, ObjectVar, Var from reflex.vars.base import VarData +from reflex.vars.sequence import LiteralStringVar + + +@once +def get_performance_mode(): + """Get the performance mode. + + Returns: + The performance mode. + """ + return environment.REFLEX_PERF_MODE.get() + + +def validate_str(value: str): + """Validate a string value. + + Args: + value: The value to validate. + + Raises: + ValueError: If the value is a Var and the performance mode is set to raise. + """ + perf_mode = get_performance_mode() + if perf_mode != PerformanceMode.OFF and value.startswith("reflex___state"): + if perf_mode == PerformanceMode.WARN: + console.warn( + f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead." + ) + elif perf_mode == PerformanceMode.RAISE: + raise ValueError( + f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead." + ) class Bare(Component): @@ -28,9 +63,14 @@ class Bare(Component): The component. """ if isinstance(contents, Var): + if isinstance(contents, LiteralStringVar): + validate_str(contents._var_value) return cls(contents=contents) else: + if isinstance(contents, str): + validate_str(contents) contents = str(contents) if contents is not None else "" + return cls(contents=contents) def _get_all_hooks_internal(self) -> dict[str, VarData | None]: diff --git a/reflex/components/component.py b/reflex/components/component.py index 6d1264f4d..005f7791d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -23,6 +23,8 @@ from typing import ( Union, ) +from typing_extensions import Self + import reflex.state from reflex.base import Base from reflex.compiler.templates import STATEFUL_COMPONENT @@ -49,13 +51,7 @@ from reflex.event import ( ) from reflex.style import Style, format_as_emotion from reflex.utils import format, imports, types -from reflex.utils.imports import ( - ImmutableParsedImportDict, - ImportDict, - ImportVar, - ParsedImportDict, - parse_imports, -) +from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.vars import VarData from reflex.vars.base import ( CachedVarOperation, @@ -179,6 +175,7 @@ ComponentStyle = Dict[ Union[str, Type[BaseComponent], Callable, ComponentNamespace], Any ] ComponentChild = Union[types.PrimitiveType, Var, BaseComponent] +ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent) def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: @@ -191,11 +188,7 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: Returns: Whether the object satisfies the type hint. """ - if isinstance(obj, LiteralVar): - return types._isinstance(obj._var_value, type_hint) - if isinstance(obj, Var): - return types._issubclass(obj._var_type, type_hint) - return types._isinstance(obj, type_hint) + return types._isinstance(obj, type_hint, nested=1) class Component(BaseComponent, ABC): @@ -688,7 +681,7 @@ class Component(BaseComponent, ABC): } @classmethod - def create(cls, *children, **props) -> Component: + def create(cls, *children, **props) -> Self: """Create the component. Args: @@ -712,8 +705,8 @@ class Component(BaseComponent, ABC): validate_children(child) # Make sure the child is a valid type. - if isinstance(child, dict) or not types._isinstance( - child, ComponentChild + if isinstance(child, dict) or not isinstance( + child, ComponentChildTypes ): raise ChildrenTypeError(component=cls.__name__, child=child) @@ -1209,7 +1202,7 @@ class Component(BaseComponent, ABC): Returns: True if the dependency should be transpiled. """ - return ( + return bool(self.transpile_packages) and ( dep in self.transpile_packages or format.format_library_name(dep or "") in self.transpile_packages ) @@ -1292,9 +1285,10 @@ class Component(BaseComponent, ABC): event_imports = Imports.EVENTS if self.event_triggers else {} # Collect imports from Vars used directly by this component. - var_datas = [var._get_all_var_data() for var in self._get_vars()] - var_imports: List[ImmutableParsedImportDict] = [ - var_data.imports for var_data in var_datas if var_data is not None + var_imports = [ + var_data.imports + for var in self._get_vars() + if (var_data := var._get_all_var_data()) is not None ] added_import_dicts: list[ParsedImportDict] = [] @@ -1771,9 +1765,7 @@ class CustomComponent(Component): return [ Var( _js_expr=name, - _var_type=( - prop._var_type if types._isinstance(prop, Var) else type(prop) - ), + _var_type=(prop._var_type if isinstance(prop, Var) else type(prop)), ).guess_type() for name, prop in self.props.items() ] @@ -1795,9 +1787,6 @@ class CustomComponent(Component): include_children=include_children, ignore_ids=ignore_ids ) yield from filter(lambda prop: isinstance(prop, Var), self.props.values()) - yield from self.get_component(self)._get_vars( - include_children=include_children, ignore_ids=ignore_ids - ) @lru_cache(maxsize=None) # noqa: B019 def get_component(self) -> Component: diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index e9222b200..13db48575 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -54,9 +54,10 @@ class Foreach(Component): TypeError: If the render function is a ComponentState. UntypedVarError: If the iterable is of type Any without a type annotation. """ - from reflex.vars.object import ObjectVar + from reflex.vars import ArrayVar, ObjectVar, StringVar + + iterable = LiteralVar.create(iterable).guess_type() - iterable = LiteralVar.create(iterable) if iterable._var_type == Any: raise ForeachVarError( f"Could not foreach over var `{iterable!s}` of type Any. " @@ -75,6 +76,15 @@ class Foreach(Component): if isinstance(iterable, ObjectVar): iterable = iterable.entries() + if isinstance(iterable, StringVar): + iterable = iterable.split() + + if not isinstance(iterable, ArrayVar): + raise ForeachVarError( + f"Could not foreach over var `{iterable!s}` of type {iterable._var_type}. " + "See https://reflex.dev/docs/library/dynamic-rendering/foreach/" + ) + component = cls( iterable=iterable, render_fn=render_fn, diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index 5c31669a1..2d936544a 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -178,9 +178,9 @@ class Match(MemoizationLeaf): first_case_return = match_cases[0][-1] return_type = type(first_case_return) - if types._isinstance(first_case_return, BaseComponent): + if isinstance(first_case_return, BaseComponent): return_type = BaseComponent - elif types._isinstance(first_case_return, Var): + elif isinstance(first_case_return, Var): return_type = Var for index, case in enumerate(match_cases): @@ -228,8 +228,8 @@ class Match(MemoizationLeaf): # Validate the match cases (as well as the default case) to have Var return types. if any( - case for case in match_cases if not types._isinstance(case[-1], Var) - ) or not types._isinstance(default, Var): + case for case in match_cases if not isinstance(case[-1], Var) + ) or not isinstance(default, Var): raise ValueError("Return types of match cases should be Vars.") return Var( diff --git a/reflex/components/core/sticky.py b/reflex/components/core/sticky.py index 162bab3cd..b5dd4bcfd 100644 --- a/reflex/components/core/sticky.py +++ b/reflex/components/core/sticky.py @@ -2,14 +2,12 @@ from reflex.components.component import ComponentNamespace from reflex.components.core.colors import color -from reflex.components.core.cond import color_mode_cond, cond -from reflex.components.core.responsive import tablet_and_desktop +from reflex.components.core.cond import color_mode_cond +from reflex.components.core.responsive import desktop_only from reflex.components.el.elements.inline import A from reflex.components.el.elements.media import Path, Rect, Svg from reflex.components.radix.themes.typography.text import Text -from reflex.experimental.client_state import ClientStateVar from reflex.style import Style -from reflex.vars.base import Var, VarData class StickyLogo(Svg): @@ -87,7 +85,7 @@ class StickyBadge(A): """ return super().create( StickyLogo.create(), - tablet_and_desktop(StickyLabel.create()), + desktop_only(StickyLabel.create()), href="https://reflex.dev", target="_blank", width="auto", @@ -102,36 +100,12 @@ class StickyBadge(A): Returns: The style of the component. """ - is_localhost_cs = ClientStateVar.create( - "is_localhost", - default=True, - global_ref=False, - ) - localhost_hostnames = Var.create( - ["localhost", "127.0.0.1", "[::1]"] - ).guess_type() - is_localhost_expr = localhost_hostnames.contains( - Var("window.location.hostname", _var_type=str).guess_type(), - ) - check_is_localhost = Var( - f"useEffect(({is_localhost_cs}) => {is_localhost_cs.set}({is_localhost_expr}), [])", - _var_data=VarData( - imports={"react": "useEffect"}, - ), - ) - is_localhost = is_localhost_cs.value._replace( - merge_var_data=VarData.merge( - check_is_localhost._get_all_var_data(), - VarData(hooks={str(check_is_localhost): None}), - ), - ) return Style( { "position": "fixed", "bottom": "1rem", "right": "1rem", - # Do not show the badge on localhost. - "display": cond(is_localhost, "none", "flex"), + "display": "flex", "flex-direction": "row", "gap": "0.375rem", "align-items": "center", diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 897b89608..6c86d3c44 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -29,7 +29,7 @@ from reflex.event import ( from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var, get_unique_variable_name +from reflex.vars.base import Var, get_unique_variable_name from reflex.vars.sequence import LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -45,7 +45,6 @@ upload_files_context_var_data: VarData = VarData( ) -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the file upload drop trigger. @@ -75,7 +74,6 @@ def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ) -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: """Get the list of selected files. diff --git a/reflex/components/core/upload.pyi b/reflex/components/core/upload.pyi index 6ed96a15e..d1ddceb4d 100644 --- a/reflex/components/core/upload.pyi +++ b/reflex/components/core/upload.pyi @@ -13,14 +13,12 @@ from reflex.event import CallableEventSpec, EventSpec, EventType from reflex.style import Style from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, Var +from reflex.vars.base import Var DEFAULT_UPLOAD_ID: str upload_files_context_var_data: VarData -@CallableVar def upload_file(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... -@CallableVar def selected_files(id_: str = DEFAULT_UPLOAD_ID) -> Var: ... @CallableEventSpec def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: ... diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 4f1eb493e..3e4794482 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +import typing from typing import ClassVar, Dict, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace @@ -503,7 +504,7 @@ class CodeBlock(Component, MarkdownComponentMap): return ["can_copy", "copy_button"] @classmethod - def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str: + def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> Var: """Get the hook to register the language. Args: @@ -514,21 +515,46 @@ class CodeBlock(Component, MarkdownComponentMap): Returns: The hook to register the language. """ - return f""" - if ({language_var!s}) {{ - (async () => {{ - try {{ + language_in_there = Var.create(typing.get_args(LiteralCodeLanguage)).contains( + language_var + ) + async_load = f""" +(async () => {{ + try {{ const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`); SyntaxHighlighter.registerLanguage({language_var!s}, module.default); - }} catch (error) {{ - console.error(`Error importing language module for ${{{language_var!s}}}:`, error); - }} - }})(); + }} catch (error) {{ + console.error(`Language ${{{language_var!s}}} is not supported for code blocks inside of markdown: `, error); + }} +}})(); +""" + return Var( + f""" + if ({language_var!s}) {{ + if (!{language_in_there!s}) {{ + console.warn(`Language \\`${{{language_var!s}}}\\` is not supported for code blocks inside of markdown.`); + {language_var!s} = ''; + }} else {{ + {async_load!s} + }} }} """ + if not isinstance(language_var, LiteralVar) + else f""" +if ({language_var!s}) {{ + {async_load!s} +}}""", + _var_data=VarData( + imports={ + cls.__fields__["library"].default: [ + ImportVar(tag="PrismAsyncLight", alias="SyntaxHighlighter") + ] + }, + ), + ) @classmethod - def get_component_map_custom_code(cls) -> str: + def get_component_map_custom_code(cls) -> Var: """Get the custom code for the component. Returns: diff --git a/reflex/components/datadisplay/code.pyi b/reflex/components/datadisplay/code.pyi index fc35092fe..fda92a974 100644 --- a/reflex/components/datadisplay/code.pyi +++ b/reflex/components/datadisplay/code.pyi @@ -984,7 +984,7 @@ class CodeBlock(Component, MarkdownComponentMap): def add_style(self): ... @classmethod - def get_component_map_custom_code(cls) -> str: ... + def get_component_map_custom_code(cls) -> Var: ... def add_hooks(self) -> list[str | Var]: ... class CodeblockNamespace(ComponentNamespace): diff --git a/reflex/components/lucide/icon.py b/reflex/components/lucide/icon.py index 6c7cbede7..ea6efe133 100644 --- a/reflex/components/lucide/icon.py +++ b/reflex/components/lucide/icon.py @@ -4,7 +4,7 @@ from reflex.components.component import Component from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars.base import LiteralVar, Var -from reflex.vars.sequence import LiteralStringVar +from reflex.vars.sequence import LiteralStringVar, StringVar class LucideIconComponent(Component): @@ -40,7 +40,12 @@ class Icon(LucideIconComponent): The created component. """ if children: - if len(children) == 1 and isinstance(children[0], str): + if len(children) == 1: + child = Var.create(children[0]).guess_type() + if not isinstance(child, StringVar): + raise AttributeError( + f"Icon name must be a string, got {children[0]._var_type if isinstance(children[0], Var) else children[0]}" + ) props["tag"] = children[0] else: raise AttributeError( @@ -49,22 +54,33 @@ class Icon(LucideIconComponent): if "tag" not in props: raise AttributeError("Missing 'tag' keyword-argument for Icon") - tag: str | Var | LiteralVar = props.pop("tag") + tag: str | Var | LiteralVar = Var.create(props.pop("tag")) if isinstance(tag, LiteralVar): if isinstance(tag, LiteralStringVar): tag = tag._var_value else: raise TypeError(f"Icon name must be a string, got {type(tag)}") elif isinstance(tag, Var): - return DynamicIcon.create(name=tag, **props) + tag_stringified = tag.guess_type() + if not isinstance(tag_stringified, StringVar): + raise TypeError(f"Icon name must be a string, got {tag._var_type}") + return DynamicIcon.create(name=tag_stringified.replace("_", "-"), **props) if ( not isinstance(tag, str) or format.to_snake_case(tag) not in LUCIDE_ICON_LIST ): + if isinstance(tag, str): + icons_sorted = sorted( + LUCIDE_ICON_LIST, + key=lambda s: format.length_of_largest_common_substring(tag, s), + reverse=True, + ) + else: + icons_sorted = LUCIDE_ICON_LIST raise ValueError( - f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(LUCIDE_ICON_LIST[0:25])}, ..." - "\nSee full list at https://lucide.dev/icons." + f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(icons_sorted[0:25])}, ..." + "\nSee full list at https://reflex.dev/docs/library/data-display/icon/#icons-list." ) if tag in LUCIDE_ICON_MAPPING_OVERRIDE: diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 27bd5bd62..51d3dd3dd 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -6,13 +6,12 @@ import dataclasses import textwrap from functools import lru_cache from hashlib import md5 -from typing import Any, Callable, Dict, Sequence, Union +from typing import Any, Callable, Dict, Sequence from reflex.components.component import BaseComponent, Component, CustomComponent from reflex.components.tags.tag import Tag -from reflex.utils import types from reflex.utils.imports import ImportDict, ImportVar -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import LiteralVar, Var, VarData from reflex.vars.function import ARRAY_ISARRAY, ArgsFunctionOperation, DestructuredArg from reflex.vars.number import ternary_operation @@ -83,13 +82,13 @@ class MarkdownComponentMap: _explicit_return: bool = dataclasses.field(default=False) @classmethod - def get_component_map_custom_code(cls) -> str: + def get_component_map_custom_code(cls) -> Var: """Get the custom code for the component map. Returns: The custom code for the component map. """ - return "" + return Var("") @classmethod def create_map_fn_var( @@ -97,6 +96,7 @@ class MarkdownComponentMap: fn_body: Var | None = None, fn_args: Sequence[str] | None = None, explicit_return: bool | None = None, + var_data: VarData | None = None, ) -> Var: """Create a function Var for the component map. @@ -104,6 +104,7 @@ class MarkdownComponentMap: fn_body: The formatted component as a string. fn_args: The function arguments. explicit_return: Whether to use explicit return syntax. + var_data: The var data for the function. Returns: The function Var for the component map. @@ -116,6 +117,7 @@ class MarkdownComponentMap: args_names=(DestructuredArg(fields=tuple(fn_args)),), return_expr=fn_body, explicit_return=explicit_return, + _var_data=var_data, ) @classmethod @@ -166,7 +168,7 @@ class Markdown(Component): Returns: The markdown component. """ - if len(children) != 1 or not types._isinstance(children[0], Union[str, Var]): + if len(children) != 1 or not isinstance(children[0], (str, Var)): raise ValueError( "Markdown component must have exactly one child containing the markdown source." ) @@ -239,6 +241,15 @@ class Markdown(Component): component(_MOCK_ARG)._get_all_imports() for component in self.component_map.values() ], + *( + [inline_code_var_data.old_school_imports()] + if ( + inline_code_var_data + := self._get_inline_code_fn_var()._get_all_var_data() + ) + is not None + else [] + ), ] def _get_tag_map_fn_var(self, tag: str) -> Var: @@ -278,12 +289,20 @@ class Markdown(Component): self._get_map_fn_custom_code_from_children(self.get_component("code")) ) - codeblock_custom_code = "\n".join(custom_code_list) + var_data = VarData.merge( + *[ + code._get_all_var_data() + for code in custom_code_list + if isinstance(code, Var) + ] + ) + + codeblock_custom_code = "\n".join(map(str, custom_code_list)) # Format the code to handle inline and block code. formatted_code = f""" const match = (className || '').match(/language-(?.*)/); -const {_LANGUAGE!s} = match ? match[1] : ''; +let {_LANGUAGE!s} = match ? match[1] : ''; {codeblock_custom_code}; return inline ? ( {self.format_component("code")} @@ -302,6 +321,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; ), fn_body=Var(_js_expr=formatted_code), explicit_return=True, + var_data=var_data, ) def get_component(self, tag: str, **props) -> Component: @@ -381,7 +401,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; def _get_map_fn_custom_code_from_children( self, component: BaseComponent - ) -> list[str]: + ) -> list[str | Var]: """Recursively get markdown custom code from children components. Args: @@ -390,7 +410,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; Returns: A list of markdown custom code strings. """ - custom_code_list = [] + custom_code_list: list[str | Var] = [] if isinstance(component, MarkdownComponentMap): custom_code_list.append(component.get_component_map_custom_code()) diff --git a/reflex/components/markdown/markdown.pyi b/reflex/components/markdown/markdown.pyi index 606780a7a..61ddee094 100644 --- a/reflex/components/markdown/markdown.pyi +++ b/reflex/components/markdown/markdown.pyi @@ -11,7 +11,7 @@ from reflex.components.component import Component from reflex.event import EventType from reflex.style import Style from reflex.utils.imports import ImportDict -from reflex.vars.base import LiteralVar, Var +from reflex.vars.base import LiteralVar, Var, VarData _CHILDREN = Var(_js_expr="children", _var_type=str) _PROPS = Var(_js_expr="...props") @@ -32,13 +32,14 @@ def get_base_component_map() -> dict[str, Callable]: ... @dataclasses.dataclass() class MarkdownComponentMap: @classmethod - def get_component_map_custom_code(cls) -> str: ... + def get_component_map_custom_code(cls) -> Var: ... @classmethod def create_map_fn_var( cls, fn_body: Var | None = None, fn_args: Sequence[str] | None = None, explicit_return: bool | None = None, + var_data: VarData | None = None, ) -> Var: ... @classmethod def get_fn_args(cls) -> Sequence[str]: ... diff --git a/reflex/components/plotly/__init__.py b/reflex/components/plotly/__init__.py index 5620d5fc4..8743b31b2 100644 --- a/reflex/components/plotly/__init__.py +++ b/reflex/components/plotly/__init__.py @@ -1,5 +1,32 @@ """Plotly components.""" -from .plotly import Plotly +from reflex.components.component import ComponentNamespace -plotly = Plotly.create +from .plotly import ( + Plotly, + PlotlyBasic, + PlotlyCartesian, + PlotlyFinance, + PlotlyGeo, + PlotlyGl2d, + PlotlyGl3d, + PlotlyMapbox, + PlotlyStrict, +) + + +class PlotlyNamespace(ComponentNamespace): + """Plotly namespace.""" + + __call__ = Plotly.create + basic = PlotlyBasic.create + cartesian = PlotlyCartesian.create + geo = PlotlyGeo.create + gl2d = PlotlyGl2d.create + gl3d = PlotlyGl3d.create + finance = PlotlyFinance.create + mapbox = PlotlyMapbox.create + strict = PlotlyStrict.create + + +plotly = PlotlyNamespace() diff --git a/reflex/components/plotly/plotly.py b/reflex/components/plotly/plotly.py index c85423d35..2ddaad8d7 100644 --- a/reflex/components/plotly/plotly.py +++ b/reflex/components/plotly/plotly.py @@ -10,6 +10,7 @@ from reflex.components.component import Component, NoSSRComponent from reflex.components.core.cond import color_mode_cond from reflex.event import EventHandler, no_args_event_spec from reflex.utils import console +from reflex.utils.imports import ImportDict, ImportVar from reflex.vars.base import LiteralVar, Var try: @@ -278,3 +279,237 @@ const extractPoints = (points) => { # Spread the figure dict over props, nothing to merge. tag.special_props.append(Var(_js_expr=f"{{...{figure!s}}}")) return tag + + +CREATE_PLOTLY_COMPONENT: ImportDict = { + "react-plotly.js": [ + ImportVar( + tag="createPlotlyComponent", + is_default=True, + package_path="/factory", + ), + ] +} + + +def dynamic_plotly_import(name: str, package: str) -> str: + """Create a dynamic import for a plotly component. + + Args: + name: The name of the component. + package: The package path of the component. + + Returns: + The dynamic import for the plotly component. + """ + return f""" +const {name} = dynamic(() => import('{package}').then(mod => createPlotlyComponent(mod)), {{ssr: false}}) +""" + + +class PlotlyBasic(Plotly): + """Display a basic plotly graph.""" + + tag: str = "BasicPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-basic-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly basic component. + + Returns: + The imports for the plotly basic component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly basic component. + + Returns: + The dynamic imports for the plotly basic component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-basic-dist-min") + + +class PlotlyCartesian(Plotly): + """Display a plotly cartesian graph.""" + + tag: str = "CartesianPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-cartesian-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly cartesian component. + + Returns: + The imports for the plotly cartesian component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly cartesian component. + + Returns: + The dynamic imports for the plotly cartesian component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-cartesian-dist-min") + + +class PlotlyGeo(Plotly): + """Display a plotly geo graph.""" + + tag: str = "GeoPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-geo-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly geo component. + + Returns: + The imports for the plotly geo component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly geo component. + + Returns: + The dynamic imports for the plotly geo component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-geo-dist-min") + + +class PlotlyGl3d(Plotly): + """Display a plotly 3d graph.""" + + tag: str = "Gl3dPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-gl3d-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly 3d component. + + Returns: + The imports for the plotly 3d component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly 3d component. + + Returns: + The dynamic imports for the plotly 3d component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-gl3d-dist-min") + + +class PlotlyGl2d(Plotly): + """Display a plotly 2d graph.""" + + tag: str = "Gl2dPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-gl2d-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly 2d component. + + Returns: + The imports for the plotly 2d component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly 2d component. + + Returns: + The dynamic imports for the plotly 2d component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-gl2d-dist-min") + + +class PlotlyMapbox(Plotly): + """Display a plotly mapbox graph.""" + + tag: str = "MapboxPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-mapbox-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly mapbox component. + + Returns: + The imports for the plotly mapbox component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly mapbox component. + + Returns: + The dynamic imports for the plotly mapbox component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-mapbox-dist-min") + + +class PlotlyFinance(Plotly): + """Display a plotly finance graph.""" + + tag: str = "FinancePlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-finance-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly finance component. + + Returns: + The imports for the plotly finance component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly finance component. + + Returns: + The dynamic imports for the plotly finance component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-finance-dist-min") + + +class PlotlyStrict(Plotly): + """Display a plotly strict graph.""" + + tag: str = "StrictPlotlyPlot" + + library = "react-plotly.js@2.6.0" + + lib_dependencies: list[str] = ["plotly.js-strict-dist-min@3.0.0"] + + def add_imports(self) -> ImportDict | list[ImportDict]: + """Add imports for the plotly strict component. + + Returns: + The imports for the plotly strict component. + """ + return CREATE_PLOTLY_COMPONENT + + def _get_dynamic_imports(self) -> str: + """Get the dynamic imports for the plotly strict component. + + Returns: + The dynamic imports for the plotly strict component. + """ + return dynamic_plotly_import(self.tag, "plotly.js-strict-dist-min") diff --git a/reflex/components/plotly/plotly.pyi b/reflex/components/plotly/plotly.pyi index f60e5a6a4..c4d8bf64a 100644 --- a/reflex/components/plotly/plotly.pyi +++ b/reflex/components/plotly/plotly.pyi @@ -11,6 +11,7 @@ from reflex.components.component import NoSSRComponent from reflex.event import EventType from reflex.style import Style from reflex.utils import console +from reflex.utils.imports import ImportDict from reflex.vars.base import Var try: @@ -141,3 +142,767 @@ class Plotly(NoSSRComponent): The Plotly component. """ ... + +CREATE_PLOTLY_COMPONENT: ImportDict + +def dynamic_plotly_import(name: str, package: str) -> str: ... + +class PlotlyBasic(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyBasic": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyCartesian(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyCartesian": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGeo(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGeo": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGl3d(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGl3d": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyGl2d(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyGl2d": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyMapbox(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyMapbox": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyFinance(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyFinance": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... + +class PlotlyStrict(Plotly): + def add_imports(self) -> ImportDict | list[ImportDict]: ... + @overload + @classmethod + def create( # type: ignore + cls, + *children, + data: Optional[Union[Figure, Var[Figure]]] = None, # type: ignore + layout: Optional[Union[Dict, Var[Dict]]] = None, + template: Optional[Union[Template, Var[Template]]] = None, # type: ignore + config: Optional[Union[Dict, Var[Dict]]] = None, + use_resize_handler: Optional[Union[Var[bool], bool]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_after_plot: Optional[EventType[()]] = None, + on_animated: Optional[EventType[()]] = None, + on_animating_frame: Optional[EventType[()]] = None, + on_animation_interrupted: Optional[EventType[()]] = None, + on_autosize: Optional[EventType[()]] = None, + on_before_hover: Optional[EventType[()]] = None, + on_blur: Optional[EventType[()]] = None, + on_button_clicked: Optional[EventType[()]] = None, + on_click: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_context_menu: Optional[EventType[()]] = None, + on_deselect: Optional[EventType[()]] = None, + on_double_click: Optional[EventType[()]] = None, + on_focus: Optional[EventType[()]] = None, + on_hover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_mount: Optional[EventType[()]] = None, + on_mouse_down: Optional[EventType[()]] = None, + on_mouse_enter: Optional[EventType[()]] = None, + on_mouse_leave: Optional[EventType[()]] = None, + on_mouse_move: Optional[EventType[()]] = None, + on_mouse_out: Optional[EventType[()]] = None, + on_mouse_over: Optional[EventType[()]] = None, + on_mouse_up: Optional[EventType[()]] = None, + on_redraw: Optional[EventType[()]] = None, + on_relayout: Optional[EventType[()]] = None, + on_relayouting: Optional[EventType[()]] = None, + on_restyle: Optional[EventType[()]] = None, + on_scroll: Optional[EventType[()]] = None, + on_selected: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_selecting: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_transition_interrupted: Optional[EventType[()]] = None, + on_transitioning: Optional[EventType[()]] = None, + on_unhover: Optional[Union[EventType[()], EventType[List[Point]]]] = None, + on_unmount: Optional[EventType[()]] = None, + **props, + ) -> "PlotlyStrict": + """Create the Plotly component. + + Args: + *children: The children of the component. + data: The figure to display. This can be a plotly figure or a plotly data json. + layout: The layout of the graph. + template: The template for visual appearance of the graph. + config: The config of the graph. + use_resize_handler: If true, the graph will resize when the window is resized. + on_after_plot: Fired after the plot is redrawn. + on_animated: Fired after the plot was animated. + on_animating_frame: Fired while animating a single frame (does not currently pass data through). + on_animation_interrupted: Fired when an animation is interrupted (to start a new animation for example). + on_autosize: Fired when the plot is responsively sized. + on_before_hover: Fired whenever mouse moves over a plot. + on_button_clicked: Fired when a plotly UI button is clicked. + on_click: Fired when the plot is clicked. + on_deselect: Fired when a selection is cleared (via double click). + on_double_click: Fired when the plot is double clicked. + on_hover: Fired when a plot element is hovered over. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. + on_restyle: Fired after the plot style is changed. + on_redraw: Fired after the plot is redrawn. + on_selected: Fired after selecting plot elements. + on_selecting: Fired while dragging a selection. + on_transitioning: Fired while an animation is occurring. + on_transition_interrupted: Fired when a transition is stopped early. + on_unhover: Fired when a hovered element is no longer hovered. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The Plotly component. + """ + ... diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index d9b7c0b02..0718aaac9 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -144,7 +144,7 @@ class ColorModeIconButton(IconButton): if allow_system: - def color_mode_item(_color_mode: str): + def color_mode_item(_color_mode: Literal["light", "dark", "system"]): return dropdown_menu.item( _color_mode.title(), on_click=set_color_mode(_color_mode) ) diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index dbac8e733..d1f9464d8 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -8,6 +8,7 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase +from reflex.constants.base import Dirs from reflex.event import EventSpec, run_script from reflex.style import Style, resolved_color_mode from reflex.utils import format @@ -27,7 +28,10 @@ LiteralPosition = Literal[ "bottom-right", ] -toast_ref = Var(_js_expr="refs['__toast']") +toast_ref = Var( + _js_expr="refs['__toast']", + _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}), +) class ToastAction(Base): @@ -327,6 +331,19 @@ class Toaster(Component): """ return Toaster.send_toast(message, level="success", **kwargs) + @staticmethod + def toast_loading(message: str | Var = "", **kwargs: Any): + """Display a loading toast message. + + Args: + message: The message to display. + **kwargs: Additional toast props. + + Returns: + The toast event. + """ + return Toaster.send_toast(message, level="loading", **kwargs) + @staticmethod def toast_dismiss(id: Var | str | None = None): """Dismiss a toast. @@ -378,6 +395,7 @@ class ToastNamespace(ComponentNamespace): warning = staticmethod(Toaster.toast_warning) error = staticmethod(Toaster.toast_error) success = staticmethod(Toaster.toast_success) + loading = staticmethod(Toaster.toast_loading) dismiss = staticmethod(Toaster.toast_dismiss) __call__ = staticmethod(Toaster.send_toast) diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index cb12834d5..cb637bfff 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -9,9 +9,12 @@ from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon from reflex.components.props import NoExtrasAllowedProps, PropsBase +from reflex.constants.base import Dirs from reflex.event import EventSpec, EventType from reflex.style import Style +from reflex.utils.imports import ImportVar from reflex.utils.serializers import serializer +from reflex.vars import VarData from reflex.vars.base import Var LiteralPosition = Literal[ @@ -22,7 +25,10 @@ LiteralPosition = Literal[ "bottom-center", "bottom-right", ] -toast_ref = Var(_js_expr="refs['__toast']") +toast_ref = Var( + _js_expr="refs['__toast']", + _var_data=VarData(imports={f"$/{Dirs.STATE_PATH}": [ImportVar(tag="refs")]}), +) class ToastAction(Base): label: str @@ -70,6 +76,8 @@ class Toaster(Component): @staticmethod def toast_success(message: str | Var = "", **kwargs: Any): ... @staticmethod + def toast_loading(message: str | Var = "", **kwargs: Any): ... + @staticmethod def toast_dismiss(id: Var | str | None = None): ... @overload @classmethod @@ -172,6 +180,7 @@ class ToastNamespace(ComponentNamespace): warning = staticmethod(Toaster.toast_warning) error = staticmethod(Toaster.toast_error) success = staticmethod(Toaster.toast_success) + loading = staticmethod(Toaster.toast_loading) dismiss = staticmethod(Toaster.toast_dismiss) @staticmethod diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index cb02ca000..221b65ca9 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -134,6 +134,10 @@ class IterTag(Tag): if isinstance(component, (Foreach, Cond)): component = Fragment.create(component) + # If the component is a tuple, unpack and wrap it in a fragment. + if isinstance(component, tuple): + component = Fragment.create(*component) + # Set the component key. if component.key is None: component.key = index diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index 983726e56..515d9e05f 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Mapping, Optional, Sequence from reflex.event import EventChain from reflex.utils import format, types @@ -103,7 +103,7 @@ class Tag: { format.to_camel_case(name, allow_hyphens=True): ( prop - if types._isinstance(prop, Union[EventChain, dict]) + if types._isinstance(prop, (EventChain, Mapping)) else LiteralVar.create(prop) ) # rx.color is always a string for name, prop in kwargs.items() diff --git a/reflex/config.py b/reflex/config.py index f3d40dc37..d0829e627 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -2,20 +2,25 @@ from __future__ import annotations +import concurrent.futures import dataclasses import enum import importlib import inspect +import multiprocessing import os +import platform import sys import threading import urllib.parse +from functools import lru_cache from importlib.util import find_spec from pathlib import Path from types import ModuleType from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Generic, List, @@ -23,10 +28,15 @@ from typing import ( Set, TypeVar, get_args, + get_origin, ) +from reflex_cli.constants.hosting import Hosting from typing_extensions import Annotated, get_type_hints +from reflex import constants +from reflex.base import Base +from reflex.utils import console from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError from reflex.utils.types import GenericType, is_union, value_inside_optional @@ -35,11 +45,11 @@ try: except ModuleNotFoundError: import pydantic -from reflex_cli.constants.hosting import Hosting -from reflex import constants -from reflex.base import Base -from reflex.utils import console +try: + from dotenv import load_dotenv # pyright: ignore [reportMissingImports] +except ImportError: + load_dotenv = None class DBConfig(Base): @@ -304,6 +314,15 @@ def interpret_env_var_value( return interpret_path_env(value, field_name) elif field_type is ExistingPath: return interpret_existing_path_env(value, field_name) + elif get_origin(field_type) is list: + return [ + interpret_env_var_value( + v, + get_args(field_type)[0], + f"{field_name}[{i}]", + ) + for i, v in enumerate(value.split(":")) + ] elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum): return interpret_enum_env(value, field_type, field_name) @@ -387,7 +406,24 @@ class EnvVar(Generic[T]): else: if isinstance(value, enum.Enum): value = value.value - os.environ[self.name] = str(value) + if isinstance(value, list): + str_value = ":".join(str(v) for v in value) + else: + str_value = str(value) + os.environ[self.name] = str_value + + +@lru_cache() +def get_type_hints_environment(cls: type) -> dict[str, Any]: + """Get the type hints for the environment variables. + + Args: + cls: The class. + + Returns: + The type hints. + """ + return get_type_hints(cls) class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] @@ -416,7 +452,9 @@ class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] """ self.name = name - def __get__(self, instance: Any, owner: Any): + def __get__( + self, instance: EnvironmentVariables, owner: type[EnvironmentVariables] + ): """Get the EnvVar instance. Args: @@ -426,7 +464,7 @@ class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] Returns: The EnvVar instance. """ - type_ = get_args(get_type_hints(owner)[self.name])[0] + type_ = get_args(get_type_hints_environment(owner)[self.name])[0] env_name = self.name if self.internal: env_name = f"__{env_name}" @@ -463,6 +501,95 @@ class PerformanceMode(enum.Enum): OFF = "off" +class ExecutorType(enum.Enum): + """Executor for compiling the frontend.""" + + THREAD = "thread" + PROCESS = "process" + MAIN_THREAD = "main_thread" + + @classmethod + def get_executor_from_environment(cls): + """Get the executor based on the environment variables. + + Returns: + The executor. + """ + executor_type = environment.REFLEX_COMPILE_EXECUTOR.get() + + reflex_compile_processes = environment.REFLEX_COMPILE_PROCESSES.get() + reflex_compile_threads = environment.REFLEX_COMPILE_THREADS.get() + # By default, use the main thread. Unless the user has specified a different executor. + # Using a process pool is much faster, but not supported on all platforms. It's gated behind a flag. + if executor_type is None: + if ( + platform.system() not in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + console.warn("Multiprocessing is only supported on Linux and MacOS.") + + if ( + platform.system() in ("Linux", "Darwin") + and reflex_compile_processes is not None + ): + if reflex_compile_processes == 0: + console.warn( + "Number of processes must be greater than 0. If you want to use the default number of processes, set REFLEX_COMPILE_EXECUTOR to 'process'. Defaulting to None." + ) + reflex_compile_processes = None + elif reflex_compile_processes < 0: + console.warn( + "Number of processes must be greater than 0. Defaulting to None." + ) + reflex_compile_processes = None + executor_type = ExecutorType.PROCESS + elif reflex_compile_threads is not None: + if reflex_compile_threads == 0: + console.warn( + "Number of threads must be greater than 0. If you want to use the default number of threads, set REFLEX_COMPILE_EXECUTOR to 'thread'. Defaulting to None." + ) + reflex_compile_threads = None + elif reflex_compile_threads < 0: + console.warn( + "Number of threads must be greater than 0. Defaulting to None." + ) + reflex_compile_threads = None + executor_type = ExecutorType.THREAD + else: + executor_type = ExecutorType.MAIN_THREAD + + match executor_type: + case ExecutorType.PROCESS: + executor = concurrent.futures.ProcessPoolExecutor( + max_workers=reflex_compile_processes, + mp_context=multiprocessing.get_context("fork"), + ) + case ExecutorType.THREAD: + executor = concurrent.futures.ThreadPoolExecutor( + max_workers=reflex_compile_threads + ) + case ExecutorType.MAIN_THREAD: + FUTURE_RESULT_TYPE = TypeVar("FUTURE_RESULT_TYPE") + + class MainThreadExecutor: + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def submit( + self, fn: Callable[..., FUTURE_RESULT_TYPE], *args, **kwargs + ) -> concurrent.futures.Future[FUTURE_RESULT_TYPE]: + future_job = concurrent.futures.Future() + future_job.set_result(fn(*args, **kwargs)) + return future_job + + executor = MainThreadExecutor() + + return executor + + class EnvironmentVariables: """Environment variables class to instantiate environment variables.""" @@ -504,6 +631,8 @@ class EnvironmentVariables: Path(constants.Dirs.UPLOADED_FILES) ) + REFLEX_COMPILE_EXECUTOR: EnvVar[Optional[ExecutorType]] = env_var(None) + # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor. REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None) @@ -544,6 +673,12 @@ class EnvironmentVariables: # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY. REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False) + # The port to run the frontend on. + REFLEX_FRONTEND_PORT: EnvVar[int | None] = env_var(None) + + # The port to run the backend on. + REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None) + # Reflex internal env to reload the config. RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True) @@ -563,7 +698,7 @@ class EnvironmentVariables: REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) # In which performance mode to run the app. - REFLEX_PERF_MODE: EnvVar[Optional[PerformanceMode]] = env_var(PerformanceMode.WARN) + REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN) # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) @@ -571,6 +706,12 @@ class EnvironmentVariables: # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(True) + # Additional paths to include in the hot reload. Separated by a colon. + REFLEX_HOT_RELOAD_INCLUDE_PATHS: EnvVar[List[Path]] = env_var([]) + + # Paths to exclude from the hot reload. Takes precedence over include paths. Separated by a colon. + REFLEX_HOT_RELOAD_EXCLUDE_PATHS: EnvVar[List[Path]] = env_var([]) + environment = EnvironmentVariables() @@ -604,6 +745,7 @@ class Config(Base): """Pydantic config for the config.""" validate_assignment = True + use_enum_values = False # The name of the app (should match the name of the app directory). app_name: str @@ -615,19 +757,21 @@ class Config(Base): loglevel: constants.LogLevel = constants.LogLevel.DEFAULT # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - frontend_port: int = constants.DefaultPorts.FRONTEND_PORT + frontend_port: int | None = None # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app frontend_path: str = "" # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken. - backend_port: int = constants.DefaultPorts.BACKEND_PORT + backend_port: int | None = None # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production. - api_url: str = f"http://localhost:{backend_port}" + api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}" # The url the frontend will be hosted on. - deploy_url: Optional[str] = f"http://localhost:{frontend_port}" + deploy_url: Optional[str] = ( + f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}" + ) # The url the backend will be hosted on. backend_host: str = "0.0.0.0" @@ -679,7 +823,7 @@ class Config(Base): # Number of gunicorn workers from user gunicorn_workers: Optional[int] = None - # Number of requests before a worker is restarted + # Number of requests before a worker is restarted; set to 0 to disable gunicorn_max_requests: int = 100 # Variance limit for max requests; gunicorn only @@ -734,6 +878,9 @@ class Config(Base): self._non_default_attributes.update(kwargs) self._replace_defaults(**kwargs) + # Set the log level for this process + console.set_log_level(self.loglevel) + if ( self.state_manager_mode == constants.StateManagerMode.REDIS and not self.redis_url @@ -773,16 +920,15 @@ class Config(Base): Returns: The updated config values. """ - if self.env_file: - try: - from dotenv import load_dotenv # pyright: ignore [reportMissingImports] - - # load env file if exists - load_dotenv(self.env_file, override=True) - except ImportError: + env_file = self.env_file or os.environ.get("ENV_FILE", None) + if env_file: + if load_dotenv is None: console.error( """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`.""" ) + else: + # load env file if exists + load_dotenv(env_file, override=True) updated_values = {} # Iterate over the fields. diff --git a/reflex/event.py b/reflex/event.py index f247047cf..c2eb8db3a 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -26,6 +26,7 @@ from typing import ( from typing_extensions import ( Protocol, + Self, TypeAliasType, TypedDict, TypeVar, @@ -110,7 +111,7 @@ class EventActionsMixin: event_actions: Dict[str, Union[bool, int]] = dataclasses.field(default_factory=dict) @property - def stop_propagation(self): + def stop_propagation(self) -> Self: """Stop the event from bubbling up the DOM tree. Returns: @@ -122,7 +123,7 @@ class EventActionsMixin: ) @property - def prevent_default(self): + def prevent_default(self) -> Self: """Prevent the default behavior of the event. Returns: @@ -133,7 +134,7 @@ class EventActionsMixin: event_actions={"preventDefault": True, **self.event_actions}, ) - def throttle(self, limit_ms: int): + def throttle(self, limit_ms: int) -> Self: """Throttle the event handler. Args: @@ -147,7 +148,7 @@ class EventActionsMixin: event_actions={"throttle": limit_ms, **self.event_actions}, ) - def debounce(self, delay_ms: int): + def debounce(self, delay_ms: int) -> Self: """Debounce the event handler. Args: @@ -162,7 +163,7 @@ class EventActionsMixin: ) @property - def temporal(self): + def temporal(self) -> Self: """Do not queue the event if the backend is down. Returns: @@ -1773,7 +1774,7 @@ V4 = TypeVar("V4") V5 = TypeVar("V5") -class EventCallback(Generic[Unpack[P]]): +class EventCallback(Generic[Unpack[P]], EventActionsMixin): """A descriptor that wraps a function to be used as an event.""" def __init__(self, func: Callable[[Any, Unpack[P]], Any]): @@ -1784,24 +1785,6 @@ class EventCallback(Generic[Unpack[P]]): """ self.func = func - @property - def prevent_default(self): - """Prevent default behavior. - - Returns: - The event callback with prevent default behavior. - """ - return self - - @property - def stop_propagation(self): - """Stop event propagation. - - Returns: - The event callback with stop propagation behavior. - """ - return self - @overload def __call__( self: EventCallback[Unpack[Q]], diff --git a/reflex/reflex.py b/reflex/reflex.py index 70aa16a05..410485551 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -127,8 +127,8 @@ def _run( env: constants.Env = constants.Env.DEV, frontend: bool = True, backend: bool = True, - frontend_port: int = config.frontend_port, - backend_port: int = config.backend_port, + frontend_port: int | None = None, + backend_port: int | None = None, backend_host: str = config.backend_host, loglevel: constants.LogLevel = config.loglevel, ): @@ -145,10 +145,7 @@ def _run( exec.output_system_info() # If no --frontend-only and no --backend-only, then turn on frontend and backend both - if not frontend and not backend: - frontend = True - backend = True - + frontend, backend = prerequisites.check_running_mode(frontend, backend) if not frontend and backend: _skip_compile() @@ -161,17 +158,28 @@ def _run( # Find the next available open port if applicable. if frontend: + auto_increment_frontend = not bool(frontend_port or config.frontend_port) frontend_port = processes.handle_port( "frontend", - frontend_port, - constants.DefaultPorts.FRONTEND_PORT, + ( + frontend_port + or config.frontend_port + or constants.DefaultPorts.FRONTEND_PORT + ), + auto_increment=auto_increment_frontend, ) if backend: + auto_increment_backend = not bool(backend_port or config.backend_port) + backend_port = processes.handle_port( "backend", - backend_port, - constants.DefaultPorts.BACKEND_PORT, + ( + backend_port + or config.backend_port + or constants.DefaultPorts.BACKEND_PORT + ), + auto_increment=auto_increment_backend, ) # Apply the new ports to the config. @@ -249,7 +257,7 @@ def _run( # Start the frontend and backend. with processes.run_concurrently_context(*commands): # In dev mode, run the backend on the main thread. - if backend and env == constants.Env.DEV: + if backend and backend_port and env == constants.Env.DEV: backend_cmd( backend_host, int(backend_port), loglevel.subprocess_level(), frontend ) @@ -278,10 +286,14 @@ def run( envvar=environment.REFLEX_BACKEND_ONLY.name, ), frontend_port: int = typer.Option( - config.frontend_port, help="Specify a different frontend port." + config.frontend_port, + help="Specify a different frontend port.", + envvar=environment.REFLEX_FRONTEND_PORT.name, ), backend_port: int = typer.Option( - config.backend_port, help="Specify a different backend port." + config.backend_port, + help="Specify a different backend port.", + envvar=environment.REFLEX_BACKEND_PORT.name, ), backend_host: str = typer.Option( config.backend_host, help="Specify the backend host." @@ -306,10 +318,18 @@ def export( True, "--no-zip", help="Disable zip for backend and frontend exports." ), frontend: bool = typer.Option( - True, "--backend-only", help="Export only backend.", show_default=False + False, + "--frontend-only", + help="Export only frontend.", + show_default=False, + envvar=environment.REFLEX_FRONTEND_ONLY.name, ), backend: bool = typer.Option( - True, "--frontend-only", help="Export only frontend.", show_default=False + False, + "--backend-only", + help="Export only backend.", + show_default=False, + envvar=environment.REFLEX_BACKEND_ONLY.name, ), zip_dest_dir: str = typer.Option( str(Path.cwd()), @@ -332,7 +352,9 @@ def export( from reflex.utils import export as export_utils from reflex.utils import prerequisites - if prerequisites.needs_reinit(frontend=True): + frontend, backend = prerequisites.check_running_mode(frontend, backend) + + if prerequisites.needs_reinit(frontend=frontend or not backend): _init(name=config.app_name, loglevel=loglevel) if frontend and not config.show_built_with_reflex: diff --git a/reflex/state.py b/reflex/state.py index a1362d15e..c8ef2b814 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1747,6 +1747,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Yields: StateUpdate object + + Raises: + ValueError: If a string value is received for an int or float type and cannot be converted. """ from reflex.utils import telemetry @@ -1784,12 +1787,25 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): hinted_args, (Base, BaseModelV1, BaseModelV2) ): payload[arg] = hinted_args(**value) - if isinstance(value, list) and (hinted_args is set or hinted_args is Set): + elif isinstance(value, list) and (hinted_args is set or hinted_args is Set): payload[arg] = set(value) - if isinstance(value, list) and ( + elif isinstance(value, list) and ( hinted_args is tuple or hinted_args is Tuple ): payload[arg] = tuple(value) + elif isinstance(value, str) and ( + hinted_args is int or hinted_args is float + ): + try: + payload[arg] = hinted_args(value) + except ValueError: + raise ValueError( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}" + ) from None + else: + console.warn( + f"Received a string value ({value}) for {arg} but expected a {hinted_args}. A simple conversion was successful." + ) # Wrap the function in a try/except block. try: @@ -2464,6 +2480,8 @@ class ComponentState(State, mixin=True): Returns: A new instance of the Component with an independent copy of the State. """ + from reflex.compiler.compiler import into_component + cls._per_component_state_instance_count += 1 state_cls_name = f"{cls.__name__}_n{cls._per_component_state_instance_count}" component_state = type( @@ -2475,6 +2493,7 @@ class ComponentState(State, mixin=True): # Save a reference to the dynamic state for pickle/unpickle. setattr(reflex.istate.dynamic, state_cls_name, component_state) component = component_state.get_component(*children, **props) + component = into_component(component) component.State = component_state return component diff --git a/reflex/style.py b/reflex/style.py index 192835ca3..1d818ed06 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -12,7 +12,7 @@ from reflex.utils.exceptions import ReflexError from reflex.utils.imports import ImportVar from reflex.utils.types import get_origin from reflex.vars import VarData -from reflex.vars.base import CallableVar, LiteralVar, Var +from reflex.vars.base import LiteralVar, Var from reflex.vars.function import FunctionVar from reflex.vars.object import ObjectVar @@ -48,7 +48,6 @@ def _color_mode_var(_js_expr: str, _var_type: Type = str) -> Var: ).guess_type() -@CallableVar def set_color_mode( new_color_mode: LiteralColorMode | Var[LiteralColorMode] | None = None, ) -> Var[EventChain]: diff --git a/reflex/testing.py b/reflex/testing.py index 25f9e7aac..e463ddea7 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -43,6 +43,7 @@ import reflex.utils.exec import reflex.utils.format import reflex.utils.prerequisites import reflex.utils.processes +from reflex.components.component import CustomComponent from reflex.config import environment from reflex.state import ( BaseState, @@ -254,6 +255,7 @@ class AppHarness: # disable telemetry reporting for tests os.environ["TELEMETRY_ENABLED"] = "false" + CustomComponent.create().get_component.cache_clear() self.app_path.mkdir(parents=True, exist_ok=True) if self.app_source is not None: app_globals = self._get_globals_from_signature(self.app_source) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index d5b7a0d6e..70bbb0e82 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -2,8 +2,11 @@ from __future__ import annotations +import contextlib import inspect +import os import shutil +import time from pathlib import Path from types import FrameType @@ -58,6 +61,9 @@ def set_log_level(log_level: LogLevel): f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead." ) global _LOG_LEVEL + if log_level != _LOG_LEVEL: + # Set the loglevel persistenly for subprocesses. + os.environ["LOGLEVEL"] = log_level.value _LOG_LEVEL = log_level @@ -317,3 +323,20 @@ def status(*args, **kwargs): A new status. """ return _console.status(*args, **kwargs) + + +@contextlib.contextmanager +def timing(msg: str): + """Create a context manager to time a block of code. + + Args: + msg: The message to display. + + Yields: + None. + """ + start = time.time() + try: + yield + finally: + debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]") diff --git a/reflex/utils/decorator.py b/reflex/utils/decorator.py new file mode 100644 index 000000000..5c9c0bf3a --- /dev/null +++ b/reflex/utils/decorator.py @@ -0,0 +1,25 @@ +"""Decorator utilities.""" + +from typing import Callable, TypeVar + +T = TypeVar("T") + + +def once(f: Callable[[], T]) -> Callable[[], T]: + """A decorator that calls the function once and caches the result. + + Args: + f: The function to call. + + Returns: + A function that calls the function once and caches the result. + """ + unset = object() + value: object | T = unset + + def wrapper() -> T: + nonlocal value + value = f() if value is unset else value + return value # pyright: ignore[reportReturnType] + + return wrapper diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 67df7ea91..b16aaea1c 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -10,6 +10,7 @@ import re import subprocess import sys from pathlib import Path +from typing import Sequence from urllib.parse import urljoin import psutil @@ -242,29 +243,63 @@ def run_backend( run_uvicorn_backend(host, port, loglevel) -def get_reload_dirs() -> list[Path]: - """Get the reload directories for the backend. +def get_reload_paths() -> Sequence[Path]: + """Get the reload paths for the backend. Returns: - The reload directories for the backend. + The reload paths for the backend. """ config = get_config() - reload_dirs = [Path(config.app_name)] + reload_paths = [Path(config.app_name).parent] if config.app_module is not None and config.app_module.__file__: module_path = Path(config.app_module.__file__).resolve().parent - while module_path.parent.name: - if any( - sibling_file.name == "__init__.py" - for sibling_file in module_path.parent.iterdir() - ): - # go up a level to find dir without `__init__.py` - module_path = module_path.parent - else: - break + while module_path.parent.name and any( + sibling_file.name == "__init__.py" + for sibling_file in module_path.parent.iterdir() + ): + # go up a level to find dir without `__init__.py` + module_path = module_path.parent - reload_dirs = [module_path] - return reload_dirs + reload_paths = [module_path] + + include_dirs = tuple( + map(Path.absolute, environment.REFLEX_HOT_RELOAD_INCLUDE_PATHS.get()) + ) + exclude_dirs = tuple( + map(Path.absolute, environment.REFLEX_HOT_RELOAD_EXCLUDE_PATHS.get()) + ) + + def is_excluded_by_default(path: Path) -> bool: + if path.is_dir(): + if path.name.startswith("."): + # exclude hidden directories + return True + if path.name.startswith("__"): + # ignore things like __pycache__ + return True + return path.name in (".gitignore", "uploaded_files") + + reload_paths = ( + tuple( + path.absolute() + for dir in reload_paths + for path in dir.iterdir() + if not is_excluded_by_default(path) + ) + + include_dirs + ) + + if exclude_dirs: + reload_paths = tuple( + path + for path in reload_paths + if all(not path.samefile(exclude) for exclude in exclude_dirs) + ) + + console.debug(f"Reload paths: {list(map(str, reload_paths))}") + + return reload_paths def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): @@ -283,7 +318,7 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): port=port, log_level=loglevel.value, reload=True, - reload_dirs=list(map(str, get_reload_dirs())), + reload_dirs=list(map(str, get_reload_paths())), ) @@ -310,8 +345,7 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel): interface=Interfaces.ASGI, log_level=LogLevels(loglevel.value), reload=True, - reload_paths=get_reload_dirs(), - reload_ignore_dirs=[".web", ".states"], + reload_paths=get_reload_paths(), ).serve() except ImportError: console.error( @@ -368,34 +402,49 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): app_module = get_app_module() - run_backend_prod = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split() - run_backend_prod_windows = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split() command = ( [ - *run_backend_prod_windows, - "--host", - host, - "--port", - str(port), + "uvicorn", + *( + [ + "--limit-max-requests", + str(config.gunicorn_max_requests), + ] + if config.gunicorn_max_requests > 0 + else [] + ), + *("--timeout-keep-alive", str(config.timeout)), + *("--host", host), + *("--port", str(port)), + *("--workers", str(_get_backend_workers())), app_module, ] if constants.IS_WINDOWS else [ - *run_backend_prod, - "--bind", - f"{host}:{port}", - "--threads", - str(_get_backend_workers()), + "gunicorn", + *("--worker-class", config.gunicorn_worker_class), + *( + [ + "--max-requests", + str(config.gunicorn_max_requests), + "--max-requests-jitter", + str(config.gunicorn_max_requests_jitter), + ] + if config.gunicorn_max_requests > 0 + else [] + ), + "--preload", + *("--timeout", str(config.timeout)), + *("--bind", f"{host}:{port}"), + *("--threads", str(_get_backend_workers())), f"{app_module}()", ] ) command += [ - "--log-level", - loglevel.value, - "--workers", - str(_get_backend_workers()), + *("--log-level", loglevel.value), ] + processes.new_process( command, run=True, diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 225d52f3a..214c845f8 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -27,6 +27,36 @@ WRAP_MAP = { } +def length_of_largest_common_substring(str1: str, str2: str) -> int: + """Find the length of the largest common substring between two strings. + + Args: + str1: The first string. + str2: The second string. + + Returns: + The length of the largest common substring. + """ + if not str1 or not str2: + return 0 + + # Create a matrix of size (len(str1) + 1) x (len(str2) + 1) + dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)] + + # Variables to keep track of maximum length and ending position + max_length = 0 + + # Fill the dp matrix + for i in range(1, len(str1) + 1): + for j in range(1, len(str2) + 1): + if str1[i - 1] == str2[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + if dp[i][j] > max_length: + max_length = dp[i][j] + + return max_length + + def get_close_char(open: str, close: str | None = None) -> str: """Check if the given character is a valid brace. diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py index 46e8e7362..66ae4b023 100644 --- a/reflex/utils/imports.py +++ b/reflex/utils/imports.py @@ -109,6 +109,9 @@ class ImportVar: # whether this import should be rendered or not render: Optional[bool] = True + # The path of the package to import from. + package_path: str = "/" + # whether this import package should be added to transpilePackages in next.config.js # https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages transpile: Optional[bool] = False diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index 07a541201..dae938316 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -260,3 +260,49 @@ def find_replace(directory: str | Path, find: str, replace: str): text = filepath.read_text(encoding="utf-8") text = re.sub(find, replace, text) filepath.write_text(text, encoding="utf-8") + + +def samefile(file1: Path, file2: Path) -> bool: + """Check if two files are the same. + + Args: + file1: The first file. + file2: The second file. + + Returns: + Whether the files are the same. If either file does not exist, returns False. + """ + if file1.exists() and file2.exists(): + return file1.samefile(file2) + + return False + + +def update_directory_tree(src: Path, dest: Path): + """Recursively copies a directory tree from src to dest. + Only copies files if the destination file is missing or modified earlier than the source file. + + Args: + src: Source directory + dest: Destination directory + + Raises: + ValueError: If the source is not a directory + """ + if not src.is_dir(): + raise ValueError(f"Source {src} is not a directory") + + # Ensure the destination directory exists + dest.mkdir(parents=True, exist_ok=True) + + for item in src.iterdir(): + dest_item = dest / item.name + + if item.is_dir(): + # Recursively copy subdirectories + update_directory_tree(item, dest_item) + elif item.is_file() and ( + not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime + ): + # Copy file if it doesn't exist in the destination or is older than the source + shutil.copy2(item, dest_item) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index c4e37f532..1744a55df 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -24,6 +24,7 @@ from datetime import datetime from pathlib import Path from types import ModuleType from typing import Any, Callable, List, NamedTuple, Optional +from urllib.parse import urlparse import httpx import typer @@ -1234,6 +1235,21 @@ def install_frontend_packages(packages: set[str], config: Config): ) +def check_running_mode(frontend: bool, backend: bool) -> tuple[bool, bool]: + """Check if the app is running in frontend or backend mode. + + Args: + frontend: Whether to run the frontend of the app. + backend: Whether to run the backend of the app. + + Returns: + The running modes. + """ + if not frontend and not backend: + return True, True + return frontend, backend + + def needs_reinit(frontend: bool = True) -> bool: """Check if an app needs to be reinitialized. @@ -1302,10 +1318,13 @@ def validate_bun(): """ bun_path = path_ops.get_bun_path() - if bun_path and not bun_path.samefile(constants.Bun.DEFAULT_PATH): + if bun_path is None: + return + + if not path_ops.samefile(bun_path, constants.Bun.DEFAULT_PATH): console.info(f"Using custom Bun path: {bun_path}") bun_version = get_bun_version() - if not bun_version: + if bun_version is None: console.error( "Failed to obtain bun version. Make sure the specified bun path in your config is correct." ) @@ -1670,9 +1689,11 @@ def validate_and_create_app_using_remote_template( template_url = templates[template].code_url else: + template_parsed_url = urlparse(template) # Check if the template is a github repo. - if template.startswith("https://github.com"): - template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip" + if template_parsed_url.hostname == "github.com": + path = template_parsed_url.path.strip("/").removesuffix(".git") + template_url = f"https://github.com/{path}/archive/main.zip" else: console.error(f"Template `{template}` not found or invalid.") raise typer.Exit(1) diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index c92fb7d1a..a0c13300d 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -116,17 +116,14 @@ def change_port(port: int, _type: str) -> int: return new_port -def handle_port(service_name: str, port: int, default_port: int) -> int: +def handle_port(service_name: str, port: int, auto_increment: bool) -> int: """Change port if the specified port is in use and is not explicitly specified as a CLI arg or config arg. - otherwise tell the user the port is in use and exit the app. - - We make an assumption that when port is the default port,then it hasn't been explicitly set since its not straightforward - to know whether a port was explicitly provided by the user unless its any other than the default. + Otherwise tell the user the port is in use and exit the app. Args: service_name: The frontend or backend. port: The provided port. - default_port: The default port number associated with the specified service. + auto_increment: Whether to automatically increment the port. Returns: The port to run the service on. @@ -134,13 +131,15 @@ def handle_port(service_name: str, port: int, default_port: int) -> int: Raises: Exit:when the port is in use. """ - if is_process_on_port(port): - if port == int(default_port): - return change_port(port, service_name) - else: - console.error(f"{service_name.capitalize()} port: {port} is already in use") - raise typer.Exit() - return port + if (process := get_process_on_port(port)) is None: + return port + if auto_increment: + return change_port(port, service_name) + else: + console.error( + f"{service_name.capitalize()} port: {port} is already in use by PID: {process.pid}." + ) + raise typer.Exit() def new_process( diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 58fec8f3b..b432319e0 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -95,6 +95,7 @@ GenericType = Union[Type, _GenericAlias] # Valid state var types. JSONType = {str, int, float, bool} PrimitiveType = Union[int, float, bool, str, list, dict, set, tuple] +PrimitiveTypes = (int, float, bool, str, list, dict, set, tuple) StateVar = Union[PrimitiveType, Base, None] StateIterVar = Union[list, set, tuple] @@ -551,13 +552,13 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool: return required_keys.issubset(required_keys) -def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: +def _isinstance(obj: Any, cls: GenericType, nested: int = 0) -> bool: """Check if an object is an instance of a class. Args: obj: The object to check. cls: The class to check against. - nested: Whether the check is nested. + nested: How many levels deep to check. Returns: Whether the object is an instance of the class. @@ -565,15 +566,24 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: if cls is Any: return True + from reflex.vars import LiteralVar, Var + + if cls is Var: + return isinstance(obj, Var) + if isinstance(obj, LiteralVar): + return _isinstance(obj._var_value, cls, nested=nested) + if isinstance(obj, Var): + return _issubclass(obj._var_type, cls) + if cls is None or cls is type(None): return obj is None + if cls and is_union(cls): + return any(_isinstance(obj, arg, nested=nested) for arg in get_args(cls)) + if is_literal(cls): return obj in get_args(cls) - if is_union(cls): - return any(_isinstance(obj, arg) for arg in get_args(cls)) - origin = get_origin(cls) if origin is None: @@ -596,38 +606,40 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: # cls is a simple generic class return isinstance(obj, origin) - if nested and args: + if nested > 0 and args: if origin is list: return isinstance(obj, list) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) if origin is tuple: if args[-1] is Ellipsis: return isinstance(obj, tuple) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) return ( isinstance(obj, tuple) and len(obj) == len(args) and all( - _isinstance(item, arg) for item, arg in zip(obj, args, strict=True) + _isinstance(item, arg, nested=nested - 1) + for item, arg in zip(obj, args, strict=True) ) ) - if origin in (dict, Breakpoints): - return isinstance(obj, dict) and all( - _isinstance(key, args[0]) and _isinstance(value, args[1]) + if origin in (dict, Mapping, Breakpoints): + return isinstance(obj, Mapping) and all( + _isinstance(key, args[0], nested=nested - 1) + and _isinstance(value, args[1], nested=nested - 1) for key, value in obj.items() ) if origin is set: return isinstance(obj, set) and all( - _isinstance(item, args[0]) for item in obj + _isinstance(item, args[0], nested=nested - 1) for item in obj ) if args: from reflex.vars import Field if origin is Field: - return _isinstance(obj, args[0]) + return _isinstance(obj, args[0], nested=nested) return isinstance(obj, get_base_class(cls)) @@ -749,7 +761,7 @@ def check_prop_in_allowed_types(prop: Any, allowed_types: Iterable) -> bool: """ from reflex.vars import Var - type_ = prop._var_type if _isinstance(prop, Var) else type(prop) + type_ = prop._var_type if isinstance(prop, Var) else type(prop) return type_ in allowed_types diff --git a/reflex/vars/base.py b/reflex/vars/base.py index c9dd81986..a6786b18a 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -75,9 +75,9 @@ from reflex.utils.types import ( if TYPE_CHECKING: from reflex.state import BaseState - from .number import BooleanVar, NumberVar - from .object import ObjectVar - from .sequence import ArrayVar, StringVar + from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar + from .object import LiteralObjectVar, ObjectVar + from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar VAR_TYPE = TypeVar("VAR_TYPE", covariant=True) @@ -573,13 +573,21 @@ class Var(Generic[VAR_TYPE]): return value_with_replaced + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: NoReturn, + _var_data: VarData | None = None, + ) -> Var[Any]: ... + @overload @classmethod def create( # pyright: ignore[reportOverlappingOverload] cls, value: bool, _var_data: VarData | None = None, - ) -> BooleanVar: ... + ) -> LiteralBooleanVar: ... @overload @classmethod @@ -587,7 +595,7 @@ class Var(Generic[VAR_TYPE]): cls, value: int, _var_data: VarData | None = None, - ) -> NumberVar[int]: ... + ) -> LiteralNumberVar[int]: ... @overload @classmethod @@ -595,7 +603,15 @@ class Var(Generic[VAR_TYPE]): cls, value: float, _var_data: VarData | None = None, - ) -> NumberVar[float]: ... + ) -> LiteralNumberVar[float]: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: str, + _var_data: VarData | None = None, + ) -> LiteralStringVar: ... @overload @classmethod @@ -611,7 +627,7 @@ class Var(Generic[VAR_TYPE]): cls, value: None, _var_data: VarData | None = None, - ) -> NoneVar: ... + ) -> LiteralNoneVar: ... @overload @classmethod @@ -619,7 +635,7 @@ class Var(Generic[VAR_TYPE]): cls, value: MAPPING_TYPE, _var_data: VarData | None = None, - ) -> ObjectVar[MAPPING_TYPE]: ... + ) -> LiteralObjectVar[MAPPING_TYPE]: ... @overload @classmethod @@ -627,7 +643,7 @@ class Var(Generic[VAR_TYPE]): cls, value: SEQUENCE_TYPE, _var_data: VarData | None = None, - ) -> ArrayVar[SEQUENCE_TYPE]: ... + ) -> LiteralArrayVar[SEQUENCE_TYPE]: ... @overload @classmethod @@ -935,7 +951,7 @@ class Var(Generic[VAR_TYPE]): """ actual_name = self._var_field_name - def setter(state: BaseState, value: Any): + def setter(state: Any, value: Any): """Get the setter for the var. Args: @@ -953,6 +969,8 @@ class Var(Generic[VAR_TYPE]): else: setattr(state, actual_name, value) + setter.__annotations__["value"] = self._var_type + setter.__qualname__ = self._get_setter_name() return setter @@ -1885,61 +1903,6 @@ def _or_operation(a: Var, b: Var): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - slots=True, -) -class CallableVar(Var): - """Decorate a Var-returning function to act as both a Var and a function. - - This is used as a compatibility shim for replacing Var objects in the - API with functions that return a family of Var. - """ - - fn: Callable[..., Var] = dataclasses.field( - default_factory=lambda: lambda: Var(_js_expr="undefined") - ) - original_var: Var = dataclasses.field( - default_factory=lambda: Var(_js_expr="undefined") - ) - - def __init__(self, fn: Callable[..., Var]): - """Initialize a CallableVar. - - Args: - fn: The function to decorate (must return Var) - """ - original_var = fn() - super(CallableVar, self).__init__( - _js_expr=original_var._js_expr, - _var_type=original_var._var_type, - _var_data=VarData.merge(original_var._get_all_var_data()), - ) - object.__setattr__(self, "fn", fn) - object.__setattr__(self, "original_var", original_var) - - def __call__(self, *args: Any, **kwargs: Any) -> Var: - """Call the decorated function. - - Args: - *args: The args to pass to the function. - **kwargs: The kwargs to pass to the function. - - Returns: - The Var returned from calling the function. - """ - return self.fn(*args, **kwargs) - - def __hash__(self) -> int: - """Calculate the hash of the object. - - Returns: - The hash of the object. - """ - return hash((type(self).__name__, self.original_var)) - - RETURN_TYPE = TypeVar("RETURN_TYPE") DICT_KEY = TypeVar("DICT_KEY") @@ -2238,6 +2201,27 @@ class ComputedVar(Var[RETURN_TYPE]): owner: Type, ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + @overload + def __get__( + self: ComputedVar[BASE_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[BASE_TYPE]: ... + + @overload + def __get__( + self: ComputedVar[SQLA_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: ComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + @overload def __get__(self, instance: None, owner: Type) -> ComputedVar[RETURN_TYPE]: ... @@ -2484,6 +2468,27 @@ class AsyncComputedVar(ComputedVar[RETURN_TYPE]): owner: Type, ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + @overload + def __get__( + self: AsyncComputedVar[BASE_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[BASE_TYPE]: ... + + @overload + def __get__( + self: AsyncComputedVar[SQLA_TYPE], + instance: None, + owner: Type, + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: AsyncComputedVar[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... + @overload def __get__(self, instance: None, owner: Type) -> AsyncComputedVar[RETURN_TYPE]: ... diff --git a/reflex/vars/number.py b/reflex/vars/number.py index 35a55490a..87f1760a6 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -974,7 +974,7 @@ def boolean_not_operation(value: BooleanVar): frozen=True, slots=True, ) -class LiteralNumberVar(LiteralVar, NumberVar): +class LiteralNumberVar(LiteralVar, NumberVar[NUMBER_T]): """Base class for immutable literal number vars.""" _var_value: float | int = dataclasses.field(default=0) diff --git a/reflex/vars/object.py b/reflex/vars/object.py index cb29cabfb..89479bbc4 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -22,7 +22,12 @@ from typing_extensions import is_typeddict from reflex.utils import types from reflex.utils.exceptions import VarAttributeError -from reflex.utils.types import GenericType, get_attribute_access_type, get_origin +from reflex.utils.types import ( + GenericType, + get_attribute_access_type, + get_origin, + safe_issubclass, +) from .base import ( CachedVarOperation, @@ -187,10 +192,14 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping): Returns: The item from the object. """ + from .sequence import LiteralStringVar + if not isinstance(key, (StringVar, str, int, NumberVar)) or ( isinstance(key, NumberVar) and key._is_strict_float() ): raise_unsupported_operand_types("[]", (type(self), type(key))) + if isinstance(key, str) and isinstance(Var.create(key), LiteralStringVar): + return self.__getattr__(key) return ObjectItemOperation.create(self, key).guess_type() # NoReturn is used here to catch when key value is Any @@ -260,12 +269,12 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping): if types.is_optional(var_type): var_type = get_args(var_type)[0] - fixed_type = var_type if isclass(var_type) else get_origin(var_type) + fixed_type = get_origin(var_type) or var_type if ( - (isclass(fixed_type) and not issubclass(fixed_type, Mapping)) + is_typeddict(fixed_type) + or (isclass(fixed_type) and not safe_issubclass(fixed_type, Mapping)) or (fixed_type in types.UnionTypes) - or is_typeddict(fixed_type) ): attribute_type = get_attribute_access_type(var_type, name) if attribute_type is None: diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index fb797b4ec..0e7b082f9 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -372,6 +372,33 @@ class StringVar(Var[STRING_TYPE], python_types=str): return string_ge_operation(self, other) + @overload + def replace( # pyright: ignore [reportOverlappingOverload] + self, search_value: StringVar | str, new_value: StringVar | str + ) -> StringVar: ... + + @overload + def replace( + self, search_value: Any, new_value: Any + ) -> CustomVarOperationReturn[StringVar]: ... + + def replace(self, search_value: Any, new_value: Any) -> StringVar: # pyright: ignore [reportInconsistentOverload] + """Replace a string with a value. + + Args: + search_value: The string to search. + new_value: The value to be replaced with. + + Returns: + The string replace operation. + """ + if not isinstance(search_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(search_value))) + if not isinstance(new_value, (StringVar, str)): + raise_unsupported_operand_types("replace", (type(self), type(new_value))) + + return string_replace_operation(self, search_value, new_value) + @var_operation def string_lt_operation(lhs: StringVar[Any] | str, rhs: StringVar[Any] | str): @@ -570,7 +597,7 @@ def array_join_operation(array: ArrayVar, sep: StringVar[Any] | str = ""): @var_operation def string_replace_operation( - string: StringVar, search_value: StringVar | str, new_value: StringVar | str + string: StringVar[Any], search_value: StringVar | str, new_value: StringVar | str ): """Replace a string with a value. @@ -583,7 +610,7 @@ def string_replace_operation( The string replace operation. """ return var_operation_return( - js_expression=f"{string}.replace({search_value}, {new_value})", + js_expression=f"{string}.replaceAll({search_value}, {new_value})", var_type=str, ) diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index bf777be2e..ff8f01f3a 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -1,3 +1,3 @@ -from .fixtures import evaluated_page +from .fixtures import evaluated_page, unevaluated_page -__all__ = ["evaluated_page"] +__all__ = ["evaluated_page", "unevaluated_page"] diff --git a/tests/benchmarks/fixtures.py b/tests/benchmarks/fixtures.py index d9c0d7688..334d48282 100644 --- a/tests/benchmarks/fixtures.py +++ b/tests/benchmarks/fixtures.py @@ -213,6 +213,75 @@ def side_bar(): ) +class NestedElement(rx.Base): + """A nested element.""" + + identifier: str + value: list[int] + + +class BenchmarkState(rx.State): + """State for the benchmark.""" + + counter: rx.Field[int] = rx.field(17) + + current_key: rx.Field[str] = rx.field("key_2") + + @rx.event + def increment(self): + """Increment the counter.""" + self.counter = self.counter + 1 + + @rx.event + def decrement(self): + """Decrement the counter.""" + self.counter = self.counter - 1 + + @rx.var + def elements(self) -> list[int]: + """List of elements. + + Returns: + List of elements. + """ + if self.counter < 0: + return list(range(0)) + return list(range(self.counter)) + + @rx.var + def nested_elements(self) -> list[NestedElement]: + """List of nested elements. + + Returns: + List of nested elements. + """ + return [ + NestedElement( + identifier=str(i), + value=list(range(i)), + ) + for i in range(self.counter) + ] + + @rx.var + def show_odd(self) -> bool: + """Check if the counter is odd. + + Returns: + True if the counter is odd, False otherwise. + """ + return self.counter % 2 == 1 + + @rx.var + def show_even(self) -> bool: + """Check if the counter is even. + + Returns: + True if the counter is even, False otherwise. + """ + return self.counter % 2 == 0 + + LOREM_IPSUM = "Lorem ipsum dolor sit amet, dolor ut dolore pariatur aliqua enim tempor sed. Labore excepteur sed exercitation. Ullamco aliquip lorem sunt enim in incididunt. Magna anim officia sint cillum labore. Ut eu non dolore minim nostrud magna eu, aute ex in incididunt irure eu. Fugiat et magna magna est excepteur eiusmod minim. Quis eiusmod et non pariatur dolor veniam incididunt, eiusmod irure enim sed dolor lorem pariatur do. Occaecat duis irure excepteur dolore. Proident ut laborum pariatur sit sit, nisi nostrud voluptate magna commodo laborum esse velit. Voluptate non minim deserunt adipiscing irure deserunt cupidatat. Laboris veniam commodo incididunt veniam lorem occaecat, fugiat ipsum dolor cupidatat. Ea officia sed eu excepteur culpa adipiscing, tempor consectetur ullamco eu. Anim ex proident nulla sunt culpa, voluptate veniam proident est adipiscing sint elit velit. Laboris adipiscing est culpa cillum magna. Sit veniam nulla nulla, aliqua eiusmod commodo lorem cupidatat commodo occaecat. Fugiat cillum dolor incididunt mollit eiusmod sint. Non lorem dolore labore excepteur minim laborum sed. Irure nisi do lorem nulla sunt commodo, deserunt quis mollit consectetur minim et esse est, proident nostrud officia enim sed reprehenderit. Magna cillum consequat aute reprehenderit duis sunt ullamco. Labore qui mollit voluptate. Duis dolor sint aute amet aliquip officia, est non mollit tempor enim quis fugiat, eu do culpa consectetur magna. Do ullamco aliqua voluptate culpa excepteur reprehenderit reprehenderit. Occaecat nulla sit est magna. Deserunt ea voluptate veniam cillum. Amet cupidatat duis est tempor fugiat ex eu, officia est sunt consectetur labore esse exercitation. Nisi cupidatat irure est nisi. Officia amet eu veniam reprehenderit. In amet incididunt tempor commodo ea labore. Mollit dolor aliquip excepteur, voluptate aute occaecat id officia proident. Ullamco est amet tempor. Proident aliquip proident mollit do aliquip ipsum, culpa quis aute id irure. Velit excepteur cillum cillum ut cupidatat. Occaecat qui elit esse nulla minim. Consequat velit id ad pariatur tempor. Eiusmod deserunt aliqua ex sed quis non. Dolor sint commodo ex in deserunt nostrud excepteur, pariatur ex aliqua anim adipiscing amet proident. Laboris eu laborum magna lorem ipsum fugiat velit." @@ -233,6 +302,82 @@ def _complicated_page(): ) -@pytest.fixture(params=[_simple_page, _complicated_page]) -def evaluated_page(request): +def _counter(): + return ( + rx.text(BenchmarkState.counter), + rx.button("Increment", on_click=BenchmarkState.increment), + rx.button("Decrement", on_click=BenchmarkState.decrement), + rx.cond( + BenchmarkState.counter < 0, + rx.text("Counter is negative"), + rx.fragment( + rx.cond( + BenchmarkState.show_odd, + rx.text("Counter is odd"), + ), + rx.cond( + BenchmarkState.show_even, + rx.text("Counter is even"), + ), + ), + ), + ) + + +def _show_key(): + return rx.match( + BenchmarkState.current_key, + ( + "key_1", + rx.text("Key 1"), + ), + ( + "key_2", + rx.text("Key 2"), + ), + ( + "key_3", + rx.text("Key 3"), + ), + rx.text("Key not found"), + ) + + +def _simple_foreach(): + return rx.foreach( + BenchmarkState.elements, + lambda elem: rx.text(elem), + ) + + +def _render_nested_element(elem: NestedElement, idx): + return ( + rx.text(f"{idx} {elem.identifier}"), + rx.foreach(elem.value, lambda value: rx.text(value)), + ) + + +def _nested_foreach(): + return rx.foreach( + BenchmarkState.nested_elements, + _render_nested_element, + ) + + +def _stateful_page(): + return rx.hstack( + _counter(), + _show_key(), + _simple_foreach(), + _nested_foreach(), + ) + + +@pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) +def unevaluated_page(request: pytest.FixtureRequest): + return request.param + + +@pytest.fixture(params=[_simple_page, _complicated_page, _stateful_page]) +def evaluated_page(request: pytest.FixtureRequest): return request.param() diff --git a/tests/benchmarks/test_compilation.py b/tests/benchmarks/test_compilation.py index 0a20ed521..dbea81023 100644 --- a/tests/benchmarks/test_compilation.py +++ b/tests/benchmarks/test_compilation.py @@ -1,18 +1,25 @@ -import pytest +from pytest_codspeed import BenchmarkFixture from reflex.compiler.compiler import _compile_page, _compile_stateful_components +from reflex.components.component import Component -@pytest.mark.benchmark -def test_compile_page(evaluated_page): - _compile_page(evaluated_page, None) +def import_templates(): + # Importing the templates module to avoid the import time in the benchmark + import reflex.compiler.templates # noqa: F401 -@pytest.mark.benchmark -def test_compile_stateful(evaluated_page): - _compile_stateful_components([evaluated_page]) +def test_compile_page(evaluated_page: Component, benchmark: BenchmarkFixture): + import_templates() + + benchmark(lambda: _compile_page(evaluated_page, None)) -@pytest.mark.benchmark -def test_get_all_imports(evaluated_page): - evaluated_page._get_all_imports() +def test_compile_stateful(evaluated_page: Component, benchmark: BenchmarkFixture): + import_templates() + + benchmark(lambda: _compile_stateful_components([evaluated_page])) + + +def test_get_all_imports(evaluated_page: Component, benchmark: BenchmarkFixture): + benchmark(lambda: evaluated_page._get_all_imports()) diff --git a/tests/benchmarks/test_evaluate.py b/tests/benchmarks/test_evaluate.py index c8a6b392d..7fd75fc6c 100644 --- a/tests/benchmarks/test_evaluate.py +++ b/tests/benchmarks/test_evaluate.py @@ -1,9 +1,11 @@ -import pytest +from typing import Callable -from .fixtures import _complicated_page, _simple_page +from pytest_codspeed import BenchmarkFixture + +from reflex.components.component import Component -@pytest.mark.benchmark -@pytest.mark.parametrize("page", [_simple_page, _complicated_page]) -def test_evaluate_page(page): - page() +def test_evaluate_page( + unevaluated_page: Callable[[], Component], benchmark: BenchmarkFixture +): + benchmark(unevaluated_page) diff --git a/tests/integration/test_background_task.py b/tests/integration/test_background_task.py index f312f8122..91a1b5ae1 100644 --- a/tests/integration/test_background_task.py +++ b/tests/integration/test_background_task.py @@ -20,7 +20,11 @@ def BackgroundTask(): class State(rx.State): counter: int = 0 _task_id: int = 0 - iterations: int = 10 + iterations: rx.Field[int] = rx.field(10) + + @rx.event + def set_iterations(self, value: str): + self.iterations = int(value) @rx.event(background=True) async def handle_event(self): @@ -125,8 +129,8 @@ def BackgroundTask(): rx.input( id="iterations", placeholder="Iterations", - value=State.iterations.to_string(), # pyright: ignore [reportAttributeAccessIssue] - on_change=State.set_iterations, # pyright: ignore [reportAttributeAccessIssue] + value=State.iterations.to_string(), + on_change=State.set_iterations, ), rx.button( "Delayed Increment", diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index e20b1cd6d..471382570 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -87,7 +87,7 @@ def UploadFile(): ), rx.box( rx.foreach( - rx.selected_files, + rx.selected_files(), lambda f: rx.text(f, as_="p"), ), id="selected_files", diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index a5a74c9ee..35763556a 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -10,6 +10,8 @@ from reflex.testing import AppHarness def VarOperations(): """App with var operations.""" + from typing import TypedDict + import reflex as rx from reflex.vars.base import LiteralVar from reflex.vars.sequence import ArrayVar @@ -17,6 +19,10 @@ def VarOperations(): class Object(rx.Base): name: str = "hello" + class Person(TypedDict): + name: str + age: int + class VarOperationState(rx.State): int_var1: rx.Field[int] = rx.field(10) int_var2: rx.Field[int] = rx.field(5) @@ -34,6 +40,9 @@ def VarOperations(): dict1: rx.Field[dict[int, int]] = rx.field({1: 2}) dict2: rx.Field[dict[int, int]] = rx.field({3: 4}) html_str: rx.Field[str] = rx.field("
hello
") + people: rx.Field[list[Person]] = rx.field( + [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}] + ) app = rx.App(_state=rx.State) @@ -619,6 +628,23 @@ def VarOperations(): ), id="dict_in_foreach3", ), + rx.box( + rx.foreach("abcdef", lambda x: rx.text.span(x + " ")), + id="str_in_foreach", + ), + rx.box( + rx.foreach(VarOperationState.str_var1, lambda x: rx.text.span(x + " ")), + id="str_var_in_foreach", + ), + rx.box( + rx.foreach( + VarOperationState.people, + lambda person: rx.text.span( + "Hello " + person["name"], person["age"] + 3 + ), + ), + id="typed_dict_in_foreach", + ), ) @@ -826,6 +852,9 @@ def test_var_operations(driver, var_operations: AppHarness): ("dict_in_foreach1", "a1b2"), ("dict_in_foreach2", "12"), ("dict_in_foreach3", "1234"), + ("str_in_foreach", "a b c d e f"), + ("str_var_in_foreach", "f i r s t"), + ("typed_dict_in_foreach", "Hello Alice33Hello Bob28"), ] for tag, expected in tests: diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py index d325b183f..0b1440ed1 100644 --- a/tests/integration/tests_playwright/test_appearance.py +++ b/tests/integration/tests_playwright/test_appearance.py @@ -61,7 +61,7 @@ def ColorToggleApp(): rx.icon(tag="moon", size=20), value="dark", ), - on_change=set_color_mode, + on_change=set_color_mode(), variant="classic", radius="large", value=color_mode, diff --git a/tests/units/components/datadisplay/test_shiki_code.py b/tests/units/components/datadisplay/test_shiki_code.py index cc05c35b0..e1c7984f1 100644 --- a/tests/units/components/datadisplay/test_shiki_code.py +++ b/tests/units/components/datadisplay/test_shiki_code.py @@ -11,6 +11,7 @@ from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.layout.box import Box from reflex.style import Style from reflex.vars import Var +from reflex.vars.base import LiteralVar @pytest.mark.parametrize( @@ -99,7 +100,9 @@ def test_create_shiki_code_block( applied_styles = component.style for key, value in expected_styles.items(): - assert Var.create(applied_styles[key])._var_value == value + var = Var.create(applied_styles[key]) + assert isinstance(var, LiteralVar) + assert var._var_value == value @pytest.mark.parametrize( diff --git a/tests/units/components/markdown/test_markdown.py b/tests/units/components/markdown/test_markdown.py index 866f32ae1..c6d395eb1 100644 --- a/tests/units/components/markdown/test_markdown.py +++ b/tests/units/components/markdown/test_markdown.py @@ -148,7 +148,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ( "code", {}, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; if (_language) { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Error importing language module for ${_language}:`, error); } })(); } ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( {children} ) : ( ); })""", ), ( "code", @@ -157,7 +157,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe value, **props ) }, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", ), ( "h1", @@ -171,7 +171,7 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ( "code", {"codeblock": syntax_highlighter_memoized_component(CodeBlock)}, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; if (_language) { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Error importing language module for ${_language}:`, error); } })(); } ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; if (_language) { if (!["abap", "abnf", "actionscript", "ada", "agda", "al", "antlr4", "apacheconf", "apex", "apl", "applescript", "aql", "arduino", "arff", "asciidoc", "asm6502", "asmatmel", "aspnet", "autohotkey", "autoit", "avisynth", "avro-idl", "bash", "basic", "batch", "bbcode", "bicep", "birb", "bison", "bnf", "brainfuck", "brightscript", "bro", "bsl", "c", "cfscript", "chaiscript", "cil", "clike", "clojure", "cmake", "cobol", "coffeescript", "concurnas", "coq", "core", "cpp", "crystal", "csharp", "cshtml", "csp", "css", "css-extras", "csv", "cypher", "d", "dart", "dataweave", "dax", "dhall", "diff", "django", "dns-zone-file", "docker", "dot", "ebnf", "editorconfig", "eiffel", "ejs", "elixir", "elm", "erb", "erlang", "etlua", "excel-formula", "factor", "false", "firestore-security-rules", "flow", "fortran", "fsharp", "ftl", "gap", "gcode", "gdscript", "gedcom", "gherkin", "git", "glsl", "gml", "gn", "go", "go-module", "graphql", "groovy", "haml", "handlebars", "haskell", "haxe", "hcl", "hlsl", "hoon", "hpkp", "hsts", "http", "ichigojam", "icon", "icu-message-format", "idris", "iecst", "ignore", "index", "inform7", "ini", "io", "j", "java", "javadoc", "javadoclike", "javascript", "javastacktrace", "jexl", "jolie", "jq", "js-extras", "js-templates", "jsdoc", "json", "json5", "jsonp", "jsstacktrace", "jsx", "julia", "keepalived", "keyman", "kotlin", "kumir", "kusto", "latex", "latte", "less", "lilypond", "liquid", "lisp", "livescript", "llvm", "log", "lolcode", "lua", "magma", "makefile", "markdown", "markup", "markup-templating", "matlab", "maxscript", "mel", "mermaid", "mizar", "mongodb", "monkey", "moonscript", "n1ql", "n4js", "nand2tetris-hdl", "naniscript", "nasm", "neon", "nevod", "nginx", "nim", "nix", "nsis", "objectivec", "ocaml", "opencl", "openqasm", "oz", "parigp", "parser", "pascal", "pascaligo", "pcaxis", "peoplecode", "perl", "php", "php-extras", "phpdoc", "plsql", "powerquery", "powershell", "processing", "prolog", "promql", "properties", "protobuf", "psl", "pug", "puppet", "pure", "purebasic", "purescript", "python", "q", "qml", "qore", "qsharp", "r", "racket", "reason", "regex", "rego", "renpy", "rest", "rip", "roboconf", "robotframework", "ruby", "rust", "sas", "sass", "scala", "scheme", "scss", "shell-session", "smali", "smalltalk", "smarty", "sml", "solidity", "solution-file", "soy", "sparql", "splunk-spl", "sqf", "sql", "squirrel", "stan", "stylus", "swift", "systemd", "t4-cs", "t4-templating", "t4-vb", "tap", "tcl", "textile", "toml", "tremor", "tsx", "tt2", "turtle", "twig", "typescript", "typoscript", "unrealscript", "uorazor", "uri", "v", "vala", "vbnet", "velocity", "verilog", "vhdl", "vim", "visual-basic", "warpscript", "wasm", "web-idl", "wiki", "wolfram", "wren", "xeora", "xml-doc", "xojo", "xquery", "yaml", "yang", "zig"].includes(_language)) { console.warn(`Language \`${_language}\` is not supported for code blocks inside of markdown.`); _language = ''; } else { (async () => { try { const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${_language}`); SyntaxHighlighter.registerLanguage(_language, module.default); } catch (error) { console.error(`Language ${_language} is not supported for code blocks inside of markdown: `, error); } })(); } } ; return inline ? ( {children} ) : ( ); })""", ), ( "code", @@ -180,11 +180,12 @@ def test_create_map_fn_var_subclass(cls, fn_body, fn_args, explicit_return, expe ShikiHighLevelCodeBlock ) }, - """(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); const _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", + r"""(({node, inline, className, children, ...props}) => { const match = (className || '').match(/language-(?.*)/); let _language = match ? match[1] : ''; ; return inline ? ( {children} ) : ( ); })""", ), ], ) def test_markdown_format_component(key, component_map, expected): markdown = Markdown.create("# header", component_map=component_map) result = markdown.format_component_map() + print(str(result[key])) assert str(result[key]) == expected diff --git a/tests/units/test_app.py b/tests/units/test_app.py index ae5a01c1a..88cb36509 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1299,6 +1299,7 @@ def test_app_wrap_compile_theme( app_js_lines = [ line.strip() for line in app_js_contents.splitlines() if line.strip() ] + lines = "".join(app_js_lines) assert ( "function AppWrap({children}) {" "return (" @@ -1306,14 +1307,17 @@ def test_app_wrap_compile_theme( + "" "" "" + "" + "" "{children}" "" + "" "" "" + ("" if react_strict_mode else "") + ")" "}" - ) in "".join(app_js_lines) + ) in lines @pytest.mark.parametrize( @@ -1362,6 +1366,7 @@ def test_app_wrap_priority( app_js_lines = [ line.strip() for line in app_js_contents.splitlines() if line.strip() ] + lines = "".join(app_js_lines) assert ( "function AppWrap({children}) {" "return (" + ("" if react_strict_mode else "") + "" @@ -1369,14 +1374,16 @@ def test_app_wrap_priority( "" "" "" + "" + "" "{children}" "" + "" "" "" "" - "" + ("" if react_strict_mode else "") + ")" - "}" - ) in "".join(app_js_lines) + "" + ("" if react_strict_mode else "") + ) in lines def test_app_state_determination(): diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 88d8b5f2f..18d8cd90c 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -252,6 +252,7 @@ def test_env_var(): BLUBB: EnvVar[str] = env_var("default") INTERNAL: EnvVar[str] = env_var("default", internal=True) BOOLEAN: EnvVar[bool] = env_var(False) + LIST: EnvVar[list[int]] = env_var([1, 2, 3]) assert TestEnv.BLUBB.get() == "default" assert TestEnv.BLUBB.name == "BLUBB" @@ -280,3 +281,11 @@ def test_env_var(): assert TestEnv.BOOLEAN.get() is False TestEnv.BOOLEAN.set(None) assert "BOOLEAN" not in os.environ + + assert TestEnv.LIST.get() == [1, 2, 3] + assert TestEnv.LIST.name == "LIST" + TestEnv.LIST.set([4, 5, 6]) + assert os.environ.get("LIST") == "4:5:6" + assert TestEnv.LIST.get() == [4, 5, 6] + TestEnv.LIST.set(None) + assert "LIST" not in os.environ diff --git a/tests/units/test_var.py b/tests/units/test_var.py index a72242814..8fcd288e6 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -8,6 +8,7 @@ from pandas import DataFrame import reflex as rx from reflex.base import Base +from reflex.config import PerformanceMode from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.state import BaseState from reflex.utils.exceptions import ( @@ -1893,3 +1894,27 @@ def test_var_data_hooks(): def test_var_data_with_hooks_value(): var_data = VarData(hooks={"what": VarData(hooks={"whot": VarData(hooks="whott")})}) assert var_data == VarData(hooks=["what", "whot", "whott"]) + + +def test_str_var_in_components(mocker): + class StateWithVar(rx.State): + field: int = 1 + + mocker.patch( + "reflex.components.base.bare.get_performance_mode", + return_value=PerformanceMode.RAISE, + ) + + with pytest.raises(ValueError): + rx.vstack( + str(StateWithVar.field), + ) + + mocker.patch( + "reflex.components.base.bare.get_performance_mode", + return_value=PerformanceMode.OFF, + ) + + rx.vstack( + str(StateWithVar.field), + ) diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 7cd53f14a..74dcf79b0 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -115,7 +115,20 @@ def test_typehint_issubclass(subclass, superclass, expected): assert types.typehint_issubclass(subclass, superclass) == expected -def test_validate_invalid_bun_path(mocker): +def test_validate_none_bun_path(mocker): + """Test that an error is thrown when a bun path is not specified. + + Args: + mocker: Pytest mocker object. + """ + mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None) + # with pytest.raises(typer.Exit): + prerequisites.validate_bun() + + +def test_validate_invalid_bun_path( + mocker, +): """Test that an error is thrown when a custom specified bun path is not valid or does not exist. @@ -123,13 +136,12 @@ def test_validate_invalid_bun_path(mocker): mocker: Pytest mocker object. """ mock_path = mocker.Mock() - mock_path.samefile.return_value = False mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path) + mocker.patch("reflex.utils.path_ops.samefile", return_value=False) mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None) with pytest.raises(typer.Exit): prerequisites.validate_bun() - mock_path.samefile.assert_called_once() def test_validate_bun_path_incompatible_version(mocker): @@ -141,6 +153,7 @@ def test_validate_bun_path_incompatible_version(mocker): mock_path = mocker.Mock() mock_path.samefile.return_value = False mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path) + mocker.patch("reflex.utils.path_ops.samefile", return_value=False) mocker.patch( "reflex.utils.prerequisites.get_bun_version", return_value=version.parse("0.6.5"), diff --git a/tests/units/vars/test_object.py b/tests/units/vars/test_object.py index 90e34be96..89ace55bb 100644 --- a/tests/units/vars/test_object.py +++ b/tests/units/vars/test_object.py @@ -74,11 +74,11 @@ class ObjectState(rx.State): @pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) -def test_var_create(type_: GenericType) -> None: +def test_var_create(type_: type[Base | Bare | SqlaModel | Dataclass]) -> None: my_object = type_() var = Var.create(my_object) assert var._var_type is type_ - + assert isinstance(var, ObjectVar) quantity = var.quantity assert quantity._var_type is int @@ -94,12 +94,12 @@ def test_literal_create(type_: GenericType) -> None: @pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) -def test_guess(type_: GenericType) -> None: +def test_guess(type_: type[Base | Bare | SqlaModel | Dataclass]) -> None: my_object = type_() var = Var.create(my_object) var = var.guess_type() assert var._var_type is type_ - + assert isinstance(var, ObjectVar) quantity = var.quantity assert quantity._var_type is int