From 2656ee5f1cf97225628acd861182dcb64081cbb5 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 24 Jan 2026 20:43:29 +0100 Subject: [PATCH 01/74] feat: Add explicit state ID minification for states --- reflex/environment.py | 12 + reflex/state.py | 86 +++++- reflex/vars/base.py | 5 + tests/integration/test_state_minification.py | 290 +++++++++++++++++++ tests/units/test_state_minification.py | 176 +++++++++++ uploaded_files/test.txt | 1 + 6 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 tests/integration/test_state_minification.py create mode 100644 tests/units/test_state_minification.py create mode 100644 uploaded_files/test.txt diff --git a/reflex/environment.py b/reflex/environment.py index 279fc5f60c1..481caa4ce4d 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -486,6 +486,15 @@ class PerformanceMode(enum.Enum): OFF = "off" +@enum.unique +class StateMinifyMode(enum.Enum): + """Mode for state name minification.""" + + DISABLED = "disabled" # Never minify state names (default) + ENABLED = "enabled" # Minify states that have explicit state_id + ENFORCE = "enforce" # Require all non-mixin states to have state_id + + class ExecutorType(enum.Enum): """Executor for compiling the frontend.""" @@ -688,6 +697,9 @@ class EnvironmentVariables: # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) + # State name minification mode: disabled, enabled, or enforce. + REFLEX_MINIFY_STATES: EnvVar[StateMinifyMode] = env_var(StateMinifyMode.DISABLED) + # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) diff --git a/reflex/state.py b/reflex/state.py index 572e67f67c0..888e19b75c3 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -102,6 +102,34 @@ # For BaseState.get_var_value VAR_TYPE = TypeVar("VAR_TYPE") +# Global registry: state_id -> state class (for duplicate detection) +_state_id_registry: dict[int, type[BaseState]] = {} + + +def _int_to_minified_name(state_id: int) -> str: + """Convert integer state_id to minified name using base-54 encoding. + + Args: + state_id: The integer state ID to convert. + + Returns: + The minified state name (e.g., 0->'a', 1->'b', 54->'ba'). + """ + # All possible chars for minified state name (valid JS identifiers) + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" + base = len(chars) + + if state_id == 0: + return chars[0] + + name = "" + num = state_id + while num > 0: + name = chars[num % base] + name + num //= base + + return name + def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Callable: """Protect against directly chaining a background task from another event handler. @@ -391,6 +419,9 @@ class BaseState(EvenMoreBasicBaseState): # Set of states which might need to be recomputed if vars in this state change. _potentially_dirty_states: ClassVar[set[str]] = set() + # The explicit state ID for minification (None = use full name). + _state_id: ClassVar[int | None] = None + # The parent state. parent_state: BaseState | None = field(default=None, is_var=False) @@ -508,20 +539,42 @@ def _validate_module_name(cls) -> None: raise NameError(msg) @classmethod - def __init_subclass__(cls, mixin: bool = False, **kwargs): + def __init_subclass__( + cls, mixin: bool = False, state_id: int | None = None, **kwargs + ): """Do some magic for the subclass initialization. Args: mixin: Whether the subclass is a mixin and should not be initialized. + state_id: Explicit state ID for minified state names. **kwargs: The kwargs to pass to the init_subclass method. Raises: - StateValueError: If a substate class shadows another. + StateValueError: If a substate class shadows another or duplicate state_id. """ from reflex.utils.exceptions import StateValueError super().__init_subclass__(**kwargs) + # Store state_id as class variable + cls._state_id = state_id + + # Validate state_id if provided (check for duplicates) + if state_id is not None: + if state_id in _state_id_registry: + existing_cls = _state_id_registry[state_id] + # Allow re-registration if it's the same class (e.g., module reload) + existing_key = f"{existing_cls.__module__}.{existing_cls.__name__}" + new_key = f"{cls.__module__}.{cls.__name__}" + if existing_key != new_key: + msg = ( + f"Duplicate state_id={state_id}. Already used by " + f"'{existing_cls.__module__}.{existing_cls.__name__}', " + f"cannot be reused by '{cls.__module__}.{cls.__name__}'." + ) + raise StateValueError(msg) + _state_id_registry[state_id] = cls + if cls._mixin: return @@ -988,9 +1041,34 @@ def get_name(cls) -> str: Returns: The name of the state. + + Raises: + StateValueError: If ENFORCE mode is set and state_id is missing. """ + from reflex.environment import StateMinifyMode + from reflex.utils.exceptions import StateValueError + module = cls.__module__.replace(".", "___") - return format.to_snake_case(f"{module}___{cls.__name__}") + full_name = format.to_snake_case(f"{module}___{cls.__name__}") + + minify_mode = environment.REFLEX_MINIFY_STATES.get() + + if minify_mode == StateMinifyMode.DISABLED: + return full_name + + if cls._state_id is not None: + return _int_to_minified_name(cls._state_id) + + # state_id not set + if minify_mode == StateMinifyMode.ENFORCE: + msg = ( + f"State '{cls.__module__}.{cls.__name__}' is missing required state_id. " + f"Add state_id parameter: class {cls.__name__}(rx.State, state_id=N)" + ) + raise StateValueError(msg) + + # ENABLED mode with no state_id - use full name + return full_name @classmethod @functools.lru_cache @@ -2464,7 +2542,7 @@ def is_serializable(value: Any) -> bool: T_STATE = TypeVar("T_STATE", bound=BaseState) -class State(BaseState): +class State(BaseState, state_id=0): """The app Base State.""" # The hydrated bool. diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 8a8f0e754d1..549b4896d57 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -3540,6 +3540,7 @@ def __new__( bases: tuple[type], namespace: dict[str, Any], mixin: bool = False, + state_id: int | None = None, ) -> type: """Create a new class. @@ -3548,6 +3549,7 @@ def __new__( bases: The bases of the class. namespace: The namespace of the class. mixin: Whether the class is a mixin and should not be instantiated. + state_id: Explicit state ID for minified state names. Returns: The new class. @@ -3648,6 +3650,9 @@ def __new__( namespace["__inherited_fields__"] = inherited_fields namespace["__fields__"] = inherited_fields | own_fields namespace["_mixin"] = mixin + # Pass state_id to __init_subclass__ if provided (for BaseState subclasses) + if state_id is not None: + return super().__new__(cls, name, bases, namespace, state_id=state_id) return super().__new__(cls, name, bases, namespace) diff --git a/tests/integration/test_state_minification.py b/tests/integration/test_state_minification.py new file mode 100644 index 00000000000..8c0754cea4b --- /dev/null +++ b/tests/integration/test_state_minification.py @@ -0,0 +1,290 @@ +"""Integration tests for explicit state ID minification.""" + +from __future__ import annotations + +import os +from collections.abc import Generator +from functools import partial +from typing import TYPE_CHECKING + +import pytest +from selenium.webdriver.common.by import By + +from reflex.environment import StateMinifyMode, environment +from reflex.state import _int_to_minified_name, _state_id_registry +from reflex.testing import AppHarness + +if TYPE_CHECKING: + from selenium.webdriver.remote.webdriver import WebDriver + + +def StateMinificationApp(root_state_id: int, sub_state_id: int): + """Test app for state minification. + + Args: + root_state_id: The state_id for the root state. + sub_state_id: The state_id for the sub state. + """ + import reflex as rx + + class RootState(rx.State, state_id=root_state_id): + """Root state with explicit state_id.""" + + count: int = 0 + + @rx.event + def increment(self): + """Increment the count.""" + self.count += 1 + + class SubState(RootState, state_id=sub_state_id): + """Sub state with explicit state_id.""" + + message: str = "hello" + + @rx.event + def update_message(self): + """Update the message.""" + parent = self.parent_state + assert parent is not None + assert isinstance(parent, RootState) + self.message = f"count is {parent.count}" + + def index() -> rx.Component: + return rx.vstack( + rx.input( + value=RootState.router.session.client_token, + is_read_only=True, + id="token", + ), + rx.text(f"Root state name: {RootState.get_name()}", id="root_state_name"), + rx.text(f"Sub state name: {SubState.get_name()}", id="sub_state_name"), + rx.text("Count: ", id="count_label"), + rx.text(RootState.count, id="count_value"), + rx.text("Message: ", id="message_label"), + rx.text(SubState.message, id="message_value"), + rx.button("Increment", on_click=RootState.increment, id="increment_btn"), + rx.button( + "Update Message", on_click=SubState.update_message, id="update_msg_btn" + ), + ) + + app = rx.App() + app.add_page(index) + + +@pytest.fixture(autouse=True) +def reset_state_registry(): + """Reset the state_id registry before and after each test.""" + _state_id_registry.clear() + yield + _state_id_registry.clear() + + +@pytest.fixture +def minify_disabled_app( + app_harness_env: type[AppHarness], + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[AppHarness, None, None]: + """Start app with REFLEX_MINIFY_STATES=disabled. + + Args: + app_harness_env: AppHarness or AppHarnessProd + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + Running AppHarness instance + """ + os.environ["REFLEX_MINIFY_STATES"] = "disabled" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + + with app_harness_env.create( + root=tmp_path_factory.mktemp("state_minify_disabled"), + app_name="state_minify_disabled", + app_source=partial(StateMinificationApp, root_state_id=0, sub_state_id=1), + ) as harness: + yield harness + + # Cleanup + os.environ.pop("REFLEX_MINIFY_STATES", None) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + + +@pytest.fixture +def minify_enabled_app( + app_harness_env: type[AppHarness], + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[AppHarness, None, None]: + """Start app with REFLEX_MINIFY_STATES=enabled. + + Args: + app_harness_env: AppHarness or AppHarnessProd + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + Running AppHarness instance + """ + os.environ["REFLEX_MINIFY_STATES"] = "enabled" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + + with app_harness_env.create( + root=tmp_path_factory.mktemp("state_minify_enabled"), + app_name="state_minify_enabled", + app_source=partial(StateMinificationApp, root_state_id=10, sub_state_id=11), + ) as harness: + yield harness + + # Cleanup + os.environ.pop("REFLEX_MINIFY_STATES", None) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + + +@pytest.fixture +def driver_disabled( + minify_disabled_app: AppHarness, +) -> Generator[WebDriver, None, None]: + """Get browser instance for disabled mode app. + + Args: + minify_disabled_app: harness for the app + + Yields: + WebDriver instance. + """ + assert minify_disabled_app.app_instance is not None, "app is not running" + driver = minify_disabled_app.frontend() + try: + yield driver + finally: + driver.quit() + + +@pytest.fixture +def driver_enabled( + minify_enabled_app: AppHarness, +) -> Generator[WebDriver, None, None]: + """Get browser instance for enabled mode app. + + Args: + minify_enabled_app: harness for the app + + Yields: + WebDriver instance. + """ + assert minify_enabled_app.app_instance is not None, "app is not running" + driver = minify_enabled_app.frontend() + try: + yield driver + finally: + driver.quit() + + +def test_state_minification_disabled( + minify_disabled_app: AppHarness, + driver_disabled: WebDriver, +) -> None: + """Test that DISABLED mode uses full state names. + + Args: + minify_disabled_app: harness for the app + driver_disabled: WebDriver instance + """ + assert minify_disabled_app.app_instance is not None + + # Wait for the app to load + token_input = AppHarness.poll_for_or_raise_timeout( + lambda: driver_disabled.find_element(By.ID, "token") + ) + assert token_input + token = minify_disabled_app.poll_for_value(token_input) + assert token + + # Check state names are full names (not minified) + root_state_name_el = driver_disabled.find_element(By.ID, "root_state_name") + sub_state_name_el = driver_disabled.find_element(By.ID, "sub_state_name") + + root_state_name = root_state_name_el.text + sub_state_name = sub_state_name_el.text + + # In disabled mode, names should be the full module___class_name format + assert "root_state" in root_state_name.lower() + assert "sub_state" in sub_state_name.lower() + # Full names should be long (not single char minified names) + # Extract just the state name part after "Root state name: " + root_name_only = ( + root_state_name.split(": ")[-1] if ": " in root_state_name else root_state_name + ) + sub_name_only = ( + sub_state_name.split(": ")[-1] if ": " in sub_state_name else sub_state_name + ) + assert len(root_name_only) > 5, f"Expected long name, got: {root_name_only}" + assert len(sub_name_only) > 5, f"Expected long name, got: {sub_name_only}" + + # Test that state updates work + count_value = driver_disabled.find_element(By.ID, "count_value") + assert count_value.text == "0" + + increment_btn = driver_disabled.find_element(By.ID, "increment_btn") + increment_btn.click() + + # Wait for count to update + AppHarness._poll_for(lambda: count_value.text == "1") + assert count_value.text == "1" + + +def test_state_minification_enabled( + minify_enabled_app: AppHarness, + driver_enabled: WebDriver, +) -> None: + """Test that ENABLED mode uses minified state names. + + Args: + minify_enabled_app: harness for the app + driver_enabled: WebDriver instance + """ + assert minify_enabled_app.app_instance is not None + + # Wait for the app to load + token_input = AppHarness.poll_for_or_raise_timeout( + lambda: driver_enabled.find_element(By.ID, "token") + ) + assert token_input + token = minify_enabled_app.poll_for_value(token_input) + assert token + + # Check state names are minified + root_state_name_el = driver_enabled.find_element(By.ID, "root_state_name") + sub_state_name_el = driver_enabled.find_element(By.ID, "sub_state_name") + + root_state_name = root_state_name_el.text + sub_state_name = sub_state_name_el.text + + # In enabled mode with state_id, names should be minified + # state_id=10 -> 'k', state_id=11 -> 'l' + expected_root_minified = _int_to_minified_name(10) + expected_sub_minified = _int_to_minified_name(11) + + assert expected_root_minified in root_state_name + assert expected_sub_minified in sub_state_name + + # Test that state updates work with minified names + count_value = driver_enabled.find_element(By.ID, "count_value") + assert count_value.text == "0" + + increment_btn = driver_enabled.find_element(By.ID, "increment_btn") + increment_btn.click() + + # Wait for count to update + AppHarness._poll_for(lambda: count_value.text == "1") + assert count_value.text == "1" + + # Test substate event handler works + message_value = driver_enabled.find_element(By.ID, "message_value") + assert message_value.text == "hello" + + update_msg_btn = driver_enabled.find_element(By.ID, "update_msg_btn") + update_msg_btn.click() + + # Wait for message to update + AppHarness._poll_for(lambda: "count is 1" in message_value.text) + assert message_value.text == "count is 1" diff --git a/tests/units/test_state_minification.py b/tests/units/test_state_minification.py new file mode 100644 index 00000000000..5cc446e9d1b --- /dev/null +++ b/tests/units/test_state_minification.py @@ -0,0 +1,176 @@ +"""Unit tests for state name minification.""" + +from __future__ import annotations + +import pytest + +from reflex.environment import StateMinifyMode, environment +from reflex.state import BaseState, _int_to_minified_name, _state_id_registry +from reflex.utils.exceptions import StateValueError + + +@pytest.fixture(autouse=True) +def reset_state_registry(): + """Reset the state_id registry before and after each test.""" + _state_id_registry.clear() + yield + _state_id_registry.clear() + + +@pytest.fixture +def reset_minify_mode(): + """Reset REFLEX_MINIFY_STATES to DISABLED after each test.""" + original = environment.REFLEX_MINIFY_STATES.get() + yield + environment.REFLEX_MINIFY_STATES.set(original) + + +class TestIntToMinifiedName: + """Tests for _int_to_minified_name function.""" + + def test_zero(self): + """Test that 0 maps to 'a'.""" + assert _int_to_minified_name(0) == "a" + + def test_single_char(self): + """Test single character mappings.""" + assert _int_to_minified_name(1) == "b" + assert _int_to_minified_name(25) == "z" + assert _int_to_minified_name(26) == "A" + assert _int_to_minified_name(51) == "Z" + assert _int_to_minified_name(52) == "$" + assert _int_to_minified_name(53) == "_" + + def test_two_chars(self): + """Test two character mappings (base 54).""" + # 54 = 1*54 + 0 -> 'ba' + assert _int_to_minified_name(54) == "ba" + # 55 = 1*54 + 1 -> 'bb' + assert _int_to_minified_name(55) == "bb" + + def test_unique_names(self): + """Test that a large range of IDs produce unique names.""" + names = set() + for i in range(10000): + name = _int_to_minified_name(i) + assert name not in names, f"Duplicate name {name} for id {i}" + names.add(name) + + +class TestStateIdValidation: + """Tests for state_id validation in __init_subclass__.""" + + def test_state_with_explicit_id(self): + """Test that a state can be created with an explicit state_id.""" + + class TestState(BaseState, state_id=100): + pass + + assert TestState._state_id == 100 + assert 100 in _state_id_registry + assert _state_id_registry[100] is TestState + + def test_state_without_id(self): + """Test that a state can be created without state_id.""" + + class TestState(BaseState): + pass + + assert TestState._state_id is None + + def test_duplicate_state_id_raises(self): + """Test that duplicate state_id raises StateValueError.""" + + class FirstState(BaseState, state_id=200): + pass + + with pytest.raises(StateValueError, match="Duplicate state_id=200"): + + class SecondState(BaseState, state_id=200): + pass + + +class TestGetNameMinification: + """Tests for get_name with minification modes.""" + + def test_disabled_mode_uses_full_name(self, reset_minify_mode): + """Test DISABLED mode always uses full name even with state_id.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + + class TestState(BaseState, state_id=300): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + # Should be full name, not minified + assert "test_state" in name.lower() + assert name != _int_to_minified_name(300) + + def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): + """Test ENABLED mode with state_id uses minified name.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + + class TestState(BaseState, state_id=301): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + assert name == _int_to_minified_name(301) + + def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): + """Test ENABLED mode without state_id uses full name.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + + class TestState(BaseState): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + # Should contain the class name + assert "test_state" in name.lower() + + def test_enforce_mode_without_id_raises(self, reset_minify_mode): + """Test ENFORCE mode without state_id raises error during class definition.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) + + # Error is raised during class definition because get_name() is called + # during __init_subclass__ + with pytest.raises(StateValueError, match="missing required state_id"): + + class TestState(BaseState): + pass + + def test_enforce_mode_with_id_uses_minified(self, reset_minify_mode): + """Test ENFORCE mode with state_id uses minified name.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) + + class TestState(BaseState, state_id=302): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + assert name == _int_to_minified_name(302) + + +class TestMixinState: + """Tests for mixin states.""" + + def test_mixin_no_state_id_required(self, reset_minify_mode): + """Test that mixin states don't require state_id even in ENFORCE mode.""" + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) + + class MixinState(BaseState, mixin=True): + pass + + # Mixin states should not raise even without state_id + assert MixinState._state_id is None + # Mixin states have _mixin = True set, so get_name isn't typically called + # but the class should be created without error diff --git a/uploaded_files/test.txt b/uploaded_files/test.txt new file mode 100644 index 00000000000..fdffb5316f1 --- /dev/null +++ b/uploaded_files/test.txt @@ -0,0 +1 @@ +test file contents! \ No newline at end of file From 1c5114d5eee0a0ff336b03834298da776e1f7274 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 24 Jan 2026 20:54:10 +0100 Subject: [PATCH 02/74] drop uploaded_files, don't allow state_id for mixins --- reflex/state.py | 15 +++++++++++---- tests/units/test_state_minification.py | 7 +++++++ uploaded_files/test.txt | 1 - 3 files changed, 18 insertions(+), 5 deletions(-) delete mode 100644 uploaded_files/test.txt diff --git a/reflex/state.py b/reflex/state.py index 888e19b75c3..6e745cdd26d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -556,7 +556,17 @@ def __init_subclass__( super().__init_subclass__(**kwargs) - # Store state_id as class variable + # Mixin states cannot have state_id + if cls._mixin: + if state_id is not None: + msg = ( + f"Mixin state '{cls.__module__}.{cls.__name__}' cannot have a state_id. " + "Remove state_id or mixin=True." + ) + raise StateValueError(msg) + return + + # Store state_id as class variable (only for non-mixins) cls._state_id = state_id # Validate state_id if provided (check for duplicates) @@ -575,9 +585,6 @@ def __init_subclass__( raise StateValueError(msg) _state_id_registry[state_id] = cls - if cls._mixin: - return - # Handle locally-defined states for pickling. if "" in cls.__qualname__: cls._handle_local_def() diff --git a/tests/units/test_state_minification.py b/tests/units/test_state_minification.py index 5cc446e9d1b..ac2148efa60 100644 --- a/tests/units/test_state_minification.py +++ b/tests/units/test_state_minification.py @@ -174,3 +174,10 @@ class MixinState(BaseState, mixin=True): assert MixinState._state_id is None # Mixin states have _mixin = True set, so get_name isn't typically called # but the class should be created without error + + def test_mixin_with_state_id_raises(self): + """Test that mixin states cannot have state_id.""" + with pytest.raises(StateValueError, match="cannot have a state_id"): + + class MixinWithId(BaseState, mixin=True, state_id=999): + pass diff --git a/uploaded_files/test.txt b/uploaded_files/test.txt deleted file mode 100644 index fdffb5316f1..00000000000 --- a/uploaded_files/test.txt +++ /dev/null @@ -1 +0,0 @@ -test file contents! \ No newline at end of file From 62bf127ad09b03451ed19adaca8c985eefdb80a2 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:09:03 +0100 Subject: [PATCH 03/74] Update reflex/state.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- reflex/state.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reflex/state.py b/reflex/state.py index 6e745cdd26d..6f21c8ac949 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -114,7 +114,13 @@ def _int_to_minified_name(state_id: int) -> str: Returns: The minified state name (e.g., 0->'a', 1->'b', 54->'ba'). + + Raises: + ValueError: If state_id is negative. """ + if state_id < 0: + raise ValueError(f"state_id must be non-negative, got {state_id}") + # All possible chars for minified state name (valid JS identifiers) chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" base = len(chars) From 0858c9e2eac54619b94f257b43f70af3310c3a60 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 24 Jan 2026 21:23:24 +0100 Subject: [PATCH 04/74] ruffing --- reflex/state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 6f21c8ac949..1795b98b719 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -119,8 +119,9 @@ def _int_to_minified_name(state_id: int) -> str: ValueError: If state_id is negative. """ if state_id < 0: - raise ValueError(f"state_id must be non-negative, got {state_id}") - + msg = f"state_id must be non-negative, got {state_id}" + raise ValueError(msg) + # All possible chars for minified state name (valid JS identifiers) chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" base = len(chars) From be13be5a2391a26e71fe40918ac7dd20c169c2ee Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 24 Jan 2026 22:43:27 +0100 Subject: [PATCH 05/74] minify event names as well --- .gitignore | 3 +- reflex/environment.py | 15 +- reflex/event.py | 9 + reflex/state.py | 75 +++- reflex/utils/format.py | 15 +- ...e_minification.py => test_minification.py} | 145 +++++- tests/units/test_minification.py | 411 ++++++++++++++++++ tests/units/test_state_minification.py | 183 -------- 8 files changed, 637 insertions(+), 219 deletions(-) rename tests/integration/{test_state_minification.py => test_minification.py} (60%) create mode 100644 tests/units/test_minification.py delete mode 100644 tests/units/test_state_minification.py diff --git a/.gitignore b/.gitignore index 508d57ca9d6..c4c0a159276 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ reflex.db node_modules package-lock.json *.pyi -.pre-commit-config.yaml \ No newline at end of file +.pre-commit-config.yaml +uploaded_files/* diff --git a/reflex/environment.py b/reflex/environment.py index 481caa4ce4d..87fcf4484d7 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -487,12 +487,12 @@ class PerformanceMode(enum.Enum): @enum.unique -class StateMinifyMode(enum.Enum): - """Mode for state name minification.""" +class MinifyMode(enum.Enum): + """Mode for state/event name minification.""" - DISABLED = "disabled" # Never minify state names (default) - ENABLED = "enabled" # Minify states that have explicit state_id - ENFORCE = "enforce" # Require all non-mixin states to have state_id + DISABLED = "disabled" # Never minify names (default) + ENABLED = "enabled" # Minify items that have explicit IDs + ENFORCE = "enforce" # Require all items to have explicit IDs class ExecutorType(enum.Enum): @@ -698,7 +698,10 @@ class EnvironmentVariables: REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) # State name minification mode: disabled, enabled, or enforce. - REFLEX_MINIFY_STATES: EnvVar[StateMinifyMode] = env_var(StateMinifyMode.DISABLED) + REFLEX_MINIFY_STATES: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + + # Event handler name minification mode: disabled, enabled, or enforce. + REFLEX_MINIFY_EVENTS: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) diff --git a/reflex/event.py b/reflex/event.py index 748fcd27802..e0c7367bfdd 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -89,6 +89,7 @@ def substate_token(self) -> str: _EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} BACKGROUND_TASK_MARKER = "_reflex_background_task" +EVENT_ID_MARKER = "_rx_event_id" @dataclasses.dataclass( @@ -2311,6 +2312,7 @@ class EventNamespace: # Constants BACKGROUND_TASK_MARKER = BACKGROUND_TASK_MARKER + EVENT_ID_MARKER = EVENT_ID_MARKER _EVENT_FIELDS = _EVENT_FIELDS FORM_DATA = FORM_DATA upload_files = upload_files @@ -2334,6 +2336,7 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, + event_id: int | None = None, ) -> Callable[ [Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse] ]: ... @@ -2349,6 +2352,7 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, + event_id: int | None = None, ) -> EventCallback[Unpack[P]]: ... def __new__( @@ -2361,6 +2365,7 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, + event_id: int | None = None, ) -> ( EventCallback[Unpack[P]] | Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]] @@ -2375,6 +2380,7 @@ def __new__( throttle: Throttle the event handler to limit calls (in milliseconds). debounce: Debounce the event handler to delay calls (in milliseconds). temporal: Whether the event should be dropped when the backend is down. + event_id: Optional integer ID for deterministic minified event names. Raises: TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402 @@ -2462,6 +2468,9 @@ def wrapper( event_actions = _build_event_actions() if event_actions: func._rx_event_actions = event_actions # pyright: ignore [reportFunctionMemberAccess] + # Store event_id on the function for minification + if event_id is not None: + setattr(func, EVENT_ID_MARKER, event_id) return func # pyright: ignore [reportReturnType] if func is not None: diff --git a/reflex/state.py b/reflex/state.py index 1795b98b719..e7109fca4ce 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -42,6 +42,7 @@ from reflex.environment import PerformanceMode, environment from reflex.event import ( BACKGROUND_TASK_MARKER, + EVENT_ID_MARKER, Event, EventHandler, EventSpec, @@ -429,6 +430,9 @@ class BaseState(EvenMoreBasicBaseState): # The explicit state ID for minification (None = use full name). _state_id: ClassVar[int | None] = None + # Per-class registry mapping event_id -> event handler name for minification. + _event_id_to_name: ClassVar[builtins.dict[int, str]] = {} + # The parent state. parent_state: BaseState | None = field(default=None, is_var=False) @@ -711,6 +715,36 @@ def __init_subclass__( cls.event_handlers[name] = handler setattr(cls, name, handler) + # Build event_id registry and validate uniqueness within this state class + cls._event_id_to_name = {} + missing_event_ids: list[str] = [] + for name, fn in events.items(): + event_id = getattr(fn, EVENT_ID_MARKER, None) + if event_id is not None: + if event_id in cls._event_id_to_name: + existing_name = cls._event_id_to_name[event_id] + msg = ( + f"Duplicate event_id={event_id} in state '{cls.__name__}': " + f"handlers '{existing_name}' and '{name}' cannot share the same event_id." + ) + raise StateValueError(msg) + cls._event_id_to_name[event_id] = name + else: + missing_event_ids.append(name) + + # In ENFORCE mode, all event handlers must have event_id + from reflex.environment import MinifyMode + + if ( + environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENFORCE + and missing_event_ids + ): + msg = ( + f"State '{cls.__name__}' in ENFORCE mode: event handlers " + f"{missing_event_ids} are missing required event_id." + ) + raise StateValueError(msg) + # Initialize per-class var dependency tracking. cls._var_dependencies = {} cls._init_var_dependency_dicts() @@ -753,6 +787,10 @@ def _copy_fn(fn: Callable) -> Callable: newfn.__annotations__ = fn.__annotations__ if mark := getattr(fn, BACKGROUND_TASK_MARKER, None): setattr(newfn, BACKGROUND_TASK_MARKER, mark) + # Preserve event_id for minification + event_id = getattr(fn, EVENT_ID_MARKER, None) + if event_id is not None: + object.__setattr__(newfn, EVENT_ID_MARKER, event_id) return newfn @staticmethod @@ -1059,7 +1097,7 @@ def get_name(cls) -> str: Raises: StateValueError: If ENFORCE mode is set and state_id is missing. """ - from reflex.environment import StateMinifyMode + from reflex.environment import MinifyMode from reflex.utils.exceptions import StateValueError module = cls.__module__.replace(".", "___") @@ -1067,14 +1105,14 @@ def get_name(cls) -> str: minify_mode = environment.REFLEX_MINIFY_STATES.get() - if minify_mode == StateMinifyMode.DISABLED: + if minify_mode == MinifyMode.DISABLED: return full_name if cls._state_id is not None: return _int_to_minified_name(cls._state_id) # state_id not set - if minify_mode == StateMinifyMode.ENFORCE: + if minify_mode == MinifyMode.ENFORCE: msg = ( f"State '{cls.__module__}.{cls.__name__}' is missing required state_id. " f"Add state_id parameter: class {cls.__name__}(rx.State, state_id=N)" @@ -1797,6 +1835,25 @@ async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE: ) return getattr(other_state, var_data.field_name) + @classmethod + def _get_original_event_name(cls, minified_name: str) -> str | None: + """Look up the original event handler name from a minified name. + + This is used when the frontend sends back minified event names + and the backend needs to find the actual event handler. + + Args: + minified_name: The minified event name (e.g., 'a'). + + Returns: + The original event handler name, or None if not found. + """ + # Build reverse lookup: minified_name -> original_name + for event_id, original_name in cls._event_id_to_name.items(): + if _int_to_minified_name(event_id) == minified_name: + return original_name + return None + def _get_event_handler( self, event: Event ) -> tuple[BaseState | StateProxy, EventHandler]: @@ -1819,7 +1876,17 @@ def _get_event_handler( if not substate: msg = "The value of state cannot be None when processing an event." raise ValueError(msg) - handler = substate.event_handlers[name] + + # Try to look up the handler directly first + handler = substate.event_handlers.get(name) + if handler is None: + # If not found, the name might be minified - try reverse lookup + original_name = substate._get_original_event_name(name) + if original_name is not None: + handler = substate.event_handlers.get(original_name) + if handler is None: + msg = f"Event handler '{name}' not found in state '{type(substate).__name__}'" + raise KeyError(msg) # For background tasks, proxy the state if handler.is_background: diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 3093e455d55..6e094049103 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -446,8 +446,12 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: handler: The event handler to get the parts of. Returns: - The state and function name. + The state and function name (possibly minified based on REFLEX_MINIFY_EVENTS). """ + from reflex.environment import MinifyMode, environment + from reflex.event import EVENT_ID_MARKER + from reflex.state import State, _int_to_minified_name + # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") @@ -461,11 +465,16 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: # Get the function name name = parts[-1] - from reflex.state import State - if state_full_name == FRONTEND_EVENT_STATE and name not in State.__dict__: return ("", to_snake_case(handler.fn.__qualname__)) + # Check for event_id minification + mode = environment.REFLEX_MINIFY_EVENTS.get() + if mode != MinifyMode.DISABLED: + event_id = getattr(handler.fn, EVENT_ID_MARKER, None) + if event_id is not None: + name = _int_to_minified_name(event_id) + return (state_full_name, name) diff --git a/tests/integration/test_state_minification.py b/tests/integration/test_minification.py similarity index 60% rename from tests/integration/test_state_minification.py rename to tests/integration/test_minification.py index 8c0754cea4b..b60c7f16260 100644 --- a/tests/integration/test_state_minification.py +++ b/tests/integration/test_minification.py @@ -1,4 +1,4 @@ -"""Integration tests for explicit state ID minification.""" +"""Integration tests for state and event handler minification.""" from __future__ import annotations @@ -10,7 +10,7 @@ import pytest from selenium.webdriver.common.by import By -from reflex.environment import StateMinifyMode, environment +from reflex.environment import MinifyMode, environment from reflex.state import _int_to_minified_name, _state_id_registry from reflex.testing import AppHarness @@ -18,21 +18,29 @@ from selenium.webdriver.remote.webdriver import WebDriver -def StateMinificationApp(root_state_id: int, sub_state_id: int): - """Test app for state minification. +def MinificationApp( + root_state_id: int, + sub_state_id: int, + increment_event_id: int | None = None, + update_message_event_id: int | None = None, +): + """Test app for state and event handler minification. Args: root_state_id: The state_id for the root state. sub_state_id: The state_id for the sub state. + increment_event_id: The event_id for the increment event handler. + update_message_event_id: The event_id for the update_message event handler. """ import reflex as rx + from reflex.utils import format class RootState(rx.State, state_id=root_state_id): """Root state with explicit state_id.""" count: int = 0 - @rx.event + @rx.event(event_id=increment_event_id) def increment(self): """Increment the count.""" self.count += 1 @@ -42,7 +50,7 @@ class SubState(RootState, state_id=sub_state_id): message: str = "hello" - @rx.event + @rx.event(event_id=update_message_event_id) def update_message(self): """Update the message.""" parent = self.parent_state @@ -50,6 +58,15 @@ def update_message(self): assert isinstance(parent, RootState) self.message = f"count is {parent.count}" + # Get formatted event handler names for display + # Use event_handlers dict to get the actual EventHandler objects + increment_handler_name = format.format_event_handler( + RootState.event_handlers["increment"] + ) + update_handler_name = format.format_event_handler( + SubState.event_handlers["update_message"] + ) + def index() -> rx.Component: return rx.vstack( rx.input( @@ -59,6 +76,14 @@ def index() -> rx.Component: ), rx.text(f"Root state name: {RootState.get_name()}", id="root_state_name"), rx.text(f"Sub state name: {SubState.get_name()}", id="sub_state_name"), + rx.text( + f"Increment handler: {increment_handler_name}", + id="increment_handler_name", + ), + rx.text( + f"Update handler: {update_handler_name}", + id="update_handler_name", + ), rx.text("Count: ", id="count_label"), rx.text(RootState.count, id="count_value"), rx.text("Message: ", id="message_label"), @@ -96,18 +121,28 @@ def minify_disabled_app( Running AppHarness instance """ os.environ["REFLEX_MINIFY_STATES"] = "disabled" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + os.environ["REFLEX_MINIFY_EVENTS"] = "disabled" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) with app_harness_env.create( - root=tmp_path_factory.mktemp("state_minify_disabled"), - app_name="state_minify_disabled", - app_source=partial(StateMinificationApp, root_state_id=0, sub_state_id=1), + root=tmp_path_factory.mktemp("minify_disabled"), + app_name="minify_disabled", + app_source=partial( + MinificationApp, + root_state_id=0, + sub_state_id=1, + increment_event_id=0, + update_message_event_id=0, + ), ) as harness: yield harness # Cleanup os.environ.pop("REFLEX_MINIFY_STATES", None) - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + os.environ.pop("REFLEX_MINIFY_EVENTS", None) + environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) @pytest.fixture @@ -115,7 +150,7 @@ def minify_enabled_app( app_harness_env: type[AppHarness], tmp_path_factory: pytest.TempPathFactory, ) -> Generator[AppHarness, None, None]: - """Start app with REFLEX_MINIFY_STATES=enabled. + """Start app with minification enabled. Args: app_harness_env: AppHarness or AppHarnessProd @@ -125,18 +160,28 @@ def minify_enabled_app( Running AppHarness instance """ os.environ["REFLEX_MINIFY_STATES"] = "enabled" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + os.environ["REFLEX_MINIFY_EVENTS"] = "enabled" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) with app_harness_env.create( - root=tmp_path_factory.mktemp("state_minify_enabled"), - app_name="state_minify_enabled", - app_source=partial(StateMinificationApp, root_state_id=10, sub_state_id=11), + root=tmp_path_factory.mktemp("minify_enabled"), + app_name="minify_enabled", + app_source=partial( + MinificationApp, + root_state_id=10, + sub_state_id=11, + increment_event_id=0, + update_message_event_id=0, + ), ) as harness: yield harness # Cleanup os.environ.pop("REFLEX_MINIFY_STATES", None) - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + os.environ.pop("REFLEX_MINIFY_EVENTS", None) + environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) @pytest.fixture @@ -179,11 +224,11 @@ def driver_enabled( driver.quit() -def test_state_minification_disabled( +def test_minification_disabled( minify_disabled_app: AppHarness, driver_disabled: WebDriver, ) -> None: - """Test that DISABLED mode uses full state names. + """Test that DISABLED mode uses full state and event names. Args: minify_disabled_app: harness for the app @@ -220,6 +265,20 @@ def test_state_minification_disabled( assert len(root_name_only) > 5, f"Expected long name, got: {root_name_only}" assert len(sub_name_only) > 5, f"Expected long name, got: {sub_name_only}" + # Check event handler names are full names (not minified) + increment_handler_el = driver_disabled.find_element(By.ID, "increment_handler_name") + update_handler_el = driver_disabled.find_element(By.ID, "update_handler_name") + + increment_handler = increment_handler_el.text + update_handler = update_handler_el.text + + # In disabled mode, event handler names should contain the full method names + assert "increment" in increment_handler.lower() + assert "update_message" in update_handler.lower() + # The format should be "state_name.method_name", so check for the dot + assert "." in increment_handler + assert "." in update_handler + # Test that state updates work count_value = driver_disabled.find_element(By.ID, "count_value") assert count_value.text == "0" @@ -232,11 +291,11 @@ def test_state_minification_disabled( assert count_value.text == "1" -def test_state_minification_enabled( +def test_minification_enabled( minify_enabled_app: AppHarness, driver_enabled: WebDriver, ) -> None: - """Test that ENABLED mode uses minified state names. + """Test that ENABLED mode uses minified state and event names. Args: minify_enabled_app: harness for the app @@ -267,6 +326,48 @@ def test_state_minification_enabled( assert expected_root_minified in root_state_name assert expected_sub_minified in sub_state_name + # Check event handler names are minified + increment_handler_el = driver_enabled.find_element(By.ID, "increment_handler_name") + update_handler_el = driver_enabled.find_element(By.ID, "update_handler_name") + + increment_handler_text = increment_handler_el.text + update_handler_text = update_handler_el.text + + # Extract just the handler name part after "Increment handler: " + increment_handler = ( + increment_handler_text.split(": ")[-1] + if ": " in increment_handler_text + else increment_handler_text + ) + update_handler = ( + update_handler_text.split(": ")[-1] + if ": " in update_handler_text + else update_handler_text + ) + + # In enabled mode with event_id, names should be minified + # event_id=0 -> 'a' for both handlers + expected_event_minified = _int_to_minified_name(0) + + # Event handler format: "state_name.event_name" + # For increment: "k.a" (state_id=10 -> 'k', event_id=0 -> 'a') + # For update_message: "k.l.a" (state_id=10.11 -> 'k.l', event_id=0 -> 'a') + # The event name should be minified to 'a' + assert increment_handler.endswith(f".{expected_event_minified}"), ( + f"Expected minified event name, got: {increment_handler}" + ) + assert update_handler.endswith(f".{expected_event_minified}"), ( + f"Expected minified event name, got: {update_handler}" + ) + + # The handler names should NOT contain the original method names + assert "increment" not in increment_handler.lower(), ( + f"Expected minified name without 'increment', got: {increment_handler}" + ) + assert "update_message" not in update_handler.lower(), ( + f"Expected minified name without 'update_message', got: {update_handler}" + ) + # Test that state updates work with minified names count_value = driver_enabled.find_element(By.ID, "count_value") assert count_value.text == "0" @@ -278,7 +379,7 @@ def test_state_minification_enabled( AppHarness._poll_for(lambda: count_value.text == "1") assert count_value.text == "1" - # Test substate event handler works + # Test substate event handler works with minified names message_value = driver_enabled.find_element(By.ID, "message_value") assert message_value.text == "hello" diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py new file mode 100644 index 00000000000..3dcc72372ae --- /dev/null +++ b/tests/units/test_minification.py @@ -0,0 +1,411 @@ +"""Unit tests for state and event handler minification.""" + +from __future__ import annotations + +import pytest + +from reflex.environment import MinifyMode, environment +from reflex.event import EVENT_ID_MARKER +from reflex.state import BaseState, _int_to_minified_name, _state_id_registry +from reflex.utils.exceptions import StateValueError + + +@pytest.fixture(autouse=True) +def reset_state_registry(): + """Reset the state_id registry before and after each test.""" + _state_id_registry.clear() + yield + _state_id_registry.clear() + + +@pytest.fixture +def reset_minify_mode(): + """Reset minify modes to DISABLED after each test.""" + original_states = environment.REFLEX_MINIFY_STATES.get() + original_events = environment.REFLEX_MINIFY_EVENTS.get() + yield + environment.REFLEX_MINIFY_STATES.set(original_states) + environment.REFLEX_MINIFY_EVENTS.set(original_events) + + +class TestIntToMinifiedName: + """Tests for _int_to_minified_name function.""" + + def test_zero(self): + """Test that 0 maps to 'a'.""" + assert _int_to_minified_name(0) == "a" + + def test_single_char(self): + """Test single character mappings.""" + assert _int_to_minified_name(1) == "b" + assert _int_to_minified_name(25) == "z" + assert _int_to_minified_name(26) == "A" + assert _int_to_minified_name(51) == "Z" + assert _int_to_minified_name(52) == "$" + assert _int_to_minified_name(53) == "_" + + def test_two_chars(self): + """Test two character mappings (base 54).""" + # 54 = 1*54 + 0 -> 'ba' + assert _int_to_minified_name(54) == "ba" + # 55 = 1*54 + 1 -> 'bb' + assert _int_to_minified_name(55) == "bb" + + def test_unique_names(self): + """Test that a large range of IDs produce unique names.""" + names = set() + for i in range(10000): + name = _int_to_minified_name(i) + assert name not in names, f"Duplicate name {name} for id {i}" + names.add(name) + + +class TestStateIdValidation: + """Tests for state_id validation in __init_subclass__.""" + + def test_state_with_explicit_id(self): + """Test that a state can be created with an explicit state_id.""" + + class TestState(BaseState, state_id=100): + pass + + assert TestState._state_id == 100 + assert 100 in _state_id_registry + assert _state_id_registry[100] is TestState + + def test_state_without_id(self): + """Test that a state can be created without state_id.""" + + class TestState(BaseState): + pass + + assert TestState._state_id is None + + def test_duplicate_state_id_raises(self): + """Test that duplicate state_id raises StateValueError.""" + + class FirstState(BaseState, state_id=200): + pass + + with pytest.raises(StateValueError, match="Duplicate state_id=200"): + + class SecondState(BaseState, state_id=200): + pass + + +class TestGetNameMinification: + """Tests for get_name with minification modes.""" + + def test_disabled_mode_uses_full_name(self, reset_minify_mode): + """Test DISABLED mode always uses full name even with state_id.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + + class TestState(BaseState, state_id=300): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + # Should be full name, not minified + assert "test_state" in name.lower() + assert name != _int_to_minified_name(300) + + def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): + """Test ENABLED mode with state_id uses minified name.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + + class TestState(BaseState, state_id=301): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + assert name == _int_to_minified_name(301) + + def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): + """Test ENABLED mode without state_id uses full name.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + + class TestState(BaseState): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + # Should contain the class name + assert "test_state" in name.lower() + + def test_enforce_mode_without_id_raises(self, reset_minify_mode): + """Test ENFORCE mode without state_id raises error during class definition.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + + # Error is raised during class definition because get_name() is called + # during __init_subclass__ + with pytest.raises(StateValueError, match="missing required state_id"): + + class TestState(BaseState): + pass + + def test_enforce_mode_with_id_uses_minified(self, reset_minify_mode): + """Test ENFORCE mode with state_id uses minified name.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + + class TestState(BaseState, state_id=302): + pass + + # Clear the lru_cache to get fresh result + TestState.get_name.cache_clear() + + name = TestState.get_name() + assert name == _int_to_minified_name(302) + + +class TestMixinState: + """Tests for mixin states.""" + + def test_mixin_no_state_id_required(self, reset_minify_mode): + """Test that mixin states don't require state_id even in ENFORCE mode.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + + class MixinState(BaseState, mixin=True): + pass + + # Mixin states should not raise even without state_id + assert MixinState._state_id is None + # Mixin states have _mixin = True set, so get_name isn't typically called + # but the class should be created without error + + def test_mixin_with_state_id_raises(self): + """Test that mixin states cannot have state_id.""" + with pytest.raises(StateValueError, match="cannot have a state_id"): + + class MixinWithId(BaseState, mixin=True, state_id=999): + pass + + +class TestEventIdValidation: + """Tests for event_id validation in __init_subclass__.""" + + def test_event_with_explicit_id(self): + """Test that an event handler can be created with an explicit event_id.""" + import reflex as rx + + class TestState(BaseState, state_id=400): + @rx.event(event_id=0) + def my_handler(self): + pass + + assert 0 in TestState._event_id_to_name + assert TestState._event_id_to_name[0] == "my_handler" + + def test_event_without_id(self): + """Test that an event handler can be created without event_id.""" + import reflex as rx + + class TestState(BaseState, state_id=401): + @rx.event + def my_handler(self): + pass + + # Should not be in the registry + assert 0 not in TestState._event_id_to_name + + def test_duplicate_event_id_within_state_raises(self): + """Test that duplicate event_id within same state raises StateValueError.""" + import reflex as rx + + with pytest.raises(StateValueError, match="Duplicate event_id=0"): + + class TestState(BaseState, state_id=402): + @rx.event(event_id=0) + def handler1(self): + pass + + @rx.event(event_id=0) + def handler2(self): + pass + + def test_same_event_id_across_states_allowed(self): + """Test that same event_id can be used in different state classes.""" + import reflex as rx + + class StateA(BaseState, state_id=403): + @rx.event(event_id=0) + def handler(self): + pass + + class StateB(BaseState, state_id=404): + @rx.event(event_id=0) + def handler(self): + pass + + # Both should succeed - event_id is per-state + assert StateA._event_id_to_name[0] == "handler" + assert StateB._event_id_to_name[0] == "handler" + + def test_event_id_stored_on_function(self): + """Test that event_id is stored as EVENT_ID_MARKER on the function.""" + import reflex as rx + + @rx.event(event_id=42) + def standalone_handler(self): + pass + + assert hasattr(standalone_handler, EVENT_ID_MARKER) + assert getattr(standalone_handler, EVENT_ID_MARKER) == 42 + + +class TestEventHandlerMinification: + """Tests for event handler name minification in get_event_handler_parts.""" + + def test_disabled_mode_uses_full_name(self, reset_minify_mode): + """Test DISABLED mode uses full event name even with event_id.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) + + class TestState(BaseState, state_id=500): + @rx.event(event_id=0) + def my_handler(self): + pass + + handler = TestState.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) + + # Should use full name, not minified + assert event_name == "my_handler" + + def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): + """Test ENABLED mode with event_id uses minified name.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + + class TestState(BaseState, state_id=501): + @rx.event(event_id=5) + def my_handler(self): + pass + + TestState.get_name.cache_clear() + handler = TestState.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) + + # Should use minified name + assert event_name == _int_to_minified_name(5) + assert event_name == "f" + + def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): + """Test ENABLED mode without event_id uses full name.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + + class TestState(BaseState, state_id=502): + @rx.event + def my_handler(self): + pass + + TestState.get_name.cache_clear() + handler = TestState.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) + + # Should use full name + assert event_name == "my_handler" + + def test_enforce_mode_without_event_id_raises(self, reset_minify_mode): + """Test ENFORCE mode without event_id raises error during class definition.""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENFORCE) + + with pytest.raises(StateValueError, match="missing required event_id"): + + class TestState(BaseState, state_id=503): + @rx.event + def my_handler(self): + pass + + def test_enforce_mode_with_event_id_works(self, reset_minify_mode): + """Test ENFORCE mode with event_id creates state successfully.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENFORCE) + + class TestState(BaseState, state_id=504): + @rx.event(event_id=0) + def my_handler(self): + pass + + TestState.get_name.cache_clear() + handler = TestState.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) + + # Should use minified name + assert event_name == _int_to_minified_name(0) + assert event_name == "a" + + +class TestMixinEventHandlers: + """Tests for event handlers from mixin states.""" + + def test_mixin_event_id_preserved(self, reset_minify_mode): + """Test that event_id from mixin handlers is preserved when inherited.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + + class MixinState(BaseState, mixin=True): + @rx.event(event_id=10) + def mixin_handler(self): + pass + + # Need to inherit from both mixin AND a non-mixin base (BaseState) + # to create a non-mixin concrete state + class ConcreteState(MixinState, BaseState, state_id=600): + @rx.event(event_id=0) + def own_handler(self): + pass + + ConcreteState.get_name.cache_clear() + + # Both handlers should have their event_ids preserved + assert 10 in ConcreteState._event_id_to_name + assert ConcreteState._event_id_to_name[10] == "mixin_handler" + assert 0 in ConcreteState._event_id_to_name + assert ConcreteState._event_id_to_name[0] == "own_handler" + + # Check minified names + mixin_handler = ConcreteState.event_handlers["mixin_handler"] + own_handler = ConcreteState.event_handlers["own_handler"] + + _, mixin_name = get_event_handler_parts(mixin_handler) + _, own_name = get_event_handler_parts(own_handler) + + assert mixin_name == _int_to_minified_name(10) # "k" + assert own_name == _int_to_minified_name(0) # "a" + + def test_mixin_event_id_conflict_raises(self, reset_minify_mode): + """Test that conflicting event_ids from mixin and concrete state raises error.""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + + class MixinState(BaseState, mixin=True): + @rx.event(event_id=0) + def mixin_handler(self): + pass + + with pytest.raises(StateValueError, match="Duplicate event_id=0"): + # Need to inherit from both mixin AND a non-mixin base (BaseState) + class ConcreteState(MixinState, BaseState, state_id=601): + @rx.event(event_id=0) + def own_handler(self): + pass diff --git a/tests/units/test_state_minification.py b/tests/units/test_state_minification.py deleted file mode 100644 index ac2148efa60..00000000000 --- a/tests/units/test_state_minification.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Unit tests for state name minification.""" - -from __future__ import annotations - -import pytest - -from reflex.environment import StateMinifyMode, environment -from reflex.state import BaseState, _int_to_minified_name, _state_id_registry -from reflex.utils.exceptions import StateValueError - - -@pytest.fixture(autouse=True) -def reset_state_registry(): - """Reset the state_id registry before and after each test.""" - _state_id_registry.clear() - yield - _state_id_registry.clear() - - -@pytest.fixture -def reset_minify_mode(): - """Reset REFLEX_MINIFY_STATES to DISABLED after each test.""" - original = environment.REFLEX_MINIFY_STATES.get() - yield - environment.REFLEX_MINIFY_STATES.set(original) - - -class TestIntToMinifiedName: - """Tests for _int_to_minified_name function.""" - - def test_zero(self): - """Test that 0 maps to 'a'.""" - assert _int_to_minified_name(0) == "a" - - def test_single_char(self): - """Test single character mappings.""" - assert _int_to_minified_name(1) == "b" - assert _int_to_minified_name(25) == "z" - assert _int_to_minified_name(26) == "A" - assert _int_to_minified_name(51) == "Z" - assert _int_to_minified_name(52) == "$" - assert _int_to_minified_name(53) == "_" - - def test_two_chars(self): - """Test two character mappings (base 54).""" - # 54 = 1*54 + 0 -> 'ba' - assert _int_to_minified_name(54) == "ba" - # 55 = 1*54 + 1 -> 'bb' - assert _int_to_minified_name(55) == "bb" - - def test_unique_names(self): - """Test that a large range of IDs produce unique names.""" - names = set() - for i in range(10000): - name = _int_to_minified_name(i) - assert name not in names, f"Duplicate name {name} for id {i}" - names.add(name) - - -class TestStateIdValidation: - """Tests for state_id validation in __init_subclass__.""" - - def test_state_with_explicit_id(self): - """Test that a state can be created with an explicit state_id.""" - - class TestState(BaseState, state_id=100): - pass - - assert TestState._state_id == 100 - assert 100 in _state_id_registry - assert _state_id_registry[100] is TestState - - def test_state_without_id(self): - """Test that a state can be created without state_id.""" - - class TestState(BaseState): - pass - - assert TestState._state_id is None - - def test_duplicate_state_id_raises(self): - """Test that duplicate state_id raises StateValueError.""" - - class FirstState(BaseState, state_id=200): - pass - - with pytest.raises(StateValueError, match="Duplicate state_id=200"): - - class SecondState(BaseState, state_id=200): - pass - - -class TestGetNameMinification: - """Tests for get_name with minification modes.""" - - def test_disabled_mode_uses_full_name(self, reset_minify_mode): - """Test DISABLED mode always uses full name even with state_id.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - - class TestState(BaseState, state_id=300): - pass - - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - # Should be full name, not minified - assert "test_state" in name.lower() - assert name != _int_to_minified_name(300) - - def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): - """Test ENABLED mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) - - class TestState(BaseState, state_id=301): - pass - - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - assert name == _int_to_minified_name(301) - - def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): - """Test ENABLED mode without state_id uses full name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) - - class TestState(BaseState): - pass - - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - # Should contain the class name - assert "test_state" in name.lower() - - def test_enforce_mode_without_id_raises(self, reset_minify_mode): - """Test ENFORCE mode without state_id raises error during class definition.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - - # Error is raised during class definition because get_name() is called - # during __init_subclass__ - with pytest.raises(StateValueError, match="missing required state_id"): - - class TestState(BaseState): - pass - - def test_enforce_mode_with_id_uses_minified(self, reset_minify_mode): - """Test ENFORCE mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - - class TestState(BaseState, state_id=302): - pass - - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - assert name == _int_to_minified_name(302) - - -class TestMixinState: - """Tests for mixin states.""" - - def test_mixin_no_state_id_required(self, reset_minify_mode): - """Test that mixin states don't require state_id even in ENFORCE mode.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - - class MixinState(BaseState, mixin=True): - pass - - # Mixin states should not raise even without state_id - assert MixinState._state_id is None - # Mixin states have _mixin = True set, so get_name isn't typically called - # but the class should be created without error - - def test_mixin_with_state_id_raises(self): - """Test that mixin states cannot have state_id.""" - with pytest.raises(StateValueError, match="cannot have a state_id"): - - class MixinWithId(BaseState, mixin=True, state_id=999): - pass From e53c76af5bc1d5f94d6611ea74c8a71b92c58e3c Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 25 Jan 2026 00:53:35 +0100 Subject: [PATCH 06/74] wip fixing minifed state names + add cli --- reflex/.templates/web/utils/state.js | 17 +-- reflex/compiler/templates.py | 28 +++- reflex/constants/compiler.py | 12 -- reflex/reflex.py | 184 +++++++++++++++++++++++++++ reflex/state.py | 42 ++++-- tests/units/test_app.py | 2 +- tests/units/test_minification.py | 114 ++++++++++++++++- tests/units/test_state.py | 4 +- 8 files changed, 368 insertions(+), 35 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index e45857357ba..e46501929e5 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -17,6 +17,8 @@ import { onLoadInternalEvent, state_name, exception_state_name, + main_state_name, + update_vars_internal, } from "$/utils/context"; import debounce from "$/utils/helpers/debounce"; import throttle from "$/utils/helpers/throttle"; @@ -56,10 +58,10 @@ export const generateUUID = () => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { let r = Math.random() * 16; if (d > 0) { - r = ((d + r) % 16) | 0; + r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { - r = ((d2 + r) % 16) | 0; + r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c == "x" ? r : (r & 0x7) | 0x8).toString(16); @@ -134,7 +136,7 @@ export const isStateful = () => { if (event_queue.length === 0) { return false; } - return event_queue.some((event) => event.name.startsWith("reflex___state")); + return event_queue.some((event) => event.name.startsWith(main_state_name)); }; /** @@ -1034,10 +1036,9 @@ export const useEventLoop = ( if (storage_to_state_map[e.key]) { const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; - const event = ReflexEvent( - `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`, - { vars: vars }, - ); + const event = ReflexEvent(`${state_name}.${update_vars_internal}`, { + vars: vars, + }); addEvents([event], e); } }; @@ -1072,7 +1073,7 @@ export const useEventLoop = ( } // Equivalent to routeChangeStart - runs when navigation begins - const main_state_dispatch = dispatch["reflex___state____state"]; + const main_state_dispatch = dispatch[main_state_name]; if (main_state_dispatch !== undefined) { main_state_dispatch({ is_hydrated_rx_state_: false }); } diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 2abcb6dd533..7eccca3e6ea 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -274,6 +274,20 @@ def context_template( Returns: Rendered context file content as string. """ + # Import state classes to get dynamic names (supports minification) + from reflex.state import ( + FrontendEventExceptionState, + OnLoadInternalState, + State, + UpdateVarsInternalState, + ) + + # Compute dynamic state names that respect minification settings + main_state_name = State.get_name() + on_load_internal = f"{OnLoadInternalState.get_name()}.on_load_internal" + update_vars_internal = f"{UpdateVarsInternalState.get_name()}.update_vars_internal" + exception_state_full = FrontendEventExceptionState.get_full_name() + initial_state = initial_state or {} state_contexts_str = "".join([ f"{format_state_name(state_name)}: createContext(null)," @@ -284,7 +298,11 @@ def context_template( rf""" export const state_name = "{state_name}" -export const exception_state_name = "{constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL}" +export const main_state_name = "{main_state_name}" + +export const update_vars_internal = "{update_vars_internal}" + +export const exception_state_name = "{exception_state_full}" // These events are triggered on initial load and each page navigation. export const onLoadInternalEvent = () => {{ @@ -296,7 +314,7 @@ def context_template( if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{ internal_events.push( ReflexEvent( - '{state_name}.{constants.CompileVars.UPDATE_VARS_INTERNAL}', + '{state_name}.{update_vars_internal}', {{vars: client_storage_vars}}, ), ); @@ -304,7 +322,7 @@ def context_template( // `on_load_internal` triggers the correct on_load event(s) for the current page. // If the page does not define any on_load event, this will just set `is_hydrated = true`. - internal_events.push(ReflexEvent('{state_name}.{constants.CompileVars.ON_LOAD_INTERNAL}')); + internal_events.push(ReflexEvent('{state_name}.{on_load_internal}')); return internal_events; }} @@ -319,6 +337,10 @@ def context_template( else """ export const state_name = undefined +export const main_state_name = undefined + +export const update_vars_internal = undefined + export const exception_state_name = undefined export const onLoadInternalEvent = () => [] diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 873cce69a14..4eefafef412 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -65,18 +65,6 @@ class CompileVars(SimpleNamespace): CONNECT_ERROR = "connectErrors" # The name of the function for converting a dict to an event. TO_EVENT = "ReflexEvent" - # The name of the internal on_load event. - ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" - # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = ( - "reflex___state____update_vars_internal_state.update_vars_internal" - ) - # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" - # The full name of the frontend exception state - FRONTEND_EXCEPTION_STATE_FULL = ( - f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" - ) class PageNames(SimpleNamespace): diff --git a/reflex/reflex.py b/reflex/reflex.py index cb677c3173e..9057f844b20 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -2,6 +2,7 @@ from __future__ import annotations +import operator from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING @@ -842,6 +843,189 @@ def rename(new_name: str): rename_app(new_name, get_config().loglevel) +@cli.command(name="state-tree") +@loglevel_option +@click.option( + "--json", + "output_json", + is_flag=True, + help="Output as JSON.", +) +def state_tree(output_json: bool): + """Print the state tree with state_id's and event handlers with event_id's.""" + from reflex.event import EVENT_ID_MARKER + from reflex.state import BaseState, State, _int_to_minified_name + from reflex.utils import prerequisites + + # Load the user's app to register all state classes + prerequisites.get_app() + + def build_state_tree(state_cls: type[BaseState]) -> dict: + """Recursively build state tree data. + + Args: + state_cls: The state class to build the tree for. + + Returns: + A dictionary containing the state tree data. + """ + state_id = state_cls._state_id + + # Build event handlers list + handlers = [] + for name, handler in state_cls.event_handlers.items(): + event_id = getattr(handler.fn, EVENT_ID_MARKER, None) + handlers.append({ + "name": name, + "event_id": event_id, + "minified_name": ( + _int_to_minified_name(event_id) if event_id is not None else None + ), + }) + handlers.sort(key=operator.itemgetter("name")) + + # Build substates recursively + substates = [ + build_state_tree(substate) + for substate in sorted(state_cls.class_subclasses, key=lambda s: s.__name__) + ] + + return { + "name": state_cls.__name__, + "full_name": state_cls.get_full_name(), + "state_id": state_id, + "minified_name": ( + _int_to_minified_name(state_id) if state_id is not None else None + ), + "event_handlers": handlers, + "substates": substates, + } + + def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): + """Print a state and its children as a tree. + + Args: + state_data: The state data dictionary. + prefix: The prefix for indentation. + is_last: Whether this is the last item in the current level. + """ + state_id = state_data["state_id"] + minified = state_data["minified_name"] + + if state_id is not None: + f'{state_data["name"]} (state_id={state_id} -> "{minified}")' + else: + f"{state_data['name']} (state_id=None)" + + # Calculate new prefix for children + child_prefix = prefix + (" " if is_last else "| ") + + # Print event handlers + handlers = state_data["event_handlers"] + substates = state_data["substates"] + has_substates = len(substates) > 0 + + if handlers: + handler_prefix = child_prefix + ("| " if has_substates else " ") + for i, handler in enumerate(handlers): + is_last_handler = i == len(handlers) - 1 + event_id = handler["event_id"] + if event_id is not None: + _ = ( + handler_prefix, + is_last_handler, + ) # silence unused variable warnings + + # Print substates recursively + for i, substate in enumerate(substates): + is_last_substate = i == len(substates) - 1 + print_state_tree(substate, child_prefix, is_last_substate) + + tree_data = build_state_tree(State) + + if output_json: + pass + else: + print_state_tree(tree_data) + + +@cli.command(name="state-lookup") +@loglevel_option +@click.option( + "--json", + "output_json", + is_flag=True, + help="Output detailed info as JSON.", +) +@click.argument("minified_path") +def state_lookup(output_json: bool, minified_path: str): + """Lookup a state by its minified path (e.g., 'a.bU').""" + from reflex.state import _minified_name_to_int, _state_id_registry + from reflex.utils import prerequisites + + # Load the user's app to register all state classes + prerequisites.get_app() + + # Parse the dotted path + parts = minified_path.split(".") + + # Resolve each part + result_parts = [] + for part in parts: + try: + state_id = _minified_name_to_int(part) + except ValueError as err: + raise SystemExit(1) from err + + state_cls = _state_id_registry.get(state_id) + if state_cls is None: + raise SystemExit(1) + + result_parts.append({ + "minified": part, + "state_id": state_id, + "module": state_cls.__module__, + "class": state_cls.__name__, + "full_name": state_cls.get_full_name(), + }) + + if output_json: + pass + else: + # Simple output: module.ClassName for each part + for _info in result_parts: + pass + + +@cli.command(name="state-next-id") +@loglevel_option +@click.option( + "--after-max", + is_flag=True, + help="Return max(state_id) + 1 instead of first gap.", +) +def state_next_id(after_max: bool): + """Print the next available state_id.""" + from reflex.state import _state_id_registry + from reflex.utils import prerequisites + + # Load the user's app to register all state classes + prerequisites.get_app() + + if not _state_id_registry: + return + + if after_max: + # Return max + 1 + next_id = max(_state_id_registry.keys()) + 1 + else: + # Find first gap starting from 0 + used_ids = set(_state_id_registry.keys()) + next_id = 0 + while next_id in used_ids: + next_id += 1 + + def _convert_reflex_loglevel_to_reflex_cli_loglevel( loglevel: constants.LogLevel, ) -> HostingLogLevel: diff --git a/reflex/state.py b/reflex/state.py index e7109fca4ce..66e939a844b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -106,6 +106,9 @@ # Global registry: state_id -> state class (for duplicate detection) _state_id_registry: dict[int, type[BaseState]] = {} +# Characters used for minified names (valid JS identifiers) +MINIFIED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" + def _int_to_minified_name(state_id: int) -> str: """Convert integer state_id to minified name using base-54 encoding. @@ -123,22 +126,45 @@ def _int_to_minified_name(state_id: int) -> str: msg = f"state_id must be non-negative, got {state_id}" raise ValueError(msg) - # All possible chars for minified state name (valid JS identifiers) - chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" - base = len(chars) + base = len(MINIFIED_NAME_CHARS) if state_id == 0: - return chars[0] + return MINIFIED_NAME_CHARS[0] name = "" num = state_id while num > 0: - name = chars[num % base] + name + name = MINIFIED_NAME_CHARS[num % base] + name num //= base return name +def _minified_name_to_int(name: str) -> int: + """Convert minified name back to integer state_id. + + Args: + name: The minified state name (e.g., 'a', 'bU'). + + Returns: + The integer state_id. + + Raises: + ValueError: If the name contains invalid characters. + """ + base = len(MINIFIED_NAME_CHARS) + + result = 0 + for char in name: + index = MINIFIED_NAME_CHARS.find(char) + if index == -1: + msg = f"Invalid character '{char}' in minified name" + raise ValueError(msg) + result = result * base + index + + return result + + def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Callable: """Protect against directly chaining a background task from another event handler. @@ -2716,7 +2742,7 @@ def wrapper() -> Component: LAST_RELOADED_KEY = "reflex_last_reloaded_on_error" -class FrontendEventExceptionState(State): +class FrontendEventExceptionState(State, state_id=1): """Substate for handling frontend exceptions.""" # If the frontend error message contains any of these strings, automatically reload the page. @@ -2769,7 +2795,7 @@ def handle_frontend_exception( ) -class UpdateVarsInternalState(State): +class UpdateVarsInternalState(State, state_id=2): """Substate for handling internal state var updates.""" async def update_vars_internal(self, vars: dict[str, Any]) -> None: @@ -2793,7 +2819,7 @@ async def update_vars_internal(self, vars: dict[str, Any]) -> None: setattr(var_state, var_name, value) -class OnLoadInternalState(State): +class OnLoadInternalState(State, state_id=3): """Substate for handling on_load event enumeration. This is a separate substate to avoid deserializing the entire state tree for every page navigation. diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 6efd006f1fa..780e57c7d49 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -1253,7 +1253,7 @@ def _dynamic_state_event(name, val, **kwargs): prev_exp_val = "" for exp_index, exp_val in enumerate(exp_vals): on_load_internal = _event( - name=f"{state.get_full_name()}.{constants.CompileVars.ON_LOAD_INTERNAL.rpartition('.')[2]}", + name=f"{state.get_full_name()}.on_load_internal", val=exp_val, ) exp_router_data = { diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 3dcc72372ae..2dca8700f96 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -6,7 +6,16 @@ from reflex.environment import MinifyMode, environment from reflex.event import EVENT_ID_MARKER -from reflex.state import BaseState, _int_to_minified_name, _state_id_registry +from reflex.state import ( + BaseState, + FrontendEventExceptionState, + OnLoadInternalState, + State, + UpdateVarsInternalState, + _int_to_minified_name, + _minified_name_to_int, + _state_id_registry, +) from reflex.utils.exceptions import StateValueError @@ -409,3 +418,106 @@ class ConcreteState(MixinState, BaseState, state_id=601): @rx.event(event_id=0) def own_handler(self): pass + + +class TestMinifiedNameToInt: + """Tests for _minified_name_to_int reverse conversion.""" + + def test_single_char(self): + """Test single character conversion.""" + assert _minified_name_to_int("a") == 0 + assert _minified_name_to_int("b") == 1 + assert _minified_name_to_int("z") == 25 + assert _minified_name_to_int("A") == 26 + assert _minified_name_to_int("Z") == 51 + + def test_roundtrip(self): + """Test that int -> minified -> int roundtrip works.""" + for i in range(1000): + minified = _int_to_minified_name(i) + result = _minified_name_to_int(minified) + assert result == i, f"Roundtrip failed for {i}: {minified} -> {result}" + + def test_invalid_char_raises(self): + """Test that invalid characters raise ValueError.""" + with pytest.raises(ValueError, match="Invalid character"): + _minified_name_to_int("!") + + def test_state_lookup_returns_reflex_state(self): + """Test that looking up state_id=0 returns reflex's internal State.""" + # Re-register State after fixture clears the registry + _state_id_registry[0] = State + + assert 0 in _state_id_registry + state_cls = _state_id_registry[0] + assert state_cls is State + assert state_cls.__module__ == "reflex.state" + assert state_cls.__name__ == "State" + + def test_next_state_id_returns_1(self): + """Test that next available state_id is 1 (0 is used by internal State).""" + # Simulate reflex.state.State using state_id=0 + _state_id_registry[0] = State + + # Find first gap starting from 0 + used_ids = set(_state_id_registry.keys()) + next_id = 0 + while next_id in used_ids: + next_id += 1 + + assert next_id == 1 + + +class TestInternalStateIds: + """Tests for internal state classes having correct state_id values.""" + + def test_state_has_id_0(self): + """Test that the base State class has state_id=0.""" + assert State._state_id == 0 + + def test_frontend_exception_state_has_id_1(self): + """Test that FrontendEventExceptionState has state_id=1.""" + assert FrontendEventExceptionState._state_id == 1 + + def test_update_vars_internal_state_has_id_2(self): + """Test that UpdateVarsInternalState has state_id=2.""" + assert UpdateVarsInternalState._state_id == 2 + + def test_on_load_internal_state_has_id_3(self): + """Test that OnLoadInternalState has state_id=3.""" + assert OnLoadInternalState._state_id == 3 + + def test_internal_states_minified_names(self, reset_minify_mode): + """Test that internal states get correct minified names when enabled.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + + # Clear the lru_cache to get fresh results + State.get_name.cache_clear() + FrontendEventExceptionState.get_name.cache_clear() + UpdateVarsInternalState.get_name.cache_clear() + OnLoadInternalState.get_name.cache_clear() + + # State (id=0) -> "a" + assert State.get_name() == "a" + # FrontendEventExceptionState (id=1) -> "b" + assert FrontendEventExceptionState.get_name() == "b" + # UpdateVarsInternalState (id=2) -> "c" + assert UpdateVarsInternalState.get_name() == "c" + # OnLoadInternalState (id=3) -> "d" + assert OnLoadInternalState.get_name() == "d" + + def test_internal_states_full_names_when_disabled(self, reset_minify_mode): + """Test that internal states use full names when minification is disabled.""" + environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + + # Clear the lru_cache to get fresh results + State.get_name.cache_clear() + FrontendEventExceptionState.get_name.cache_clear() + UpdateVarsInternalState.get_name.cache_clear() + OnLoadInternalState.get_name.cache_clear() + + # Should contain the class name pattern + assert "state" in State.get_name().lower() + assert "frontend" in FrontendEventExceptionState.get_name().lower() + assert "update" in UpdateVarsInternalState.get_name().lower() + assert "on_load" in OnLoadInternalState.get_name().lower() diff --git a/tests/units/test_state.py b/tests/units/test_state.py index ca41ac37abf..ef4a80b30ad 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3094,7 +3094,7 @@ def index(): app=app, event=Event( token=token, - name=f"{state.get_name()}.{CompileVars.ON_LOAD_INTERNAL}", + name=f"{state.get_name()}.{OnLoadInternalState.get_name()}.on_load_internal", router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}}, ), sid="sid", @@ -3147,7 +3147,7 @@ def index(): app=app, event=Event( token=token, - name=f"{state.get_full_name()}.{CompileVars.ON_LOAD_INTERNAL}", + name=f"{state.get_full_name()}.{OnLoadInternalState.get_name()}.on_load_internal", router_data={RouteVar.PATH: "/", RouteVar.ORIGIN: "/", RouteVar.QUERY: {}}, ), sid="sid", From fc4c5e18fe8121edc5a0f39164e76c0adbe5dc39 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 25 Jan 2026 01:02:44 +0100 Subject: [PATCH 07/74] wip cli --- reflex/reflex.py | 64 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 9057f844b20..f321cc59d7b 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -853,14 +853,33 @@ def rename(new_name: str): ) def state_tree(output_json: bool): """Print the state tree with state_id's and event handlers with event_id's.""" + from typing import TypedDict + from reflex.event import EVENT_ID_MARKER from reflex.state import BaseState, State, _int_to_minified_name from reflex.utils import prerequisites + class EventHandlerData(TypedDict): + """Type for event handler data in state tree.""" + + name: str + event_id: int | None + minified_name: str | None + + class StateTreeData(TypedDict): + """Type for state tree data.""" + + name: str + full_name: str + state_id: int | None + minified_name: str | None + event_handlers: list[EventHandlerData] + substates: list[StateTreeData] + # Load the user's app to register all state classes prerequisites.get_app() - def build_state_tree(state_cls: type[BaseState]) -> dict: + def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: """Recursively build state tree data. Args: @@ -901,7 +920,9 @@ def build_state_tree(state_cls: type[BaseState]) -> dict: "substates": substates, } - def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): + def print_state_tree( + state_data: StateTreeData, prefix: str = "", is_last: bool = True + ): """Print a state and its children as a tree. Args: @@ -912,10 +933,14 @@ def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): state_id = state_data["state_id"] minified = state_data["minified_name"] + # Print the state node + connector = "`-- " if is_last else "|-- " if state_id is not None: - f'{state_data["name"]} (state_id={state_id} -> "{minified}")' + console.log( + f'{prefix}{connector}{state_data["name"]} (state_id={state_id} -> "{minified}")' + ) else: - f"{state_data['name']} (state_id=None)" + console.log(f"{prefix}{connector}{state_data['name']} (state_id=None)") # Calculate new prefix for children child_prefix = prefix + (" " if is_last else "| ") @@ -926,15 +951,20 @@ def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): has_substates = len(substates) > 0 if handlers: + console.log(f"{child_prefix}|-- Event Handlers:") handler_prefix = child_prefix + ("| " if has_substates else " ") for i, handler in enumerate(handlers): is_last_handler = i == len(handlers) - 1 + h_connector = "`-- " if is_last_handler else "|-- " event_id = handler["event_id"] if event_id is not None: - _ = ( - handler_prefix, - is_last_handler, - ) # silence unused variable warnings + console.log( + f'{handler_prefix}{h_connector}{handler["name"]} (event_id={event_id} -> "{handler["minified_name"]}")' + ) + else: + console.log( + f"{handler_prefix}{h_connector}{handler['name']} (event_id=None)" + ) # Print substates recursively for i, substate in enumerate(substates): @@ -944,8 +974,11 @@ def print_state_tree(state_data: dict, prefix: str = "", is_last: bool = True): tree_data = build_state_tree(State) if output_json: - pass + import json + + console.log(json.dumps(tree_data, indent=2)) else: + console.log("State Tree") print_state_tree(tree_data) @@ -975,10 +1008,12 @@ def state_lookup(output_json: bool, minified_path: str): try: state_id = _minified_name_to_int(part) except ValueError as err: + console.error(f"Invalid minified name: {part}") raise SystemExit(1) from err state_cls = _state_id_registry.get(state_id) if state_cls is None: + console.error(f"No state registered with state_id={state_id}") raise SystemExit(1) result_parts.append({ @@ -990,11 +1025,13 @@ def state_lookup(output_json: bool, minified_path: str): }) if output_json: - pass + import json + + console.log(json.dumps(result_parts, indent=2)) else: # Simple output: module.ClassName for each part - for _info in result_parts: - pass + for info in result_parts: + console.log(f"{info['module']}.{info['class']}") @cli.command(name="state-next-id") @@ -1013,6 +1050,7 @@ def state_next_id(after_max: bool): prerequisites.get_app() if not _state_id_registry: + console.log("0") return if after_max: @@ -1025,6 +1063,8 @@ def state_next_id(after_max: bool): while next_id in used_ids: next_id += 1 + console.log(str(next_id)) + def _convert_reflex_loglevel_to_reflex_cli_loglevel( loglevel: constants.LogLevel, From 6d13bec56cb93e2b088db11c137f1137588efaef Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 25 Jan 2026 01:11:25 +0100 Subject: [PATCH 08/74] wtf prettier --- reflex/.templates/web/utils/state.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index e46501929e5..ff75874d905 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -58,10 +58,10 @@ export const generateUUID = () => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { let r = Math.random() * 16; if (d > 0) { - r = (d + r) % 16 | 0; + r = ((d + r) % 16) | 0; d = Math.floor(d / 16); } else { - r = (d2 + r) % 16 | 0; + r = ((d2 + r) % 16) | 0; d2 = Math.floor(d2 / 16); } return (c == "x" ? r : (r & 0x7) | 0x8).toString(16); From db97e3af6c4819a86e9fa904d84e80a6cc3e7a38 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 28 Jan 2026 22:06:29 +0100 Subject: [PATCH 09/74] update pyi_hashes --- pyi_hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 185f64eccae..a8f4208952e 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", "reflex/components/react_router/dom.pyi": "3042fa630b7e26a7378fe045d7fbf4af", "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", - "reflex/components/recharts/cartesian.pyi": "cfca4f880239ffaecdf9fb4c7c8caed5", + "reflex/components/recharts/cartesian.pyi": "642e32b6bb3dd709b2faa726833dc701", "reflex/components/recharts/charts.pyi": "013036b9c00ad85a570efdb813c1bc40", "reflex/components/recharts/general.pyi": "d87ff9b85b2a204be01753690df4fb11", "reflex/components/recharts/polar.pyi": "ad24bd37c6acc0bc9bd4ac01af3ffe49", From 6b4c5fa88de5e41170491f58b5eb5a39e44576da Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 28 Jan 2026 23:05:15 +0100 Subject: [PATCH 10/74] unique per sibling --- reflex/reflex.py | 116 ++++++++++++++++++------ reflex/state.py | 39 ++++----- tests/integration/test_minification.py | 10 +-- tests/units/test_minification.py | 117 +++++++++++++++---------- 4 files changed, 176 insertions(+), 106 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index f321cc59d7b..b74b44f3fd1 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -992,36 +992,42 @@ def print_state_tree( ) @click.argument("minified_path") def state_lookup(output_json: bool, minified_path: str): - """Lookup a state by its minified path (e.g., 'a.bU').""" - from reflex.state import _minified_name_to_int, _state_id_registry + """Lookup a state by its minified path (e.g., 'a.bU'). + + Walks the state tree from the root to resolve each segment. + """ + from reflex.state import State from reflex.utils import prerequisites # Load the user's app to register all state classes prerequisites.get_app() - # Parse the dotted path - parts = minified_path.split(".") + try: + State.get_class_substate(minified_path) + except ValueError: + msg = f"No state found for path: {minified_path}" + console.error(msg) + raise ValueError(msg) from None - # Resolve each part + # Build info for each ancestor segment + parts = minified_path.split(".") result_parts = [] - for part in parts: - try: - state_id = _minified_name_to_int(part) - except ValueError as err: - console.error(f"Invalid minified name: {part}") - raise SystemExit(1) from err - - state_cls = _state_id_registry.get(state_id) - if state_cls is None: - console.error(f"No state registered with state_id={state_id}") - raise SystemExit(1) - + current = State + result_parts.append({ + "minified": parts[0], + "state_id": current._state_id, + "module": current.__module__, + "class": current.__name__, + "full_name": current.get_full_name(), + }) + for part in parts[1:]: + current = current.get_class_substate(part) result_parts.append({ "minified": part, - "state_id": state_id, - "module": state_cls.__module__, - "class": state_cls.__name__, - "full_name": state_cls.get_full_name(), + "state_id": current._state_id, + "module": current.__module__, + "class": current.__name__, + "full_name": current.get_full_name(), }) if output_json: @@ -1034,6 +1040,48 @@ def state_lookup(output_json: bool, minified_path: str): console.log(f"{info['module']}.{info['class']}") +def _resolve_parent_state(parent: str): + """Resolve a parent argument to a state class. + + Accepts either a state path (minified like 'a.b' or full name) or a class + name (e.g., 'State', 'MySubState'). Tries path resolution first via + get_class_substate, then falls back to searching by class name. + + Args: + parent: Class name or state path identifying the parent state. + + Returns: + The resolved state class. + + Raises: + SystemExit: If the parent cannot be resolved. + """ + from reflex.state import BaseState, State + + # Try as a state path (minified or full name) + try: + return State.get_class_substate(parent) + except ValueError: + pass + + # Fall back to searching by class name + def _find_by_name(cls: type[BaseState], name: str) -> type[BaseState] | None: + if cls.__name__ == name: + return cls + for child in cls.class_subclasses: + result = _find_by_name(child, name) + if result is not None: + return result + return None + + result = _find_by_name(State, parent) + if result is not None: + return result + + console.error(f"No state found matching '{parent}'") + raise SystemExit(1) + + @cli.command(name="state-next-id") @loglevel_option @click.option( @@ -1041,24 +1089,34 @@ def state_lookup(output_json: bool, minified_path: str): is_flag=True, help="Return max(state_id) + 1 instead of first gap.", ) -def state_next_id(after_max: bool): - """Print the next available state_id.""" - from reflex.state import _state_id_registry +@click.argument("parent") +def state_next_id(after_max: bool, parent: str): + """Print the next available state_id under PARENT. + + PARENT can be a class name (e.g., 'State', 'MySubState') or a + minified path (e.g., 'a', 'a.b'). Auto-determined from input. + """ from reflex.utils import prerequisites # Load the user's app to register all state classes prerequisites.get_app() - if not _state_id_registry: + parent_cls = _resolve_parent_state(parent) + + # Collect sibling state_ids under the parent + used_ids = { + child._state_id + for child in parent_cls.class_subclasses + if child._state_id is not None + } + + if not used_ids: console.log("0") return if after_max: - # Return max + 1 - next_id = max(_state_id_registry.keys()) + 1 + next_id = max(used_ids) + 1 else: - # Find first gap starting from 0 - used_ids = set(_state_id_registry.keys()) next_id = 0 while next_id in used_ids: next_id += 1 diff --git a/reflex/state.py b/reflex/state.py index 39c35b200da..4d0bba77034 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -104,8 +104,6 @@ # For BaseState.get_var_value VAR_TYPE = TypeVar("VAR_TYPE") -# Global registry: state_id -> state class (for duplicate detection) -_state_id_registry: dict[int, type[BaseState]] = {} # Characters used for minified names (valid JS identifiers) MINIFIED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" @@ -607,22 +605,6 @@ def __init_subclass__( # Store state_id as class variable (only for non-mixins) cls._state_id = state_id - # Validate state_id if provided (check for duplicates) - if state_id is not None: - if state_id in _state_id_registry: - existing_cls = _state_id_registry[state_id] - # Allow re-registration if it's the same class (e.g., module reload) - existing_key = f"{existing_cls.__module__}.{existing_cls.__name__}" - new_key = f"{cls.__module__}.{cls.__name__}" - if existing_key != new_key: - msg = ( - f"Duplicate state_id={state_id}. Already used by " - f"'{existing_cls.__module__}.{existing_cls.__name__}', " - f"cannot be reused by '{cls.__module__}.{cls.__name__}'." - ) - raise StateValueError(msg) - _state_id_registry[state_id] = cls - # Handle locally-defined states for pickling. if "" in cls.__qualname__: cls._handle_local_def() @@ -649,6 +631,21 @@ def __init_subclass__( cls.inherited_vars = parent_state.vars cls.inherited_backend_vars = parent_state.backend_vars + # Check for duplicate state_id among siblings. + if state_id is not None: + for sibling in parent_state.class_subclasses: + if sibling._state_id is not None and sibling._state_id == state_id: + # Allow re-registration of the same class (e.g., module reload) + existing_key = f"{sibling.__module__}.{sibling.__name__}" + new_key = f"{cls.__module__}.{cls.__name__}" + if existing_key != new_key: + msg = ( + f"Duplicate state_id={state_id} among siblings of " + f"'{parent_state.__name__}': already used by " + f"'{sibling.__name__}', cannot be reused by '{cls.__name__}'." + ) + raise StateValueError(msg) + # Check if another substate class with the same name has already been defined. if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}: # This should not happen, since we have added module prefix to state names in #3214 @@ -2746,7 +2743,7 @@ def wrapper() -> Component: LAST_RELOADED_KEY = "reflex_last_reloaded_on_error" -class FrontendEventExceptionState(State, state_id=1): +class FrontendEventExceptionState(State, state_id=0): """Substate for handling frontend exceptions.""" # If the frontend error message contains any of these strings, automatically reload the page. @@ -2799,7 +2796,7 @@ def handle_frontend_exception( ) -class UpdateVarsInternalState(State, state_id=2): +class UpdateVarsInternalState(State, state_id=1): """Substate for handling internal state var updates.""" async def update_vars_internal(self, vars: dict[str, Any]) -> None: @@ -2823,7 +2820,7 @@ async def update_vars_internal(self, vars: dict[str, Any]) -> None: setattr(var_state, var_name, value) -class OnLoadInternalState(State, state_id=3): +class OnLoadInternalState(State, state_id=2): """Substate for handling on_load event enumeration. This is a separate substate to avoid deserializing the entire state tree for every page navigation. diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index b60c7f16260..cb01d555b41 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -11,7 +11,7 @@ from selenium.webdriver.common.by import By from reflex.environment import MinifyMode, environment -from reflex.state import _int_to_minified_name, _state_id_registry +from reflex.state import _int_to_minified_name from reflex.testing import AppHarness if TYPE_CHECKING: @@ -98,14 +98,6 @@ def index() -> rx.Component: app.add_page(index) -@pytest.fixture(autouse=True) -def reset_state_registry(): - """Reset the state_id registry before and after each test.""" - _state_id_registry.clear() - yield - _state_id_registry.clear() - - @pytest.fixture def minify_disabled_app( app_harness_env: type[AppHarness], diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 2dca8700f96..49db957ea51 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -14,19 +14,10 @@ UpdateVarsInternalState, _int_to_minified_name, _minified_name_to_int, - _state_id_registry, ) from reflex.utils.exceptions import StateValueError -@pytest.fixture(autouse=True) -def reset_state_registry(): - """Reset the state_id registry before and after each test.""" - _state_id_registry.clear() - yield - _state_id_registry.clear() - - @pytest.fixture def reset_minify_mode(): """Reset minify modes to DISABLED after each test.""" @@ -79,8 +70,6 @@ class TestState(BaseState, state_id=100): pass assert TestState._state_id == 100 - assert 100 in _state_id_registry - assert _state_id_registry[100] is TestState def test_state_without_id(self): """Test that a state can be created without state_id.""" @@ -90,17 +79,45 @@ class TestState(BaseState): assert TestState._state_id is None - def test_duplicate_state_id_raises(self): - """Test that duplicate state_id raises StateValueError.""" + def test_duplicate_state_id_among_siblings_raises(self): + """Test that duplicate state_id among siblings raises StateValueError.""" - class FirstState(BaseState, state_id=200): + class ParentState(BaseState, state_id=200): pass - with pytest.raises(StateValueError, match="Duplicate state_id=200"): + class FirstChild(ParentState, state_id=10): + pass + + with pytest.raises(StateValueError, match="Duplicate state_id=10"): - class SecondState(BaseState, state_id=200): + class SecondChild(ParentState, state_id=10): pass + def test_same_state_id_across_branches_allowed(self): + """Test that the same state_id can be used in different branches.""" + + class Root(BaseState, state_id=210): + pass + + class BranchA(Root, state_id=1): + pass + + class BranchB(Root, state_id=2): + pass + + class LeafA(BranchA, state_id=5): + pass + + class LeafB(BranchB, state_id=5): # same state_id=5, different parent -- OK! + pass + + # Both should succeed - state_id is per-parent (sibling uniqueness) + assert LeafA._state_id == 5 + assert LeafB._state_id == 5 + # But they have different full names + assert LeafA.get_parent_state() is BranchA + assert LeafB.get_parent_state() is BranchB + class TestGetNameMinification: """Tests for get_name with minification modes.""" @@ -443,29 +460,35 @@ def test_invalid_char_raises(self): with pytest.raises(ValueError, match="Invalid character"): _minified_name_to_int("!") - def test_state_lookup_returns_reflex_state(self): - """Test that looking up state_id=0 returns reflex's internal State.""" - # Re-register State after fixture clears the registry - _state_id_registry[0] = State + def test_state_has_state_id_zero(self): + """Test that the root State class has state_id=0.""" + assert State._state_id == 0 + assert State.__module__ == "reflex.state" + assert State.__name__ == "State" - assert 0 in _state_id_registry - state_cls = _state_id_registry[0] - assert state_cls is State - assert state_cls.__module__ == "reflex.state" - assert state_cls.__name__ == "State" + def test_next_sibling_state_id(self): + """Test finding next available state_id among siblings.""" - def test_next_state_id_returns_1(self): - """Test that next available state_id is 1 (0 is used by internal State).""" - # Simulate reflex.state.State using state_id=0 - _state_id_registry[0] = State + class Parent(BaseState, state_id=700): + pass + + class Child0(Parent, state_id=0): + pass + + class Child1(Parent, state_id=1): + pass - # Find first gap starting from 0 - used_ids = set(_state_id_registry.keys()) + # Find first gap starting from 0 among Parent's children + used_ids = { + child._state_id + for child in Parent.class_subclasses + if child._state_id is not None + } next_id = 0 while next_id in used_ids: next_id += 1 - assert next_id == 1 + assert next_id == 2 class TestInternalStateIds: @@ -475,17 +498,17 @@ def test_state_has_id_0(self): """Test that the base State class has state_id=0.""" assert State._state_id == 0 - def test_frontend_exception_state_has_id_1(self): - """Test that FrontendEventExceptionState has state_id=1.""" - assert FrontendEventExceptionState._state_id == 1 + def test_frontend_exception_state_has_id_0(self): + """Test that FrontendEventExceptionState has state_id=0.""" + assert FrontendEventExceptionState._state_id == 0 - def test_update_vars_internal_state_has_id_2(self): - """Test that UpdateVarsInternalState has state_id=2.""" - assert UpdateVarsInternalState._state_id == 2 + def test_update_vars_internal_state_has_id_1(self): + """Test that UpdateVarsInternalState has state_id=1.""" + assert UpdateVarsInternalState._state_id == 1 - def test_on_load_internal_state_has_id_3(self): - """Test that OnLoadInternalState has state_id=3.""" - assert OnLoadInternalState._state_id == 3 + def test_on_load_internal_state_has_id_2(self): + """Test that OnLoadInternalState has state_id=2.""" + assert OnLoadInternalState._state_id == 2 def test_internal_states_minified_names(self, reset_minify_mode): """Test that internal states get correct minified names when enabled.""" @@ -499,12 +522,12 @@ def test_internal_states_minified_names(self, reset_minify_mode): # State (id=0) -> "a" assert State.get_name() == "a" - # FrontendEventExceptionState (id=1) -> "b" - assert FrontendEventExceptionState.get_name() == "b" - # UpdateVarsInternalState (id=2) -> "c" - assert UpdateVarsInternalState.get_name() == "c" - # OnLoadInternalState (id=3) -> "d" - assert OnLoadInternalState.get_name() == "d" + # FrontendEventExceptionState (id=0) -> "a" + assert FrontendEventExceptionState.get_name() == "a" + # UpdateVarsInternalState (id=1) -> "b" + assert UpdateVarsInternalState.get_name() == "b" + # OnLoadInternalState (id=2) -> "c" + assert OnLoadInternalState.get_name() == "c" def test_internal_states_full_names_when_disabled(self, reset_minify_mode): """Test that internal states use full names when minification is disabled.""" From 1647c1efd3ed6ed7ef29757f547456f75cf37ab7 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 11:25:15 +0100 Subject: [PATCH 11/74] fix test state ids --- tests/integration/test_minification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index cb01d555b41..2685225530f 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -122,8 +122,8 @@ def minify_disabled_app( app_name="minify_disabled", app_source=partial( MinificationApp, - root_state_id=0, - sub_state_id=1, + root_state_id=3, + sub_state_id=4, increment_event_id=0, update_message_event_id=0, ), From e375b7e030549b6c90bd316c09723bff8dc9b76c Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 11:48:33 +0100 Subject: [PATCH 12/74] add best-effort mode for event ids --- reflex/environment.py | 24 ++- reflex/state.py | 43 +++-- reflex/utils/format.py | 4 +- tests/integration/test_minification.py | 18 +- tests/units/test_minification.py | 226 +++++++++++++++++++++++-- 5 files changed, 269 insertions(+), 46 deletions(-) diff --git a/reflex/environment.py b/reflex/environment.py index 87fcf4484d7..d0a48208150 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -487,12 +487,22 @@ class PerformanceMode(enum.Enum): @enum.unique -class MinifyMode(enum.Enum): - """Mode for state/event name minification.""" +class StateMinifyMode(enum.Enum): + """Mode for state name minification.""" DISABLED = "disabled" # Never minify names (default) - ENABLED = "enabled" # Minify items that have explicit IDs - ENFORCE = "enforce" # Require all items to have explicit IDs + ENABLED = "enabled" # Minify states that have explicit state_id + ENFORCE = "enforce" # Require all states to have explicit state_id + + +@enum.unique +class EventMinifyMode(enum.Enum): + """Mode for event handler name minification.""" + + DISABLED = "disabled" # Never minify names (default) + ENABLED = "enabled" # Minify handlers that have explicit event_id + ENFORCE = "enforce" # Require all handlers to have explicit event_id + BEST_EFFORT = "best_effort" # Auto-assign IDs to handlers without explicit IDs class ExecutorType(enum.Enum): @@ -698,10 +708,10 @@ class EnvironmentVariables: REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) # State name minification mode: disabled, enabled, or enforce. - REFLEX_MINIFY_STATES: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + REFLEX_MINIFY_STATES: EnvVar[StateMinifyMode] = env_var(StateMinifyMode.DISABLED) - # Event handler name minification mode: disabled, enabled, or enforce. - REFLEX_MINIFY_EVENTS: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + # Event handler name minification mode: disabled, enabled, enforce, or best_effort. + REFLEX_MINIFY_EVENTS: EnvVar[EventMinifyMode] = env_var(EventMinifyMode.DISABLED) # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) diff --git a/reflex/state.py b/reflex/state.py index 4d0bba77034..3bcba70fd9d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -10,6 +10,7 @@ import datetime import functools import inspect +import operator import pickle import re import sys @@ -741,7 +742,9 @@ def __init_subclass__( # Build event_id registry and validate uniqueness within this state class cls._event_id_to_name = {} - missing_event_ids: list[str] = [] + handlers_with_id: list[tuple[str, Callable[..., Any], int]] = [] + handlers_without_id: list[tuple[str, Callable[..., Any]]] = [] + for name, fn in events.items(): event_id = getattr(fn, EVENT_ID_MARKER, None) if event_id is not None: @@ -753,22 +756,38 @@ def __init_subclass__( ) raise StateValueError(msg) cls._event_id_to_name[event_id] = name + handlers_with_id.append((name, fn, event_id)) else: - missing_event_ids.append(name) + handlers_without_id.append((name, fn)) - # In ENFORCE mode, all event handlers must have event_id - from reflex.environment import MinifyMode + from reflex.environment import EventMinifyMode - if ( - environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENFORCE - and missing_event_ids - ): + minify_mode = environment.REFLEX_MINIFY_EVENTS.get() + + # In ENFORCE mode, all event handlers must have event_id + if minify_mode == EventMinifyMode.ENFORCE and handlers_without_id: + missing = [name for name, _ in handlers_without_id] msg = ( f"State '{cls.__name__}' in ENFORCE mode: event handlers " - f"{missing_event_ids} are missing required event_id." + f"{missing} are missing required event_id." ) raise StateValueError(msg) + # In BEST_EFFORT mode, auto-assign IDs to handlers without explicit IDs + if minify_mode == EventMinifyMode.BEST_EFFORT and handlers_without_id: + # Find the highest user-defined event_id + max_explicit_id = max((eid for _, _, eid in handlers_with_id), default=-1) + + # Sort handlers without IDs alphabetically by name for deterministic ordering + handlers_without_id.sort(key=operator.itemgetter(0)) + + # Assign IDs starting from max_explicit_id + 1 + next_id = max_explicit_id + 1 + for name, fn in handlers_without_id: + setattr(fn, EVENT_ID_MARKER, next_id) + cls._event_id_to_name[next_id] = name + next_id += 1 + # Initialize per-class var dependency tracking. cls._var_dependencies = {} cls._init_var_dependency_dicts() @@ -1124,7 +1143,7 @@ def get_name(cls) -> str: Raises: StateValueError: If ENFORCE mode is set and state_id is missing. """ - from reflex.environment import MinifyMode + from reflex.environment import StateMinifyMode from reflex.utils.exceptions import StateValueError module = cls.__module__.replace(".", "___") @@ -1132,14 +1151,14 @@ def get_name(cls) -> str: minify_mode = environment.REFLEX_MINIFY_STATES.get() - if minify_mode == MinifyMode.DISABLED: + if minify_mode == StateMinifyMode.DISABLED: return full_name if cls._state_id is not None: return _int_to_minified_name(cls._state_id) # state_id not set - if minify_mode == MinifyMode.ENFORCE: + if minify_mode == StateMinifyMode.ENFORCE: msg = ( f"State '{cls.__module__}.{cls.__name__}' is missing required state_id. " f"Add state_id parameter: class {cls.__name__}(rx.State, state_id=N)" diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 6e094049103..23249f07320 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -448,7 +448,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: Returns: The state and function name (possibly minified based on REFLEX_MINIFY_EVENTS). """ - from reflex.environment import MinifyMode, environment + from reflex.environment import EventMinifyMode, environment from reflex.event import EVENT_ID_MARKER from reflex.state import State, _int_to_minified_name @@ -470,7 +470,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: # Check for event_id minification mode = environment.REFLEX_MINIFY_EVENTS.get() - if mode != MinifyMode.DISABLED: + if mode != EventMinifyMode.DISABLED: event_id = getattr(handler.fn, EVENT_ID_MARKER, None) if event_id is not None: name = _int_to_minified_name(event_id) diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 2685225530f..1524e1d285a 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -10,7 +10,7 @@ import pytest from selenium.webdriver.common.by import By -from reflex.environment import MinifyMode, environment +from reflex.environment import EventMinifyMode, StateMinifyMode, environment from reflex.state import _int_to_minified_name from reflex.testing import AppHarness @@ -114,8 +114,8 @@ def minify_disabled_app( """ os.environ["REFLEX_MINIFY_STATES"] = "disabled" os.environ["REFLEX_MINIFY_EVENTS"] = "disabled" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) with app_harness_env.create( root=tmp_path_factory.mktemp("minify_disabled"), @@ -133,8 +133,8 @@ def minify_disabled_app( # Cleanup os.environ.pop("REFLEX_MINIFY_STATES", None) os.environ.pop("REFLEX_MINIFY_EVENTS", None) - environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) @pytest.fixture @@ -153,8 +153,8 @@ def minify_enabled_app( """ os.environ["REFLEX_MINIFY_STATES"] = "enabled" os.environ["REFLEX_MINIFY_EVENTS"] = "enabled" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) with app_harness_env.create( root=tmp_path_factory.mktemp("minify_enabled"), @@ -172,8 +172,8 @@ def minify_enabled_app( # Cleanup os.environ.pop("REFLEX_MINIFY_STATES", None) os.environ.pop("REFLEX_MINIFY_EVENTS", None) - environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) @pytest.fixture diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 49db957ea51..b59343ac693 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -4,7 +4,7 @@ import pytest -from reflex.environment import MinifyMode, environment +from reflex.environment import EventMinifyMode, StateMinifyMode, environment from reflex.event import EVENT_ID_MARKER from reflex.state import ( BaseState, @@ -124,7 +124,7 @@ class TestGetNameMinification: def test_disabled_mode_uses_full_name(self, reset_minify_mode): """Test DISABLED mode always uses full name even with state_id.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) class TestState(BaseState, state_id=300): pass @@ -139,7 +139,7 @@ class TestState(BaseState, state_id=300): def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): """Test ENABLED mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) class TestState(BaseState, state_id=301): pass @@ -152,7 +152,7 @@ class TestState(BaseState, state_id=301): def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): """Test ENABLED mode without state_id uses full name.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) class TestState(BaseState): pass @@ -166,7 +166,7 @@ class TestState(BaseState): def test_enforce_mode_without_id_raises(self, reset_minify_mode): """Test ENFORCE mode without state_id raises error during class definition.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) # Error is raised during class definition because get_name() is called # during __init_subclass__ @@ -177,7 +177,7 @@ class TestState(BaseState): def test_enforce_mode_with_id_uses_minified(self, reset_minify_mode): """Test ENFORCE mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) class TestState(BaseState, state_id=302): pass @@ -194,7 +194,7 @@ class TestMixinState: def test_mixin_no_state_id_required(self, reset_minify_mode): """Test that mixin states don't require state_id even in ENFORCE mode.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENFORCE) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) class MixinState(BaseState, mixin=True): pass @@ -292,7 +292,7 @@ def test_disabled_mode_uses_full_name(self, reset_minify_mode): import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) class TestState(BaseState, state_id=500): @rx.event(event_id=0) @@ -310,7 +310,7 @@ def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) class TestState(BaseState, state_id=501): @rx.event(event_id=5) @@ -330,7 +330,7 @@ def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) class TestState(BaseState, state_id=502): @rx.event @@ -348,7 +348,7 @@ def test_enforce_mode_without_event_id_raises(self, reset_minify_mode): """Test ENFORCE mode without event_id raises error during class definition.""" import reflex as rx - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENFORCE) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENFORCE) with pytest.raises(StateValueError, match="missing required event_id"): @@ -362,7 +362,7 @@ def test_enforce_mode_with_event_id_works(self, reset_minify_mode): import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENFORCE) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENFORCE) class TestState(BaseState, state_id=504): @rx.event(event_id=0) @@ -386,7 +386,7 @@ def test_mixin_event_id_preserved(self, reset_minify_mode): import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) class MixinState(BaseState, mixin=True): @rx.event(event_id=10) @@ -422,7 +422,7 @@ def test_mixin_event_id_conflict_raises(self, reset_minify_mode): """Test that conflicting event_ids from mixin and concrete state raises error.""" import reflex as rx - environment.REFLEX_MINIFY_EVENTS.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) class MixinState(BaseState, mixin=True): @rx.event(event_id=0) @@ -512,7 +512,7 @@ def test_on_load_internal_state_has_id_2(self): def test_internal_states_minified_names(self, reset_minify_mode): """Test that internal states get correct minified names when enabled.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.ENABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) # Clear the lru_cache to get fresh results State.get_name.cache_clear() @@ -531,7 +531,7 @@ def test_internal_states_minified_names(self, reset_minify_mode): def test_internal_states_full_names_when_disabled(self, reset_minify_mode): """Test that internal states use full names when minification is disabled.""" - environment.REFLEX_MINIFY_STATES.set(MinifyMode.DISABLED) + environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) # Clear the lru_cache to get fresh results State.get_name.cache_clear() @@ -544,3 +544,197 @@ def test_internal_states_full_names_when_disabled(self, reset_minify_mode): assert "frontend" in FrontendEventExceptionState.get_name().lower() assert "update" in UpdateVarsInternalState.get_name().lower() assert "on_load" in OnLoadInternalState.get_name().lower() + + +class TestBestEffortMode: + """Tests for BEST_EFFORT event minification mode.""" + + def test_best_effort_assigns_ids_to_handlers_without_explicit_id( + self, reset_minify_mode + ): + """Test that BEST_EFFORT mode assigns event_ids to handlers without explicit IDs.""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=700): + @rx.event + def handler_a(self): + pass + + @rx.event + def handler_b(self): + pass + + # Both handlers should have event_ids assigned + assert 0 in TestState._event_id_to_name + assert 1 in TestState._event_id_to_name + # Should be assigned alphabetically + assert TestState._event_id_to_name[0] == "handler_a" + assert TestState._event_id_to_name[1] == "handler_b" + + def test_best_effort_starts_after_highest_explicit_id(self, reset_minify_mode): + """Test that BEST_EFFORT mode starts assigning IDs after the highest explicit ID.""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=701): + @rx.event(event_id=5) + def explicit_handler(self): + pass + + @rx.event(event_id=10) + def another_explicit(self): + pass + + @rx.event + def auto_handler(self): + pass + + # Explicit handlers should keep their IDs + assert TestState._event_id_to_name[5] == "explicit_handler" + assert TestState._event_id_to_name[10] == "another_explicit" + # Auto-assigned handler should start at max_explicit_id + 1 = 11 + assert TestState._event_id_to_name[11] == "auto_handler" + + def test_best_effort_assigns_ids_alphabetically(self, reset_minify_mode): + """Test that BEST_EFFORT mode assigns IDs to handlers alphabetically by name.""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=702): + @rx.event + def zebra_handler(self): + pass + + @rx.event + def alpha_handler(self): + pass + + @rx.event + def middle_handler(self): + pass + + # Should be assigned alphabetically: alpha, middle, zebra + assert TestState._event_id_to_name[0] == "alpha_handler" + assert TestState._event_id_to_name[1] == "middle_handler" + assert TestState._event_id_to_name[2] == "zebra_handler" + + def test_best_effort_with_no_explicit_ids(self, reset_minify_mode): + """Test that BEST_EFFORT mode works with no explicit IDs (starts at 0).""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=703): + @rx.event + def first_handler(self): + pass + + @rx.event + def second_handler(self): + pass + + # Should start at 0 since no explicit IDs + assert TestState._event_id_to_name[0] == "first_handler" + assert TestState._event_id_to_name[1] == "second_handler" + + def test_best_effort_minifies_all_handlers(self, reset_minify_mode): + """Test that BEST_EFFORT mode minifies all handlers in format output.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=704): + @rx.event(event_id=0) + def explicit_handler(self): + pass + + @rx.event + def auto_handler(self): + pass + + TestState.get_name.cache_clear() + + explicit = TestState.event_handlers["explicit_handler"] + auto = TestState.event_handlers["auto_handler"] + + _, explicit_name = get_event_handler_parts(explicit) + _, auto_name = get_event_handler_parts(auto) + + # Both should be minified + assert explicit_name == _int_to_minified_name(0) # 'a' + assert auto_name == _int_to_minified_name(1) # 'b' + + def test_best_effort_skips_gaps_in_explicit_ids(self, reset_minify_mode): + """Test that BEST_EFFORT mode skips gaps in explicit IDs (doesn't fill them).""" + import reflex as rx + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=705): + @rx.event(event_id=0) + def handler_zero(self): + pass + + @rx.event(event_id=5) + def handler_five(self): + pass + + @rx.event(event_id=10) + def handler_ten(self): + pass + + @rx.event + def auto_handler(self): + pass + + # Explicit handlers keep their IDs + assert TestState._event_id_to_name[0] == "handler_zero" + assert TestState._event_id_to_name[5] == "handler_five" + assert TestState._event_id_to_name[10] == "handler_ten" + # Auto-assigned starts at 11, not filling gaps at 1-4 or 6-9 + assert TestState._event_id_to_name[11] == "auto_handler" + # Verify gaps are not filled + assert 1 not in TestState._event_id_to_name + assert 6 not in TestState._event_id_to_name + + def test_best_effort_mixed_explicit_and_auto(self, reset_minify_mode): + """Test BEST_EFFORT with a mix of explicit and auto-assigned handlers.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) + + class TestState(BaseState, state_id=706): + @rx.event(event_id=3) + def explicit_three(self): + pass + + @rx.event + def auto_alpha(self): + pass + + @rx.event + def auto_beta(self): + pass + + TestState.get_name.cache_clear() + + # Check registry + assert TestState._event_id_to_name[3] == "explicit_three" + assert TestState._event_id_to_name[4] == "auto_alpha" # alphabetically first + assert TestState._event_id_to_name[5] == "auto_beta" # alphabetically second + + # Check that all handlers are minified correctly + for name, expected_id in [ + ("explicit_three", 3), + ("auto_alpha", 4), + ("auto_beta", 5), + ]: + handler = TestState.event_handlers[name] + _, minified = get_event_handler_parts(handler) + assert minified == _int_to_minified_name(expected_id) From a94c98bb4eb61f6ef993d54315abd65198151449 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 13:47:16 +0100 Subject: [PATCH 13/74] minify json poc --- reflex/environment.py | 25 - reflex/event.py | 7 - reflex/minify.py | 553 +++++++++++++++ reflex/reflex.py | 330 +++++---- reflex/state.py | 205 +----- reflex/utils/format.py | 28 +- tests/integration/test_minification.py | 158 ++--- tests/units/test_minification.py | 903 ++++++++----------------- 8 files changed, 1181 insertions(+), 1028 deletions(-) create mode 100644 reflex/minify.py diff --git a/reflex/environment.py b/reflex/environment.py index d0a48208150..279fc5f60c1 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -486,25 +486,6 @@ class PerformanceMode(enum.Enum): OFF = "off" -@enum.unique -class StateMinifyMode(enum.Enum): - """Mode for state name minification.""" - - DISABLED = "disabled" # Never minify names (default) - ENABLED = "enabled" # Minify states that have explicit state_id - ENFORCE = "enforce" # Require all states to have explicit state_id - - -@enum.unique -class EventMinifyMode(enum.Enum): - """Mode for event handler name minification.""" - - DISABLED = "disabled" # Never minify names (default) - ENABLED = "enabled" # Minify handlers that have explicit event_id - ENFORCE = "enforce" # Require all handlers to have explicit event_id - BEST_EFFORT = "best_effort" # Auto-assign IDs to handlers without explicit IDs - - class ExecutorType(enum.Enum): """Executor for compiling the frontend.""" @@ -707,12 +688,6 @@ class EnvironmentVariables: # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) - # State name minification mode: disabled, enabled, or enforce. - REFLEX_MINIFY_STATES: EnvVar[StateMinifyMode] = env_var(StateMinifyMode.DISABLED) - - # Event handler name minification mode: disabled, enabled, enforce, or best_effort. - REFLEX_MINIFY_EVENTS: EnvVar[EventMinifyMode] = env_var(EventMinifyMode.DISABLED) - # Whether to use the turbopack bundler. REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False) diff --git a/reflex/event.py b/reflex/event.py index 8e58e8d2162..825370cfa2f 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -2338,7 +2338,6 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, - event_id: int | None = None, ) -> Callable[ [Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse] ]: ... @@ -2354,7 +2353,6 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, - event_id: int | None = None, ) -> EventCallback[Unpack[P]]: ... def __new__( @@ -2367,7 +2365,6 @@ def __new__( throttle: int | None = None, debounce: int | None = None, temporal: bool | None = None, - event_id: int | None = None, ) -> ( EventCallback[Unpack[P]] | Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]] @@ -2382,7 +2379,6 @@ def __new__( throttle: Throttle the event handler to limit calls (in milliseconds). debounce: Debounce the event handler to delay calls (in milliseconds). temporal: Whether the event should be dropped when the backend is down. - event_id: Optional integer ID for deterministic minified event names. Raises: TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402 @@ -2470,9 +2466,6 @@ def wrapper( event_actions = _build_event_actions() if event_actions: setattr(func, EVENT_ACTIONS_MARKER, event_actions) - # Store event_id on the function for minification - if event_id is not None: - setattr(func, EVENT_ID_MARKER, event_id) return func # pyright: ignore [reportReturnType] if func is not None: diff --git a/reflex/minify.py b/reflex/minify.py new file mode 100644 index 00000000000..cc9a9acdc00 --- /dev/null +++ b/reflex/minify.py @@ -0,0 +1,553 @@ +"""Minification configuration for state and event names. + +This module provides centralized ID management for minifying state and event handler +names. The configuration is stored in a `minify.json` file at the project root. +""" + +from __future__ import annotations + +import functools +import json +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict + +if TYPE_CHECKING: + from reflex.state import BaseState + +# File name for the minify configuration +MINIFY_JSON = "minify.json" + +# Current schema version +SCHEMA_VERSION = 1 + + +class MinifyConfig(TypedDict): + """Schema for minify.json file. + + Version 2 format: + - states: dict mapping state_path -> minified_name (string) + - events: dict mapping state_path -> {handler_name -> minified_name} + """ + + version: int + states: dict[str, str] # state_path -> minified_name + events: dict[str, dict[str, str]] # state_path -> {handler_name -> minified_name} + + +def _get_minify_json_path() -> Path: + """Get the path to the minify.json file. + + Returns: + Path to minify.json in the current working directory. + """ + return Path.cwd() / MINIFY_JSON + + +def _load_minify_config_uncached() -> MinifyConfig | None: + """Load minify configuration from minify.json. + + Returns: + The parsed configuration, or None if file doesn't exist. + + Raises: + ValueError: If the file exists but has an invalid format. + """ + path = _get_minify_json_path() + if not path.exists(): + return None + + try: + with path.open(encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError as e: + msg = f"Invalid JSON in {MINIFY_JSON}: {e}" + raise ValueError(msg) from e + + # Validate schema version + version = data.get("version") + if version != SCHEMA_VERSION: + msg = ( + f"Unsupported {MINIFY_JSON} version: {version}. Expected {SCHEMA_VERSION}." + ) + raise ValueError(msg) + + # Validate required keys + if "states" not in data or not isinstance(data["states"], dict): + msg = f"Invalid {MINIFY_JSON}: 'states' must be a dictionary." + raise ValueError(msg) + if "events" not in data or not isinstance(data["events"], dict): + msg = f"Invalid {MINIFY_JSON}: 'events' must be a dictionary." + raise ValueError(msg) + + # Validate states: all values must be strings + for key, value in data["states"].items(): + if not isinstance(value, str): + msg = f"Invalid {MINIFY_JSON}: state '{key}' has non-string id: {value}" + raise ValueError(msg) + + # Validate events: must be dict of dicts with string values + for state_path, handlers in data["events"].items(): + if not isinstance(handlers, dict): + msg = f"Invalid {MINIFY_JSON}: events for '{state_path}' must be a dictionary." + raise ValueError(msg) + for handler_name, event_id in handlers.items(): + if not isinstance(event_id, str): + msg = f"Invalid {MINIFY_JSON}: event '{state_path}.{handler_name}' has non-string id: {event_id}" + raise ValueError(msg) + + return MinifyConfig( + version=data["version"], + states=data["states"], + events=data["events"], + ) + + +@functools.cache +def get_minify_config() -> MinifyConfig | None: + """Get the minify configuration, cached. + + This function is cached so the file is only read once per process. + + Returns: + The parsed configuration, or None if file doesn't exist. + """ + return _load_minify_config_uncached() + + +def is_minify_enabled() -> bool: + """Check if minification is enabled. + + Returns: + True if minify.json exists and is valid. + """ + return get_minify_config() is not None + + +def get_state_id(state_full_path: str) -> str | None: + """Get the minified ID for a state. + + Args: + state_full_path: The full path to the state (e.g., "myapp.state.AppState.UserState"). + + Returns: + The minified state name (e.g., "a", "ba") if configured, None otherwise. + """ + config = get_minify_config() + if config is None: + return None + return config["states"].get(state_full_path) + + +def get_event_id(state_full_path: str, handler_name: str) -> str | None: + """Get the minified ID for an event handler. + + Args: + state_full_path: The full path to the state. + handler_name: The name of the event handler. + + Returns: + The minified event name (e.g., "a", "ba") if configured, None otherwise. + """ + config = get_minify_config() + if config is None: + return None + state_events = config["events"].get(state_full_path) + if state_events is None: + return None + return state_events.get(handler_name) + + +def save_minify_config(config: MinifyConfig) -> None: + """Save minify configuration to minify.json. + + Args: + config: The configuration to save. + """ + path = _get_minify_json_path() + with path.open("w", encoding="utf-8") as f: + json.dump(config, f, indent=2, sort_keys=True) + f.write("\n") + + +def clear_config_cache() -> None: + """Clear the cached configuration. + + This should be called after modifying minify.json programmatically. + """ + get_minify_config.cache_clear() + + +# Base-54 encoding for minified names +# Using letters (a-z, A-Z) plus $ and _ which are valid JS identifier chars +_MINIFY_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" +_MINIFY_BASE = len(_MINIFY_CHARS) # 54 + + +def int_to_minified_name(id_: int) -> str: + """Convert integer ID to minified name using base-54 encoding. + + Args: + id_: The integer ID to convert. + + Returns: + A minified string representation. + + Raises: + ValueError: If id_ is negative. + """ + if id_ < 0: + msg = f"ID must be non-negative, got {id_}" + raise ValueError(msg) + + # Special case: 0 maps to 'a' + if id_ == 0: + return _MINIFY_CHARS[0] + + result = [] + num = id_ + while num > 0: + result.append(_MINIFY_CHARS[num % _MINIFY_BASE]) + num //= _MINIFY_BASE + + return "".join(reversed(result)) + + +def minified_name_to_int(name: str) -> int: + """Convert minified name back to integer ID. + + Args: + name: The minified string to convert. + + Returns: + The integer ID. + + Raises: + ValueError: If name contains invalid characters. + """ + result = 0 + for char in name: + idx = _MINIFY_CHARS.find(char) + if idx == -1: + msg = f"Invalid character in minified name: '{char}'" + raise ValueError(msg) + result = result * _MINIFY_BASE + idx + return result + + +def get_state_full_path(state_cls: type[BaseState]) -> str: + """Get the full path for a state class suitable for minify.json. + + This returns the module path plus class name hierarchy, which uniquely + identifies a state class. + + Args: + state_cls: The state class. + + Returns: + The full path string (e.g., "myapp.state.AppState.UserState"). + """ + # Build the path from module + class hierarchy + # Use __original_module__ if available (for dynamic states that get moved) + module = getattr(state_cls, "__original_module__", None) or state_cls.__module__ + parts = [module] + + # Get the class hierarchy from root to this class + class_hierarchy = [] + current: type[BaseState] | None = state_cls + while current is not None: + class_hierarchy.append(current.__name__) + current = current.get_parent_state() # type: ignore[union-attr] + + # Reverse to get root-to-leaf order + class_hierarchy.reverse() + + # Combine module and class hierarchy + parts.extend(class_hierarchy) + return ".".join(parts) + + +def collect_all_states( + root_state: type[BaseState], +) -> list[type[BaseState]]: + """Recursively collect all state classes starting from root. + + Args: + root_state: The root state class to start from. + + Returns: + List of all state classes in depth-first order. + """ + result = [root_state] + for substate in sorted(root_state.class_subclasses, key=lambda s: s.__name__): + result.extend(collect_all_states(substate)) + return result + + +def generate_minify_config(root_state: type[BaseState]) -> MinifyConfig: + """Generate a complete minify configuration for all states and events. + + Assigns minified names starting from 'a' for each scope (siblings get unique names), + sorted alphabetically by name for determinism. + + Args: + root_state: The root state class. + + Returns: + A complete MinifyConfig. + """ + states: dict[str, str] = {} + events: dict[str, dict[str, str]] = {} + + def process_state( + state_cls: type[BaseState], + sibling_counter: dict[type[BaseState] | None, int], + ) -> None: + """Process a state and its children recursively. + + Args: + state_cls: The state class to process. + sibling_counter: Counter for assigning sibling-unique IDs. + """ + parent = state_cls.get_parent_state() + + # Assign state ID (unique among siblings) + if parent not in sibling_counter: + sibling_counter[parent] = 0 + state_id = sibling_counter[parent] + sibling_counter[parent] += 1 + + # Store state minified name + state_path = get_state_full_path(state_cls) + states[state_path] = int_to_minified_name(state_id) + + # Assign event IDs for this state's handlers (sorted alphabetically) + handler_names = sorted(state_cls.event_handlers.keys()) + state_events: dict[str, str] = {} + for event_id, handler_name in enumerate(handler_names): + state_events[handler_name] = int_to_minified_name(event_id) + if state_events: + events[state_path] = state_events + + # Process children (sorted alphabetically) + children = sorted(state_cls.class_subclasses, key=lambda s: s.__name__) + for child in children: + process_state(child, sibling_counter) + + # Start processing from root + sibling_counter: dict[type[BaseState] | None, int] = {} + process_state(root_state, sibling_counter) + + return MinifyConfig( + version=SCHEMA_VERSION, + states=states, + events=events, + ) + + +def validate_minify_config( + config: MinifyConfig, + root_state: type[BaseState], +) -> tuple[list[str], list[str], list[str]]: + """Validate a minify configuration against the current state tree. + + Args: + config: The configuration to validate. + root_state: The root state class. + + Returns: + A tuple of (errors, warnings, missing_entries): + - errors: Critical issues (duplicate IDs, etc.) + - warnings: Non-critical issues (orphaned entries) + - missing_entries: States/events in code but not in config + """ + errors: list[str] = [] + + all_states = collect_all_states(root_state) + + # Check for duplicate state IDs among siblings + # Group states by parent path and check for duplicate minified names + parent_to_state_ids: dict[str | None, dict[str, list[str]]] = {} + for state_path, minified_name in config["states"].items(): + # Get parent path + parts = state_path.rsplit(".", 1) + parent_path = parts[0] if len(parts) > 1 else None + + if parent_path not in parent_to_state_ids: + parent_to_state_ids[parent_path] = {} + if minified_name not in parent_to_state_ids[parent_path]: + parent_to_state_ids[parent_path][minified_name] = [] + parent_to_state_ids[parent_path][minified_name].append(state_path) + + for parent_path, id_to_states in parent_to_state_ids.items(): + for minified_name, state_paths in id_to_states.items(): + if len(state_paths) > 1: + errors.append( + f"Duplicate state_id='{minified_name}' under '{parent_path or 'root'}': " + f"{state_paths}" + ) + + # Check for duplicate event IDs within same state + for state_path, state_events in config["events"].items(): + id_to_handlers: dict[str, list[str]] = {} + for handler_name, minified_name in state_events.items(): + if minified_name not in id_to_handlers: + id_to_handlers[minified_name] = [] + id_to_handlers[minified_name].append(handler_name) + + for minified_name, handler_names in id_to_handlers.items(): + if len(handler_names) > 1: + errors.append( + f"Duplicate event_id='{minified_name}' in '{state_path}': {handler_names}" + ) + + # Check for missing states (in code but not in config) + code_state_paths = {get_state_full_path(s) for s in all_states} + missing: list[str] = [ + f"state:{state_path}" + for state_path in code_state_paths + if state_path not in config["states"] + ] + + # Check for missing events (in code but not in config) + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + state_events = config["events"].get(state_path, {}) + missing.extend( + f"event:{state_path}.{handler_name}" + for handler_name in state_cls.event_handlers + if handler_name not in state_events + ) + + # Check for orphaned entries (in config but not in code) + warnings: list[str] = [ + f"Orphaned state in config: {state_path}" + for state_path in config["states"] + if state_path not in code_state_paths + ] + + code_event_keys: dict[str, set[str]] = {} + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + code_event_keys[state_path] = set(state_cls.event_handlers.keys()) + + for state_path, state_events in config["events"].items(): + if state_path not in code_event_keys: + warnings.append(f"Orphaned events for state: {state_path}") + else: + warnings.extend( + f"Orphaned event in config: {state_path}.{handler_name}" + for handler_name in state_events + if handler_name not in code_event_keys[state_path] + ) + + return errors, warnings, missing + + +def sync_minify_config( + existing_config: MinifyConfig, + root_state: type[BaseState], + reassign_deleted: bool = False, + prune: bool = False, +) -> MinifyConfig: + """Synchronize minify configuration with the current state tree. + + Args: + existing_config: The existing configuration to update. + root_state: The root state class. + reassign_deleted: If True, reassign IDs that are no longer in use. + prune: If True, remove entries for states/events that no longer exist. + + Returns: + The updated configuration. + """ + all_states = collect_all_states(root_state) + code_state_paths = {get_state_full_path(s) for s in all_states} + + # Build current event keys by state + code_events_by_state: dict[str, set[str]] = {} + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + code_events_by_state[state_path] = set(state_cls.event_handlers.keys()) + + new_states = dict(existing_config["states"]) + new_events: dict[str, dict[str, str]] = { + k: dict(v) for k, v in existing_config["events"].items() + } + + # Prune orphaned entries if requested + if prune: + new_states = {k: v for k, v in new_states.items() if k in code_state_paths} + new_events = { + state_path: { + h: eid + for h, eid in handlers.items() + if h in code_events_by_state.get(state_path, set()) + } + for state_path, handlers in new_events.items() + if state_path in code_state_paths + } + # Remove empty event dicts + new_events = {k: v for k, v in new_events.items() if v} + + # Find states that need IDs assigned + # Group by parent for sibling-unique assignment + parent_to_children: dict[str | None, list[str]] = {} + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + if state_path not in new_states: + parent = state_cls.get_parent_state() + parent_path = get_state_full_path(parent) if parent else None + if parent_path not in parent_to_children: + parent_to_children[parent_path] = [] + parent_to_children[parent_path].append(state_path) + + # Assign new state IDs + for parent_path, children in parent_to_children.items(): + # Get existing IDs for this parent's children (convert to ints for finding max) + existing_ids: set[int] = set() + for state_path, minified_name in new_states.items(): + parts = state_path.rsplit(".", 1) + sp_parent = parts[0] if len(parts) > 1 else None + # Compare parent paths correctly + if parent_path is None: + if sp_parent is None or "." not in state_path: + existing_ids.add(minified_name_to_int(minified_name)) + elif sp_parent == parent_path: + existing_ids.add(minified_name_to_int(minified_name)) + + # Assign IDs starting from max + 1 (or 0 if reassign_deleted and gaps exist) + next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1) + + for state_path in sorted(children): + while next_id in existing_ids: + next_id += 1 + new_states[state_path] = int_to_minified_name(next_id) + existing_ids.add(next_id) + next_id += 1 + + # Find events that need IDs assigned + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + state_events = new_events.get(state_path, {}) + new_handlers = [h for h in state_cls.event_handlers if h not in state_events] + + if new_handlers: + # Get existing IDs for this state's events + existing_ids = {minified_name_to_int(eid) for eid in state_events.values()} + + next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1) + + for handler_name in sorted(new_handlers): + while next_id in existing_ids: + next_id += 1 + state_events[handler_name] = int_to_minified_name(next_id) + existing_ids.add(next_id) + next_id += 1 + + new_events[state_path] = state_events + + return MinifyConfig( + version=SCHEMA_VERSION, + states=new_states, + events=new_events, + ) diff --git a/reflex/reflex.py b/reflex/reflex.py index b74b44f3fd1..f68865c2def 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -2,7 +2,6 @@ from __future__ import annotations -import operator from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING @@ -843,7 +842,168 @@ def rename(new_name: str): rename_app(new_name, get_config().loglevel) -@cli.command(name="state-tree") +# Minify command group +@cli.group() +def minify(): + """Manage state and event name minification.""" + + +@minify.command(name="init") +@loglevel_option +def minify_init(): + """Initialize minify.json with IDs for all states and events. + + This command scans the codebase and generates a minify.json file + with unique IDs for all states and event handlers. + """ + from reflex.minify import ( + MINIFY_JSON, + _get_minify_json_path, + generate_minify_config, + save_minify_config, + ) + from reflex.state import State + from reflex.utils import prerequisites + + path = _get_minify_json_path() + if path.exists(): + console.error( + f"{MINIFY_JSON} already exists. Use 'reflex minify sync' to update " + "or delete the file to reinitialize." + ) + raise SystemExit(1) + + # Load the user's app to register all state classes + prerequisites.get_app() + + # Generate the configuration + config = generate_minify_config(State) + save_minify_config(config) + + num_states = len(config["states"]) + num_events = len(config["events"]) + console.log( + f"Created {MINIFY_JSON} with {num_states} states and {num_events} events." + ) + + +@minify.command(name="sync") +@loglevel_option +@click.option( + "--reassign-deleted", + is_flag=True, + help="Reassign IDs that are no longer in use (potentially breaking for existing clients).", +) +@click.option( + "--prune", + is_flag=True, + help="Remove entries for states/events that no longer exist in code.", +) +def minify_sync(reassign_deleted: bool, prune: bool): + """Synchronize minify.json with the current codebase. + + Adds new states and events, optionally removes orphaned entries. + """ + from reflex.minify import ( + MINIFY_JSON, + _get_minify_json_path, + _load_minify_config_uncached, + save_minify_config, + sync_minify_config, + ) + from reflex.state import State + from reflex.utils import prerequisites + + path = _get_minify_json_path() + if not path.exists(): + console.error( + f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." + ) + raise SystemExit(1) + + # Load the user's app to register all state classes + prerequisites.get_app() + + # Load existing config + existing_config = _load_minify_config_uncached() + if existing_config is None: + console.error(f"Failed to load {MINIFY_JSON}.") + raise SystemExit(1) + + old_states = len(existing_config["states"]) + old_events = len(existing_config["events"]) + + # Sync the configuration + new_config = sync_minify_config( + existing_config, State, reassign_deleted=reassign_deleted, prune=prune + ) + save_minify_config(new_config) + + new_states = len(new_config["states"]) + new_events = len(new_config["events"]) + + console.log(f"Updated {MINIFY_JSON}:") + console.log(f" States: {old_states} -> {new_states}") + console.log(f" Events: {old_events} -> {new_events}") + + +@minify.command(name="validate") +@loglevel_option +def minify_validate(): + """Validate minify.json against the current codebase. + + Checks for duplicate IDs, missing entries, and orphaned entries. + """ + from reflex.minify import ( + MINIFY_JSON, + _get_minify_json_path, + _load_minify_config_uncached, + validate_minify_config, + ) + from reflex.state import State + from reflex.utils import prerequisites + + path = _get_minify_json_path() + if not path.exists(): + console.error( + f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." + ) + raise SystemExit(1) + + # Load the user's app to register all state classes + prerequisites.get_app() + + # Load existing config + config = _load_minify_config_uncached() + if config is None: + console.error(f"Failed to load {MINIFY_JSON}.") + raise SystemExit(1) + + # Validate + errors, warnings, missing = validate_minify_config(config, State) + + if errors: + console.error("Errors found:") + for error in errors: + console.error(f" - {error}") + + if warnings: + console.warn("Warnings:") + for warning in warnings: + console.warn(f" - {warning}") + + if missing: + console.info("Missing entries (in code but not in config):") + for entry in missing: + console.info(f" - {entry}") + + if not errors and not warnings and not missing: + console.log(f"{MINIFY_JSON} is valid and up-to-date.") + elif errors: + raise SystemExit(1) + + +@minify.command(name="list") @loglevel_option @click.option( "--json", @@ -851,34 +1011,39 @@ def rename(new_name: str): is_flag=True, help="Output as JSON.", ) -def state_tree(output_json: bool): - """Print the state tree with state_id's and event handlers with event_id's.""" +def minify_list(output_json: bool): + """Print the state tree with IDs and minified names.""" from typing import TypedDict - from reflex.event import EVENT_ID_MARKER - from reflex.state import BaseState, State, _int_to_minified_name + from reflex.minify import ( + get_event_id, + get_state_full_path, + get_state_id, + is_minify_enabled, + ) + from reflex.state import BaseState, State from reflex.utils import prerequisites class EventHandlerData(TypedDict): """Type for event handler data in state tree.""" name: str - event_id: int | None - minified_name: str | None + event_id: str | None # The minified name (e.g., "a", "ba") or None class StateTreeData(TypedDict): """Type for state tree data.""" name: str - full_name: str - state_id: int | None - minified_name: str | None + full_path: str + state_id: str | None # The minified name (e.g., "a", "ba") or None event_handlers: list[EventHandlerData] substates: list[StateTreeData] # Load the user's app to register all state classes prerequisites.get_app() + minify_enabled = is_minify_enabled() + def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: """Recursively build state tree data. @@ -888,20 +1053,21 @@ def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: Returns: A dictionary containing the state tree data. """ - state_id = state_cls._state_id + state_path = get_state_full_path(state_cls) + # state_id is now the minified name directly (a string like "a", "ba") + state_id = get_state_id(state_path) if minify_enabled else None # Build event handlers list handlers = [] - for name, handler in state_cls.event_handlers.items(): - event_id = getattr(handler.fn, EVENT_ID_MARKER, None) + for handler_name in sorted(state_cls.event_handlers.keys()): + # event_id is now the minified name directly (a string like "a", "ba") + event_id = ( + get_event_id(state_path, handler_name) if minify_enabled else None + ) handlers.append({ - "name": name, + "name": handler_name, "event_id": event_id, - "minified_name": ( - _int_to_minified_name(event_id) if event_id is not None else None - ), }) - handlers.sort(key=operator.itemgetter("name")) # Build substates recursively substates = [ @@ -911,11 +1077,8 @@ def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: return { "name": state_cls.__name__, - "full_name": state_cls.get_full_name(), + "full_path": state_path, "state_id": state_id, - "minified_name": ( - _int_to_minified_name(state_id) if state_id is not None else None - ), "event_handlers": handlers, "substates": substates, } @@ -930,17 +1093,15 @@ def print_state_tree( prefix: The prefix for indentation. is_last: Whether this is the last item in the current level. """ + # state_id is now the minified name directly (e.g., "a", "ba") state_id = state_data["state_id"] - minified = state_data["minified_name"] # Print the state node connector = "`-- " if is_last else "|-- " if state_id is not None: - console.log( - f'{prefix}{connector}{state_data["name"]} (state_id={state_id} -> "{minified}")' - ) + console.log(f'{prefix}{connector}{state_data["name"]} -> "{state_id}"') else: - console.log(f"{prefix}{connector}{state_data['name']} (state_id=None)") + console.log(f"{prefix}{connector}{state_data['name']}") # Calculate new prefix for children child_prefix = prefix + (" " if is_last else "| ") @@ -956,15 +1117,14 @@ def print_state_tree( for i, handler in enumerate(handlers): is_last_handler = i == len(handlers) - 1 h_connector = "`-- " if is_last_handler else "|-- " + # event_id is now the minified name directly event_id = handler["event_id"] if event_id is not None: console.log( - f'{handler_prefix}{h_connector}{handler["name"]} (event_id={event_id} -> "{handler["minified_name"]}")' + f'{handler_prefix}{h_connector}{handler["name"]} -> "{event_id}"' ) else: - console.log( - f"{handler_prefix}{h_connector}{handler['name']} (event_id=None)" - ) + console.log(f"{handler_prefix}{h_connector}{handler['name']}") # Print substates recursively for i, substate in enumerate(substates): @@ -978,11 +1138,14 @@ def print_state_tree( console.log(json.dumps(tree_data, indent=2)) else: - console.log("State Tree") + if minify_enabled: + console.log("State Tree (minify.json loaded)") + else: + console.log("State Tree (no minify.json)") print_state_tree(tree_data) -@cli.command(name="state-lookup") +@minify.command(name="lookup") @loglevel_option @click.option( "--json", @@ -991,11 +1154,12 @@ def print_state_tree( help="Output detailed info as JSON.", ) @click.argument("minified_path") -def state_lookup(output_json: bool, minified_path: str): +def minify_lookup(output_json: bool, minified_path: str): """Lookup a state by its minified path (e.g., 'a.bU'). Walks the state tree from the root to resolve each segment. """ + from reflex.minify import get_state_full_path, get_state_id from reflex.state import State from reflex.utils import prerequisites @@ -1013,21 +1177,25 @@ def state_lookup(output_json: bool, minified_path: str): parts = minified_path.split(".") result_parts = [] current = State + state_path = get_state_full_path(current) + state_id = get_state_id(state_path) result_parts.append({ "minified": parts[0], - "state_id": current._state_id, + "state_id": state_id, "module": current.__module__, "class": current.__name__, - "full_name": current.get_full_name(), + "full_path": state_path, }) for part in parts[1:]: current = current.get_class_substate(part) + state_path = get_state_full_path(current) + state_id = get_state_id(state_path) result_parts.append({ "minified": part, - "state_id": current._state_id, + "state_id": state_id, "module": current.__module__, "class": current.__name__, - "full_name": current.get_full_name(), + "full_path": state_path, }) if output_json: @@ -1040,90 +1208,6 @@ def state_lookup(output_json: bool, minified_path: str): console.log(f"{info['module']}.{info['class']}") -def _resolve_parent_state(parent: str): - """Resolve a parent argument to a state class. - - Accepts either a state path (minified like 'a.b' or full name) or a class - name (e.g., 'State', 'MySubState'). Tries path resolution first via - get_class_substate, then falls back to searching by class name. - - Args: - parent: Class name or state path identifying the parent state. - - Returns: - The resolved state class. - - Raises: - SystemExit: If the parent cannot be resolved. - """ - from reflex.state import BaseState, State - - # Try as a state path (minified or full name) - try: - return State.get_class_substate(parent) - except ValueError: - pass - - # Fall back to searching by class name - def _find_by_name(cls: type[BaseState], name: str) -> type[BaseState] | None: - if cls.__name__ == name: - return cls - for child in cls.class_subclasses: - result = _find_by_name(child, name) - if result is not None: - return result - return None - - result = _find_by_name(State, parent) - if result is not None: - return result - - console.error(f"No state found matching '{parent}'") - raise SystemExit(1) - - -@cli.command(name="state-next-id") -@loglevel_option -@click.option( - "--after-max", - is_flag=True, - help="Return max(state_id) + 1 instead of first gap.", -) -@click.argument("parent") -def state_next_id(after_max: bool, parent: str): - """Print the next available state_id under PARENT. - - PARENT can be a class name (e.g., 'State', 'MySubState') or a - minified path (e.g., 'a', 'a.b'). Auto-determined from input. - """ - from reflex.utils import prerequisites - - # Load the user's app to register all state classes - prerequisites.get_app() - - parent_cls = _resolve_parent_state(parent) - - # Collect sibling state_ids under the parent - used_ids = { - child._state_id - for child in parent_cls.class_subclasses - if child._state_id is not None - } - - if not used_ids: - console.log("0") - return - - if after_max: - next_id = max(used_ids) + 1 - else: - next_id = 0 - while next_id in used_ids: - next_id += 1 - - console.log(str(next_id)) - - def _convert_reflex_loglevel_to_reflex_cli_loglevel( loglevel: constants.LogLevel, ) -> HostingLogLevel: diff --git a/reflex/state.py b/reflex/state.py index 3bcba70fd9d..95265142727 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -10,7 +10,6 @@ import datetime import functools import inspect -import operator import pickle import re import sys @@ -44,7 +43,6 @@ from reflex.event import ( BACKGROUND_TASK_MARKER, EVENT_ACTIONS_MARKER, - EVENT_ID_MARKER, Event, EventHandler, EventSpec, @@ -106,65 +104,6 @@ VAR_TYPE = TypeVar("VAR_TYPE") -# Characters used for minified names (valid JS identifiers) -MINIFIED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_" - - -def _int_to_minified_name(state_id: int) -> str: - """Convert integer state_id to minified name using base-54 encoding. - - Args: - state_id: The integer state ID to convert. - - Returns: - The minified state name (e.g., 0->'a', 1->'b', 54->'ba'). - - Raises: - ValueError: If state_id is negative. - """ - if state_id < 0: - msg = f"state_id must be non-negative, got {state_id}" - raise ValueError(msg) - - base = len(MINIFIED_NAME_CHARS) - - if state_id == 0: - return MINIFIED_NAME_CHARS[0] - - name = "" - num = state_id - while num > 0: - name = MINIFIED_NAME_CHARS[num % base] + name - num //= base - - return name - - -def _minified_name_to_int(name: str) -> int: - """Convert minified name back to integer state_id. - - Args: - name: The minified state name (e.g., 'a', 'bU'). - - Returns: - The integer state_id. - - Raises: - ValueError: If the name contains invalid characters. - """ - base = len(MINIFIED_NAME_CHARS) - - result = 0 - for char in name: - index = MINIFIED_NAME_CHARS.find(char) - if index == -1: - msg = f"Invalid character '{char}' in minified name" - raise ValueError(msg) - result = result * base + index - - return result - - def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Callable: """Protect against directly chaining a background task from another event handler. @@ -453,11 +392,10 @@ class BaseState(EvenMoreBasicBaseState): # Set of states which might need to be recomputed if vars in this state change. _potentially_dirty_states: ClassVar[set[str]] = set() - # The explicit state ID for minification (None = use full name). - _state_id: ClassVar[int | None] = None - # Per-class registry mapping event_id -> event handler name for minification. - _event_id_to_name: ClassVar[builtins.dict[int, str]] = {} + # Populated from minify.json at class creation time. + # Maps minified event ID (e.g., "a") to original handler name (e.g., "increment"). + _event_id_to_name: ClassVar[builtins.dict[str, str]] = {} # The parent state. parent_state: BaseState | None = field(default=None, is_var=False) @@ -576,36 +514,24 @@ def _validate_module_name(cls) -> None: raise NameError(msg) @classmethod - def __init_subclass__( - cls, mixin: bool = False, state_id: int | None = None, **kwargs - ): + def __init_subclass__(cls, mixin: bool = False, **kwargs): """Do some magic for the subclass initialization. Args: mixin: Whether the subclass is a mixin and should not be initialized. - state_id: Explicit state ID for minified state names. **kwargs: The kwargs to pass to the init_subclass method. Raises: - StateValueError: If a substate class shadows another or duplicate state_id. + StateValueError: If a substate class shadows another. """ from reflex.utils.exceptions import StateValueError super().__init_subclass__(**kwargs) - # Mixin states cannot have state_id + # Mixin states are not initialized if cls._mixin: - if state_id is not None: - msg = ( - f"Mixin state '{cls.__module__}.{cls.__name__}' cannot have a state_id. " - "Remove state_id or mixin=True." - ) - raise StateValueError(msg) return - # Store state_id as class variable (only for non-mixins) - cls._state_id = state_id - # Handle locally-defined states for pickling. if "" in cls.__qualname__: cls._handle_local_def() @@ -632,21 +558,6 @@ def __init_subclass__( cls.inherited_vars = parent_state.vars cls.inherited_backend_vars = parent_state.backend_vars - # Check for duplicate state_id among siblings. - if state_id is not None: - for sibling in parent_state.class_subclasses: - if sibling._state_id is not None and sibling._state_id == state_id: - # Allow re-registration of the same class (e.g., module reload) - existing_key = f"{sibling.__module__}.{sibling.__name__}" - new_key = f"{cls.__module__}.{cls.__name__}" - if existing_key != new_key: - msg = ( - f"Duplicate state_id={state_id} among siblings of " - f"'{parent_state.__name__}': already used by " - f"'{sibling.__name__}', cannot be reused by '{cls.__name__}'." - ) - raise StateValueError(msg) - # Check if another substate class with the same name has already been defined. if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}: # This should not happen, since we have added module prefix to state names in #3214 @@ -740,53 +651,16 @@ def __init_subclass__( cls.event_handlers[name] = handler setattr(cls, name, handler) - # Build event_id registry and validate uniqueness within this state class - cls._event_id_to_name = {} - handlers_with_id: list[tuple[str, Callable[..., Any], int]] = [] - handlers_without_id: list[tuple[str, Callable[..., Any]]] = [] - - for name, fn in events.items(): - event_id = getattr(fn, EVENT_ID_MARKER, None) - if event_id is not None: - if event_id in cls._event_id_to_name: - existing_name = cls._event_id_to_name[event_id] - msg = ( - f"Duplicate event_id={event_id} in state '{cls.__name__}': " - f"handlers '{existing_name}' and '{name}' cannot share the same event_id." - ) - raise StateValueError(msg) - cls._event_id_to_name[event_id] = name - handlers_with_id.append((name, fn, event_id)) - else: - handlers_without_id.append((name, fn)) - - from reflex.environment import EventMinifyMode - - minify_mode = environment.REFLEX_MINIFY_EVENTS.get() - - # In ENFORCE mode, all event handlers must have event_id - if minify_mode == EventMinifyMode.ENFORCE and handlers_without_id: - missing = [name for name, _ in handlers_without_id] - msg = ( - f"State '{cls.__name__}' in ENFORCE mode: event handlers " - f"{missing} are missing required event_id." - ) - raise StateValueError(msg) - - # In BEST_EFFORT mode, auto-assign IDs to handlers without explicit IDs - if minify_mode == EventMinifyMode.BEST_EFFORT and handlers_without_id: - # Find the highest user-defined event_id - max_explicit_id = max((eid for _, _, eid in handlers_with_id), default=-1) - - # Sort handlers without IDs alphabetically by name for deterministic ordering - handlers_without_id.sort(key=operator.itemgetter(0)) + # Build event_id registry from minify.json configuration + from reflex.minify import get_event_id, get_state_full_path, is_minify_enabled - # Assign IDs starting from max_explicit_id + 1 - next_id = max_explicit_id + 1 - for name, fn in handlers_without_id: - setattr(fn, EVENT_ID_MARKER, next_id) - cls._event_id_to_name[next_id] = name - next_id += 1 + cls._event_id_to_name = {} + if is_minify_enabled(): + state_path = get_state_full_path(cls) + for handler_name in events: + event_id = get_event_id(state_path, handler_name) + if event_id is not None: + cls._event_id_to_name[event_id] = handler_name # Initialize per-class var dependency tracking. cls._var_dependencies = {} @@ -830,10 +704,6 @@ def _copy_fn(fn: Callable) -> Callable: newfn.__annotations__ = fn.__annotations__ if mark := getattr(fn, BACKGROUND_TASK_MARKER, None): setattr(newfn, BACKGROUND_TASK_MARKER, mark) - # Preserve event_id for minification - event_id = getattr(fn, EVENT_ID_MARKER, None) - if event_id is not None: - object.__setattr__(newfn, EVENT_ID_MARKER, event_id) # Preserve event_actions from @rx.event decorator if event_actions := getattr(fn, EVENT_ACTIONS_MARKER, None): object.__setattr__(newfn, EVENT_ACTIONS_MARKER, event_actions) @@ -1138,34 +1008,20 @@ def get_name(cls) -> str: """Get the name of the state. Returns: - The name of the state. - - Raises: - StateValueError: If ENFORCE mode is set and state_id is missing. + The name of the state (minified if configured in minify.json). """ - from reflex.environment import StateMinifyMode - from reflex.utils.exceptions import StateValueError + from reflex.minify import get_state_full_path, get_state_id, is_minify_enabled module = cls.__module__.replace(".", "___") full_name = format.to_snake_case(f"{module}___{cls.__name__}") - minify_mode = environment.REFLEX_MINIFY_STATES.get() - - if minify_mode == StateMinifyMode.DISABLED: - return full_name - - if cls._state_id is not None: - return _int_to_minified_name(cls._state_id) - - # state_id not set - if minify_mode == StateMinifyMode.ENFORCE: - msg = ( - f"State '{cls.__module__}.{cls.__name__}' is missing required state_id. " - f"Add state_id parameter: class {cls.__name__}(rx.State, state_id=N)" - ) - raise StateValueError(msg) + # If minification is enabled, look up the state ID from minify.json + if is_minify_enabled(): + state_path = get_state_full_path(cls) + state_id = get_state_id(state_path) + if state_id is not None: + return state_id - # ENABLED mode with no state_id - use full name return full_name @classmethod @@ -1894,11 +1750,8 @@ def _get_original_event_name(cls, minified_name: str) -> str | None: Returns: The original event handler name, or None if not found. """ - # Build reverse lookup: minified_name -> original_name - for event_id, original_name in cls._event_id_to_name.items(): - if _int_to_minified_name(event_id) == minified_name: - return original_name - return None + # Direct lookup: _event_id_to_name maps minified_name -> original_name + return cls._event_id_to_name.get(minified_name) def _get_event_handler( self, event: Event @@ -2669,7 +2522,7 @@ def is_serializable(value: Any) -> bool: T_STATE = TypeVar("T_STATE", bound=BaseState) -class State(BaseState, state_id=0): +class State(BaseState): """The app Base State.""" # The hydrated bool. @@ -2762,7 +2615,7 @@ def wrapper() -> Component: LAST_RELOADED_KEY = "reflex_last_reloaded_on_error" -class FrontendEventExceptionState(State, state_id=0): +class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" # If the frontend error message contains any of these strings, automatically reload the page. @@ -2815,7 +2668,7 @@ def handle_frontend_exception( ) -class UpdateVarsInternalState(State, state_id=1): +class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" async def update_vars_internal(self, vars: dict[str, Any]) -> None: @@ -2839,7 +2692,7 @@ async def update_vars_internal(self, vars: dict[str, Any]) -> None: setattr(var_state, var_name, value) -class OnLoadInternalState(State, state_id=2): +class OnLoadInternalState(State): """Substate for handling on_load event enumeration. This is a separate substate to avoid deserializing the entire state tree for every page navigation. diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 23249f07320..62680716cda 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -446,11 +446,10 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: handler: The event handler to get the parts of. Returns: - The state and function name (possibly minified based on REFLEX_MINIFY_EVENTS). + The state and function name (possibly minified based on minify.json). """ - from reflex.environment import EventMinifyMode, environment - from reflex.event import EVENT_ID_MARKER - from reflex.state import State, _int_to_minified_name + from reflex.minify import is_minify_enabled + from reflex.state import State # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") @@ -468,12 +467,21 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: if state_full_name == FRONTEND_EVENT_STATE and name not in State.__dict__: return ("", to_snake_case(handler.fn.__qualname__)) - # Check for event_id minification - mode = environment.REFLEX_MINIFY_EVENTS.get() - if mode != EventMinifyMode.DISABLED: - event_id = getattr(handler.fn, EVENT_ID_MARKER, None) - if event_id is not None: - name = _int_to_minified_name(event_id) + # Check for event_id minification from minify.json + # The state class stores its event ID mapping in _event_id_to_name + # where key is minified_name and value is original_handler_name + if is_minify_enabled(): + try: + # Get the state class using the path + state_cls = State.get_class_substate(state_full_name) + # Look up the minified name by original handler name + for minified_name, handler_name in state_cls._event_id_to_name.items(): + if handler_name == name: + name = minified_name + break + except ValueError: + # State not found, skip minification + pass return (state_full_name, name) diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 1524e1d285a..8a4d057d85e 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -2,55 +2,46 @@ from __future__ import annotations -import os +import json from collections.abc import Generator -from functools import partial from typing import TYPE_CHECKING import pytest from selenium.webdriver.common.by import By -from reflex.environment import EventMinifyMode, StateMinifyMode, environment -from reflex.state import _int_to_minified_name +from reflex.minify import MINIFY_JSON, clear_config_cache, int_to_minified_name from reflex.testing import AppHarness if TYPE_CHECKING: from selenium.webdriver.remote.webdriver import WebDriver -def MinificationApp( - root_state_id: int, - sub_state_id: int, - increment_event_id: int | None = None, - update_message_event_id: int | None = None, -): +def MinificationApp(): """Test app for state and event handler minification. - Args: - root_state_id: The state_id for the root state. - sub_state_id: The state_id for the sub state. - increment_event_id: The event_id for the increment event handler. - update_message_event_id: The event_id for the update_message event handler. + This app is used to test that: + 1. Without minify.json, full state/event names are used + 2. With minify.json, minified names are used based on the config """ import reflex as rx from reflex.utils import format - class RootState(rx.State, state_id=root_state_id): - """Root state with explicit state_id.""" + class RootState(rx.State): + """Root state for testing.""" count: int = 0 - @rx.event(event_id=increment_event_id) + @rx.event def increment(self): """Increment the count.""" self.count += 1 - class SubState(RootState, state_id=sub_state_id): - """Sub state with explicit state_id.""" + class SubState(RootState): + """Sub state for testing.""" message: str = "hello" - @rx.event(event_id=update_message_event_id) + @rx.event def update_message(self): """Update the message.""" parent = self.parent_state @@ -103,7 +94,7 @@ def minify_disabled_app( app_harness_env: type[AppHarness], tmp_path_factory: pytest.TempPathFactory, ) -> Generator[AppHarness, None, None]: - """Start app with REFLEX_MINIFY_STATES=disabled. + """Start app WITHOUT minify.json (full names). Args: app_harness_env: AppHarness or AppHarnessProd @@ -112,37 +103,24 @@ def minify_disabled_app( Yields: Running AppHarness instance """ - os.environ["REFLEX_MINIFY_STATES"] = "disabled" - os.environ["REFLEX_MINIFY_EVENTS"] = "disabled" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) + # Clear minify config cache to ensure clean state + clear_config_cache() + # No minify.json file - full names will be used with app_harness_env.create( root=tmp_path_factory.mktemp("minify_disabled"), app_name="minify_disabled", - app_source=partial( - MinificationApp, - root_state_id=3, - sub_state_id=4, - increment_event_id=0, - update_message_event_id=0, - ), + app_source=MinificationApp, ) as harness: yield harness - # Cleanup - os.environ.pop("REFLEX_MINIFY_STATES", None) - os.environ.pop("REFLEX_MINIFY_EVENTS", None) - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) - @pytest.fixture def minify_enabled_app( app_harness_env: type[AppHarness], tmp_path_factory: pytest.TempPathFactory, ) -> Generator[AppHarness, None, None]: - """Start app with minification enabled. + """Start app WITH minify.json (minified names). Args: app_harness_env: AppHarness or AppHarnessProd @@ -151,29 +129,53 @@ def minify_enabled_app( Yields: Running AppHarness instance """ - os.environ["REFLEX_MINIFY_STATES"] = "enabled" - os.environ["REFLEX_MINIFY_EVENTS"] = "enabled" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) + # Clear minify config cache to ensure clean state + clear_config_cache() - with app_harness_env.create( - root=tmp_path_factory.mktemp("minify_enabled"), + app_root = tmp_path_factory.mktemp("minify_enabled") + + # Create the harness object (but don't start yet) + harness = app_harness_env.create( + root=app_root, app_name="minify_enabled", - app_source=partial( - MinificationApp, - root_state_id=10, - sub_state_id=11, - increment_event_id=0, - update_message_event_id=0, - ), - ) as harness: - yield harness + app_source=MinificationApp, + ) - # Cleanup - os.environ.pop("REFLEX_MINIFY_STATES", None) - os.environ.pop("REFLEX_MINIFY_EVENTS", None) - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) + # Create minify.json with explicit IDs for our states and events + # The state paths need to match what get_state_full_path() returns + # Format: module.StateHierarchy (e.g., "minify_enabled.minify_enabled.State.RootState") + # Note: RootState extends rx.State, so the path includes State in the hierarchy + # Version 2 format: string IDs and nested events + app_module = "minify_enabled.minify_enabled" + root_state_path = f"{app_module}.State.RootState" + sub_state_path = f"{app_module}.State.RootState.SubState" + minify_config = { + "version": 1, + "states": { + # Base State needs an ID too since it's in the hierarchy + "reflex.state.State": "a", + # RootState extends State, so path is module.State.RootState + root_state_path: "k", # int_to_minified_name(10) = 'k' + # SubState extends RootState, so path is module.State.RootState.SubState + sub_state_path: "l", # int_to_minified_name(11) = 'l' + }, + "events": { + # Events are now nested under their state path + root_state_path: { + "increment": "f", # int_to_minified_name(5) = 'f' + }, + sub_state_path: { + "update_message": "h", # int_to_minified_name(7) = 'h' + }, + }, + } + + # Write minify.json to the app root directory + minify_path = app_root / MINIFY_JSON + minify_path.write_text(json.dumps(minify_config, indent=2)) + + with harness: + yield harness @pytest.fixture @@ -220,7 +222,7 @@ def test_minification_disabled( minify_disabled_app: AppHarness, driver_disabled: WebDriver, ) -> None: - """Test that DISABLED mode uses full state and event names. + """Test that without minify.json, full state and event names are used. Args: minify_disabled_app: harness for the app @@ -287,7 +289,7 @@ def test_minification_enabled( minify_enabled_app: AppHarness, driver_enabled: WebDriver, ) -> None: - """Test that ENABLED mode uses minified state and event names. + """Test that with minify.json, minified state and event names are used. Args: minify_enabled_app: harness for the app @@ -310,10 +312,11 @@ def test_minification_enabled( root_state_name = root_state_name_el.text sub_state_name = sub_state_name_el.text - # In enabled mode with state_id, names should be minified - # state_id=10 -> 'k', state_id=11 -> 'l' - expected_root_minified = _int_to_minified_name(10) - expected_sub_minified = _int_to_minified_name(11) + # In enabled mode with minify.json, names should be minified + # RootState has state_id=10 -> 'k' + # SubState has state_id=11 -> 'l' + expected_root_minified = int_to_minified_name(10) + expected_sub_minified = int_to_minified_name(11) assert expected_root_minified in root_state_name assert expected_sub_minified in sub_state_name @@ -337,19 +340,22 @@ def test_minification_enabled( else update_handler_text ) - # In enabled mode with event_id, names should be minified - # event_id=0 -> 'a' for both handlers - expected_event_minified = _int_to_minified_name(0) + # In enabled mode with minify.json: + # - increment has event_id=5 -> 'f' + # - update_message has event_id=7 -> 'h' + expected_increment_minified = int_to_minified_name(5) + expected_update_minified = int_to_minified_name(7) # Event handler format: "state_name.event_name" - # For increment: "k.a" (state_id=10 -> 'k', event_id=0 -> 'a') - # For update_message: "k.l.a" (state_id=10.11 -> 'k.l', event_id=0 -> 'a') - # The event name should be minified to 'a' - assert increment_handler.endswith(f".{expected_event_minified}"), ( - f"Expected minified event name, got: {increment_handler}" + # For increment: "k.f" (state_id=10 -> 'k', event_id=5 -> 'f') + # For update_message: "k.l.h" (state_id=10.11 -> 'k.l', event_id=7 -> 'h') + assert increment_handler.endswith(f".{expected_increment_minified}"), ( + f"Expected minified event name ending with '.{expected_increment_minified}', " + f"got: {increment_handler}" ) - assert update_handler.endswith(f".{expected_event_minified}"), ( - f"Expected minified event name, got: {update_handler}" + assert update_handler.endswith(f".{expected_update_minified}"), ( + f"Expected minified event name ending with '.{expected_update_minified}', " + f"got: {update_handler}" ) # The handler names should NOT contain the original method names diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index b59343ac693..eec1beea942 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -1,371 +1,356 @@ -"""Unit tests for state and event handler minification.""" +"""Unit tests for state and event handler minification via minify.json.""" from __future__ import annotations +import json + import pytest -from reflex.environment import EventMinifyMode, StateMinifyMode, environment -from reflex.event import EVENT_ID_MARKER -from reflex.state import ( - BaseState, - FrontendEventExceptionState, - OnLoadInternalState, - State, - UpdateVarsInternalState, - _int_to_minified_name, - _minified_name_to_int, +from reflex.minify import ( + MINIFY_JSON, + SCHEMA_VERSION, + MinifyConfig, + clear_config_cache, + generate_minify_config, + get_event_id, + get_state_full_path, + get_state_id, + int_to_minified_name, + is_minify_enabled, + minified_name_to_int, + save_minify_config, + sync_minify_config, + validate_minify_config, ) -from reflex.utils.exceptions import StateValueError - - -@pytest.fixture -def reset_minify_mode(): - """Reset minify modes to DISABLED after each test.""" - original_states = environment.REFLEX_MINIFY_STATES.get() - original_events = environment.REFLEX_MINIFY_EVENTS.get() - yield - environment.REFLEX_MINIFY_STATES.set(original_states) - environment.REFLEX_MINIFY_EVENTS.set(original_events) +from reflex.state import BaseState, State class TestIntToMinifiedName: - """Tests for _int_to_minified_name function.""" + """Tests for int_to_minified_name function.""" def test_zero(self): """Test that 0 maps to 'a'.""" - assert _int_to_minified_name(0) == "a" + assert int_to_minified_name(0) == "a" def test_single_char(self): """Test single character mappings.""" - assert _int_to_minified_name(1) == "b" - assert _int_to_minified_name(25) == "z" - assert _int_to_minified_name(26) == "A" - assert _int_to_minified_name(51) == "Z" - assert _int_to_minified_name(52) == "$" - assert _int_to_minified_name(53) == "_" + assert int_to_minified_name(1) == "b" + assert int_to_minified_name(25) == "z" + assert int_to_minified_name(26) == "A" + assert int_to_minified_name(51) == "Z" + assert int_to_minified_name(52) == "$" + assert int_to_minified_name(53) == "_" def test_two_chars(self): """Test two character mappings (base 54).""" # 54 = 1*54 + 0 -> 'ba' - assert _int_to_minified_name(54) == "ba" + assert int_to_minified_name(54) == "ba" # 55 = 1*54 + 1 -> 'bb' - assert _int_to_minified_name(55) == "bb" + assert int_to_minified_name(55) == "bb" def test_unique_names(self): """Test that a large range of IDs produce unique names.""" names = set() for i in range(10000): - name = _int_to_minified_name(i) + name = int_to_minified_name(i) assert name not in names, f"Duplicate name {name} for id {i}" names.add(name) + def test_negative_raises(self): + """Test that negative IDs raise ValueError.""" + with pytest.raises(ValueError, match="non-negative"): + int_to_minified_name(-1) -class TestStateIdValidation: - """Tests for state_id validation in __init_subclass__.""" - def test_state_with_explicit_id(self): - """Test that a state can be created with an explicit state_id.""" +class TestMinifiedNameToInt: + """Tests for minified_name_to_int reverse conversion.""" - class TestState(BaseState, state_id=100): - pass + def test_single_char(self): + """Test single character conversion.""" + assert minified_name_to_int("a") == 0 + assert minified_name_to_int("b") == 1 + assert minified_name_to_int("z") == 25 + assert minified_name_to_int("A") == 26 + assert minified_name_to_int("Z") == 51 - assert TestState._state_id == 100 + def test_roundtrip(self): + """Test that int -> minified -> int roundtrip works.""" + for i in range(1000): + minified = int_to_minified_name(i) + result = minified_name_to_int(minified) + assert result == i, f"Roundtrip failed for {i}: {minified} -> {result}" - def test_state_without_id(self): - """Test that a state can be created without state_id.""" + def test_invalid_char_raises(self): + """Test that invalid characters raise ValueError.""" + with pytest.raises(ValueError, match="Invalid character"): + minified_name_to_int("!") - class TestState(BaseState): - pass - assert TestState._state_id is None +class TestGetStateFullPath: + """Tests for get_state_full_path function.""" - def test_duplicate_state_id_among_siblings_raises(self): - """Test that duplicate state_id among siblings raises StateValueError.""" + def test_root_state_path(self): + """Test that root State has correct full path.""" + path = get_state_full_path(State) + assert path == "reflex.state.State" - class ParentState(BaseState, state_id=200): - pass + def test_substate_path(self): + """Test that substates have correct full paths.""" - class FirstChild(ParentState, state_id=10): + class TestState(BaseState): pass - with pytest.raises(StateValueError, match="Duplicate state_id=10"): + path = get_state_full_path(TestState) + assert "TestState" in path + assert path.startswith("tests.units.test_minification.") - class SecondChild(ParentState, state_id=10): - pass - def test_same_state_id_across_branches_allowed(self): - """Test that the same state_id can be used in different branches.""" +@pytest.fixture +def temp_minify_json(tmp_path, monkeypatch): + """Create a temporary directory and mock cwd to use it for minify.json. + + Yields: + The temporary directory path. + """ + monkeypatch.chdir(tmp_path) + clear_config_cache() + # Clear State caches to ensure clean slate + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + yield tmp_path + # Clean up: clear config and all cached state names + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + + +class TestMinifyConfig: + """Tests for minify.json configuration loading and saving.""" + + def test_no_config_returns_none(self, temp_minify_json): + """Test that missing minify.json returns None.""" + assert is_minify_enabled() is False + assert get_state_id("any.path") is None + assert get_event_id("any.path", "handler") is None + + def test_save_and_load_config(self, temp_minify_json): + """Test saving and loading a config.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) - class Root(BaseState, state_id=210): - pass + # Clear cache and reload + clear_config_cache() - class BranchA(Root, state_id=1): - pass + assert is_minify_enabled() is True + assert get_state_id("test.module.MyState") == "a" + assert get_event_id("test.module.MyState", "handler") == "a" - class BranchB(Root, state_id=2): - pass + def test_invalid_version_raises(self, temp_minify_json): + """Test that invalid version raises ValueError.""" + config = {"version": 999, "states": {}, "events": {}} + path = temp_minify_json / MINIFY_JSON + with path.open("w") as f: + json.dump(config, f) - class LeafA(BranchA, state_id=5): - pass + clear_config_cache() - class LeafB(BranchB, state_id=5): # same state_id=5, different parent -- OK! - pass + with pytest.raises(ValueError, match=r"Unsupported.*version"): + is_minify_enabled() - # Both should succeed - state_id is per-parent (sibling uniqueness) - assert LeafA._state_id == 5 - assert LeafB._state_id == 5 - # But they have different full names - assert LeafA.get_parent_state() is BranchA - assert LeafB.get_parent_state() is BranchB + def test_missing_states_raises(self, temp_minify_json): + """Test that missing 'states' key raises ValueError.""" + config = {"version": SCHEMA_VERSION, "events": {}} + path = temp_minify_json / MINIFY_JSON + with path.open("w") as f: + json.dump(config, f) + clear_config_cache() -class TestGetNameMinification: - """Tests for get_name with minification modes.""" + with pytest.raises(ValueError, match="'states' must be"): + is_minify_enabled() - def test_disabled_mode_uses_full_name(self, reset_minify_mode): - """Test DISABLED mode always uses full name even with state_id.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - class TestState(BaseState, state_id=300): - pass +class TestGenerateMinifyConfig: + """Tests for generate_minify_config function.""" - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() + def test_generate_for_root_state(self): + """Test generating config for the root State.""" + config = generate_minify_config(State) - name = TestState.get_name() - # Should be full name, not minified - assert "test_state" in name.lower() - assert name != _int_to_minified_name(300) + assert config["version"] == SCHEMA_VERSION + assert "reflex.state.State" in config["states"] + # State should have event handlers like set_is_hydrated + state_path = "reflex.state.State" + assert state_path in config["events"] + assert "set_is_hydrated" in config["events"][state_path] - def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): - """Test ENABLED mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) + def test_generates_unique_sibling_ids(self): + """Test that sibling states get unique IDs.""" - class TestState(BaseState, state_id=301): + class ParentState(BaseState): pass - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - assert name == _int_to_minified_name(301) - - def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): - """Test ENABLED mode without state_id uses full name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) - - class TestState(BaseState): + class ChildA(ParentState): pass - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() - - name = TestState.get_name() - # Should contain the class name - assert "test_state" in name.lower() - - def test_enforce_mode_without_id_raises(self, reset_minify_mode): - """Test ENFORCE mode without state_id raises error during class definition.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - - # Error is raised during class definition because get_name() is called - # during __init_subclass__ - with pytest.raises(StateValueError, match="missing required state_id"): - - class TestState(BaseState): - pass - - def test_enforce_mode_with_id_uses_minified(self, reset_minify_mode): - """Test ENFORCE mode with state_id uses minified name.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - - class TestState(BaseState, state_id=302): + class ChildB(ParentState): pass - # Clear the lru_cache to get fresh result - TestState.get_name.cache_clear() + config = generate_minify_config(ParentState) - name = TestState.get_name() - assert name == _int_to_minified_name(302) + # Find the IDs for ChildA and ChildB + child_a_path = get_state_full_path(ChildA) + child_b_path = get_state_full_path(ChildB) + child_a_id = config["states"].get(child_a_path) + child_b_id = config["states"].get(child_b_path) -class TestMixinState: - """Tests for mixin states.""" + assert child_a_id is not None + assert child_b_id is not None + assert child_a_id != child_b_id - def test_mixin_no_state_id_required(self, reset_minify_mode): - """Test that mixin states don't require state_id even in ENFORCE mode.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENFORCE) - class MixinState(BaseState, mixin=True): - pass +class TestValidateMinifyConfig: + """Tests for validate_minify_config function.""" - # Mixin states should not raise even without state_id - assert MixinState._state_id is None - # Mixin states have _mixin = True set, so get_name isn't typically called - # but the class should be created without error + def test_valid_config_no_errors(self): + """Test that a valid config produces no errors.""" + config = generate_minify_config(State) + errors, _warnings, missing = validate_minify_config(config, State) - def test_mixin_with_state_id_raises(self): - """Test that mixin states cannot have state_id.""" - with pytest.raises(StateValueError, match="cannot have a state_id"): + assert len(errors) == 0 + assert len(missing) == 0 - class MixinWithId(BaseState, mixin=True, state_id=999): - pass + def test_duplicate_state_ids_detected(self): + """Test that duplicate state IDs are detected.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "test.Parent": "a", + "test.Parent.ChildA": "b", + "test.Parent.ChildB": "b", # Duplicate! + }, + "events": {}, + } + # Create a mock state tree + class Parent(BaseState): + pass -class TestEventIdValidation: - """Tests for event_id validation in __init_subclass__.""" + errors, _warnings, _missing = validate_minify_config(config, Parent) - def test_event_with_explicit_id(self): - """Test that an event handler can be created with an explicit event_id.""" - import reflex as rx + assert any("Duplicate state_id='b'" in e for e in errors) - class TestState(BaseState, state_id=400): - @rx.event(event_id=0) - def my_handler(self): - pass - assert 0 in TestState._event_id_to_name - assert TestState._event_id_to_name[0] == "my_handler" +class TestSyncMinifyConfig: + """Tests for sync_minify_config function.""" - def test_event_without_id(self): - """Test that an event handler can be created without event_id.""" - import reflex as rx + def test_sync_adds_new_states(self): + """Test that sync adds new states.""" - class TestState(BaseState, state_id=401): - @rx.event - def my_handler(self): + class TestState(BaseState): + def handler(self): pass - # Should not be in the registry - assert 0 not in TestState._event_id_to_name - - def test_duplicate_event_id_within_state_raises(self): - """Test that duplicate event_id within same state raises StateValueError.""" - import reflex as rx - - with pytest.raises(StateValueError, match="Duplicate event_id=0"): + # Start with empty config + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {}, + "events": {}, + } - class TestState(BaseState, state_id=402): - @rx.event(event_id=0) - def handler1(self): - pass + new_config = sync_minify_config(existing_config, TestState) - @rx.event(event_id=0) - def handler2(self): - pass + # Should have added the state + state_path = get_state_full_path(TestState) + assert state_path in new_config["states"] + assert state_path in new_config["events"] + assert "handler" in new_config["events"][state_path] - def test_same_event_id_across_states_allowed(self): - """Test that same event_id can be used in different state classes.""" - import reflex as rx + def test_sync_preserves_existing_ids(self): + """Test that sync preserves existing IDs.""" - class StateA(BaseState, state_id=403): - @rx.event(event_id=0) - def handler(self): + class TestState(BaseState): + def handler_a(self): pass - class StateB(BaseState, state_id=404): - @rx.event(event_id=0) - def handler(self): + def handler_b(self): pass - # Both should succeed - event_id is per-state - assert StateA._event_id_to_name[0] == "handler" - assert StateB._event_id_to_name[0] == "handler" - - def test_event_id_stored_on_function(self): - """Test that event_id is stored as EVENT_ID_MARKER on the function.""" - import reflex as rx - - @rx.event(event_id=42) - def standalone_handler(self): - pass - - assert hasattr(standalone_handler, EVENT_ID_MARKER) - assert getattr(standalone_handler, EVENT_ID_MARKER) == 42 - - -class TestEventHandlerMinification: - """Tests for event handler name minification in get_event_handler_parts.""" - - def test_disabled_mode_uses_full_name(self, reset_minify_mode): - """Test DISABLED mode uses full event name even with event_id.""" - import reflex as rx - from reflex.utils.format import get_event_handler_parts + state_path = get_state_full_path(TestState) - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.DISABLED) + # Start with partial config (using string IDs in v2 format) + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {state_path: "bU"}, # Some arbitrary minified name + "events": {state_path: {"handler_a": "k"}}, # Another arbitrary name + } - class TestState(BaseState, state_id=500): - @rx.event(event_id=0) - def my_handler(self): - pass + new_config = sync_minify_config(existing_config, TestState) - handler = TestState.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) + # Existing IDs should be preserved + assert new_config["states"][state_path] == "bU" + assert new_config["events"][state_path]["handler_a"] == "k" + # New handler should be added with next ID (k=10, so next is l=11) + assert "handler_b" in new_config["events"][state_path] + assert ( + new_config["events"][state_path]["handler_b"] == "l" + ) # 10 + 1 = 11 -> 'l' - # Should use full name, not minified - assert event_name == "my_handler" - def test_enabled_mode_with_id_uses_minified(self, reset_minify_mode): - """Test ENABLED mode with event_id uses minified name.""" - import reflex as rx - from reflex.utils.format import get_event_handler_parts +class TestStateMinification: + """Tests for state name minification with minify.json.""" - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) + def test_state_uses_full_name_without_config(self, temp_minify_json): + """Test that states use full names when no minify.json exists.""" - class TestState(BaseState, state_id=501): - @rx.event(event_id=5) - def my_handler(self): - pass + class TestState(BaseState): + pass TestState.get_name.cache_clear() - handler = TestState.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) - - # Should use minified name - assert event_name == _int_to_minified_name(5) - assert event_name == "f" + name = TestState.get_name() - def test_enabled_mode_without_id_uses_full_name(self, reset_minify_mode): - """Test ENABLED mode without event_id uses full name.""" - import reflex as rx - from reflex.utils.format import get_event_handler_parts + # Should be the full name (snake_case module___class) + assert "test_state" in name.lower() - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) + def test_state_uses_minified_name_with_config(self, temp_minify_json): + """Test that states use minified names when minify.json exists.""" - class TestState(BaseState, state_id=502): - @rx.event - def my_handler(self): - pass + class TestState(BaseState): + pass + state_path = get_state_full_path(TestState) + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {state_path: "f"}, # Direct minified name + "events": {}, + } + save_minify_config(config) + clear_config_cache() TestState.get_name.cache_clear() - handler = TestState.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) - # Should use full name - assert event_name == "my_handler" - - def test_enforce_mode_without_event_id_raises(self, reset_minify_mode): - """Test ENFORCE mode without event_id raises error during class definition.""" - import reflex as rx + name = TestState.get_name() - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENFORCE) + # Should be the minified name directly + assert name == "f" - with pytest.raises(StateValueError, match="missing required event_id"): - class TestState(BaseState, state_id=503): - @rx.event - def my_handler(self): - pass +class TestEventMinification: + """Tests for event handler name minification with minify.json.""" - def test_enforce_mode_with_event_id_works(self, reset_minify_mode): - """Test ENFORCE mode with event_id creates state successfully.""" + def test_event_uses_full_name_without_config(self, temp_minify_json): + """Test that event handlers use full names when no minify.json exists.""" import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENFORCE) - - class TestState(BaseState, state_id=504): - @rx.event(event_id=0) + class TestState(BaseState): + @rx.event def my_handler(self): pass @@ -373,368 +358,64 @@ def my_handler(self): handler = TestState.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) - # Should use minified name - assert event_name == _int_to_minified_name(0) - assert event_name == "a" - - -class TestMixinEventHandlers: - """Tests for event handlers from mixin states.""" + # Should use full name + assert event_name == "my_handler" - def test_mixin_event_id_preserved(self, reset_minify_mode): - """Test that event_id from mixin handlers is preserved when inherited.""" + def test_event_uses_minified_name_with_config(self, temp_minify_json): + """Test that event handlers use minified names when minify.json exists.""" import reflex as rx from reflex.utils.format import get_event_handler_parts - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) - - class MixinState(BaseState, mixin=True): - @rx.event(event_id=10) - def mixin_handler(self): - pass - - # Need to inherit from both mixin AND a non-mixin base (BaseState) - # to create a non-mixin concrete state - class ConcreteState(MixinState, BaseState, state_id=600): - @rx.event(event_id=0) - def own_handler(self): - pass - - ConcreteState.get_name.cache_clear() - - # Both handlers should have their event_ids preserved - assert 10 in ConcreteState._event_id_to_name - assert ConcreteState._event_id_to_name[10] == "mixin_handler" - assert 0 in ConcreteState._event_id_to_name - assert ConcreteState._event_id_to_name[0] == "own_handler" - - # Check minified names - mixin_handler = ConcreteState.event_handlers["mixin_handler"] - own_handler = ConcreteState.event_handlers["own_handler"] - - _, mixin_name = get_event_handler_parts(mixin_handler) - _, own_name = get_event_handler_parts(own_handler) - - assert mixin_name == _int_to_minified_name(10) # "k" - assert own_name == _int_to_minified_name(0) # "a" - - def test_mixin_event_id_conflict_raises(self, reset_minify_mode): - """Test that conflicting event_ids from mixin and concrete state raises error.""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.ENABLED) - - class MixinState(BaseState, mixin=True): - @rx.event(event_id=0) - def mixin_handler(self): - pass - - with pytest.raises(StateValueError, match="Duplicate event_id=0"): - # Need to inherit from both mixin AND a non-mixin base (BaseState) - class ConcreteState(MixinState, BaseState, state_id=601): - @rx.event(event_id=0) - def own_handler(self): - pass - - -class TestMinifiedNameToInt: - """Tests for _minified_name_to_int reverse conversion.""" - - def test_single_char(self): - """Test single character conversion.""" - assert _minified_name_to_int("a") == 0 - assert _minified_name_to_int("b") == 1 - assert _minified_name_to_int("z") == 25 - assert _minified_name_to_int("A") == 26 - assert _minified_name_to_int("Z") == 51 - - def test_roundtrip(self): - """Test that int -> minified -> int roundtrip works.""" - for i in range(1000): - minified = _int_to_minified_name(i) - result = _minified_name_to_int(minified) - assert result == i, f"Roundtrip failed for {i}: {minified} -> {result}" - - def test_invalid_char_raises(self): - """Test that invalid characters raise ValueError.""" - with pytest.raises(ValueError, match="Invalid character"): - _minified_name_to_int("!") - - def test_state_has_state_id_zero(self): - """Test that the root State class has state_id=0.""" - assert State._state_id == 0 - assert State.__module__ == "reflex.state" - assert State.__name__ == "State" - - def test_next_sibling_state_id(self): - """Test finding next available state_id among siblings.""" - - class Parent(BaseState, state_id=700): - pass - - class Child0(Parent, state_id=0): - pass - - class Child1(Parent, state_id=1): - pass - - # Find first gap starting from 0 among Parent's children - used_ids = { - child._state_id - for child in Parent.class_subclasses - if child._state_id is not None + # First, set up the config BEFORE creating the state class + # The event_id_to_name registry is built during __init_subclass__ + # so the config must exist before the class is defined + + # For this test, we extend State (not BaseState) so that + # get_event_handler_parts can look up our state in the State tree. + # We need to include State's full path in our config too. + + # The state path includes the full class hierarchy from State. + # For a direct subclass of State defined in this test module, + # get_state_full_path returns: "tests.units.test_minification.State.TestStateWithMinifiedEvent" + # (module + class hierarchy from root state to leaf) + + expected_module = "tests.units.test_minification" + expected_state_path = f"{expected_module}.State.TestStateWithMinifiedEvent" + + # Also need to include the base State in the config (v2 format with nested events) + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", # Base State + expected_state_path: "b", # Our test state + }, + "events": { + expected_state_path: {"my_handler": "d"}, # Nested under state path + }, } - next_id = 0 - while next_id in used_ids: - next_id += 1 - - assert next_id == 2 - - -class TestInternalStateIds: - """Tests for internal state classes having correct state_id values.""" - - def test_state_has_id_0(self): - """Test that the base State class has state_id=0.""" - assert State._state_id == 0 - - def test_frontend_exception_state_has_id_0(self): - """Test that FrontendEventExceptionState has state_id=0.""" - assert FrontendEventExceptionState._state_id == 0 - - def test_update_vars_internal_state_has_id_1(self): - """Test that UpdateVarsInternalState has state_id=1.""" - assert UpdateVarsInternalState._state_id == 1 - - def test_on_load_internal_state_has_id_2(self): - """Test that OnLoadInternalState has state_id=2.""" - assert OnLoadInternalState._state_id == 2 - - def test_internal_states_minified_names(self, reset_minify_mode): - """Test that internal states get correct minified names when enabled.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.ENABLED) - - # Clear the lru_cache to get fresh results - State.get_name.cache_clear() - FrontendEventExceptionState.get_name.cache_clear() - UpdateVarsInternalState.get_name.cache_clear() - OnLoadInternalState.get_name.cache_clear() - - # State (id=0) -> "a" - assert State.get_name() == "a" - # FrontendEventExceptionState (id=0) -> "a" - assert FrontendEventExceptionState.get_name() == "a" - # UpdateVarsInternalState (id=1) -> "b" - assert UpdateVarsInternalState.get_name() == "b" - # OnLoadInternalState (id=2) -> "c" - assert OnLoadInternalState.get_name() == "c" - - def test_internal_states_full_names_when_disabled(self, reset_minify_mode): - """Test that internal states use full names when minification is disabled.""" - environment.REFLEX_MINIFY_STATES.set(StateMinifyMode.DISABLED) - - # Clear the lru_cache to get fresh results + save_minify_config(config) + clear_config_cache() State.get_name.cache_clear() - FrontendEventExceptionState.get_name.cache_clear() - UpdateVarsInternalState.get_name.cache_clear() - OnLoadInternalState.get_name.cache_clear() - - # Should contain the class name pattern - assert "state" in State.get_name().lower() - assert "frontend" in FrontendEventExceptionState.get_name().lower() - assert "update" in UpdateVarsInternalState.get_name().lower() - assert "on_load" in OnLoadInternalState.get_name().lower() - - -class TestBestEffortMode: - """Tests for BEST_EFFORT event minification mode.""" - - def test_best_effort_assigns_ids_to_handlers_without_explicit_id( - self, reset_minify_mode - ): - """Test that BEST_EFFORT mode assigns event_ids to handlers without explicit IDs.""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=700): - @rx.event - def handler_a(self): - pass - - @rx.event - def handler_b(self): - pass - - # Both handlers should have event_ids assigned - assert 0 in TestState._event_id_to_name - assert 1 in TestState._event_id_to_name - # Should be assigned alphabetically - assert TestState._event_id_to_name[0] == "handler_a" - assert TestState._event_id_to_name[1] == "handler_b" - - def test_best_effort_starts_after_highest_explicit_id(self, reset_minify_mode): - """Test that BEST_EFFORT mode starts assigning IDs after the highest explicit ID.""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=701): - @rx.event(event_id=5) - def explicit_handler(self): - pass - - @rx.event(event_id=10) - def another_explicit(self): - pass + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + # Now create the state class extending State - it will pick up the config + class TestStateWithMinifiedEvent(State): @rx.event - def auto_handler(self): - pass - - # Explicit handlers should keep their IDs - assert TestState._event_id_to_name[5] == "explicit_handler" - assert TestState._event_id_to_name[10] == "another_explicit" - # Auto-assigned handler should start at max_explicit_id + 1 = 11 - assert TestState._event_id_to_name[11] == "auto_handler" - - def test_best_effort_assigns_ids_alphabetically(self, reset_minify_mode): - """Test that BEST_EFFORT mode assigns IDs to handlers alphabetically by name.""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=702): - @rx.event - def zebra_handler(self): - pass - - @rx.event - def alpha_handler(self): - pass - - @rx.event - def middle_handler(self): - pass - - # Should be assigned alphabetically: alpha, middle, zebra - assert TestState._event_id_to_name[0] == "alpha_handler" - assert TestState._event_id_to_name[1] == "middle_handler" - assert TestState._event_id_to_name[2] == "zebra_handler" - - def test_best_effort_with_no_explicit_ids(self, reset_minify_mode): - """Test that BEST_EFFORT mode works with no explicit IDs (starts at 0).""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=703): - @rx.event - def first_handler(self): - pass - - @rx.event - def second_handler(self): - pass - - # Should start at 0 since no explicit IDs - assert TestState._event_id_to_name[0] == "first_handler" - assert TestState._event_id_to_name[1] == "second_handler" - - def test_best_effort_minifies_all_handlers(self, reset_minify_mode): - """Test that BEST_EFFORT mode minifies all handlers in format output.""" - import reflex as rx - from reflex.utils.format import get_event_handler_parts - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=704): - @rx.event(event_id=0) - def explicit_handler(self): - pass - - @rx.event - def auto_handler(self): - pass - - TestState.get_name.cache_clear() - - explicit = TestState.event_handlers["explicit_handler"] - auto = TestState.event_handlers["auto_handler"] - - _, explicit_name = get_event_handler_parts(explicit) - _, auto_name = get_event_handler_parts(auto) - - # Both should be minified - assert explicit_name == _int_to_minified_name(0) # 'a' - assert auto_name == _int_to_minified_name(1) # 'b' - - def test_best_effort_skips_gaps_in_explicit_ids(self, reset_minify_mode): - """Test that BEST_EFFORT mode skips gaps in explicit IDs (doesn't fill them).""" - import reflex as rx - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=705): - @rx.event(event_id=0) - def handler_zero(self): - pass - - @rx.event(event_id=5) - def handler_five(self): - pass - - @rx.event(event_id=10) - def handler_ten(self): - pass - - @rx.event - def auto_handler(self): - pass - - # Explicit handlers keep their IDs - assert TestState._event_id_to_name[0] == "handler_zero" - assert TestState._event_id_to_name[5] == "handler_five" - assert TestState._event_id_to_name[10] == "handler_ten" - # Auto-assigned starts at 11, not filling gaps at 1-4 or 6-9 - assert TestState._event_id_to_name[11] == "auto_handler" - # Verify gaps are not filled - assert 1 not in TestState._event_id_to_name - assert 6 not in TestState._event_id_to_name - - def test_best_effort_mixed_explicit_and_auto(self, reset_minify_mode): - """Test BEST_EFFORT with a mix of explicit and auto-assigned handlers.""" - import reflex as rx - from reflex.utils.format import get_event_handler_parts - - environment.REFLEX_MINIFY_EVENTS.set(EventMinifyMode.BEST_EFFORT) - - class TestState(BaseState, state_id=706): - @rx.event(event_id=3) - def explicit_three(self): + def my_handler(self): pass - @rx.event - def auto_alpha(self): - pass + # Verify the path matches what we expected + actual_path = get_state_full_path(TestStateWithMinifiedEvent) + assert actual_path == expected_state_path, ( + f"Expected path {expected_state_path}, got {actual_path}" + ) - @rx.event - def auto_beta(self): - pass + # The state's _event_id_to_name should be populated (key is minified name) + assert TestStateWithMinifiedEvent._event_id_to_name == {"d": "my_handler"} - TestState.get_name.cache_clear() + handler = TestStateWithMinifiedEvent.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) - # Check registry - assert TestState._event_id_to_name[3] == "explicit_three" - assert TestState._event_id_to_name[4] == "auto_alpha" # alphabetically first - assert TestState._event_id_to_name[5] == "auto_beta" # alphabetically second - - # Check that all handlers are minified correctly - for name, expected_id in [ - ("explicit_three", 3), - ("auto_alpha", 4), - ("auto_beta", 5), - ]: - handler = TestState.event_handlers[name] - _, minified = get_event_handler_parts(handler) - assert minified == _int_to_minified_name(expected_id) + # Should be the minified name directly + assert event_name == "d" From ee4c7f172652d90f453147f9f739f1744e91ddcf Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 13:51:20 +0100 Subject: [PATCH 14/74] fix codespell --- tests/units/test_minification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index eec1beea942..41dbede7167 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -288,14 +288,14 @@ def handler_b(self): # Start with partial config (using string IDs in v2 format) existing_config: MinifyConfig = { "version": SCHEMA_VERSION, - "states": {state_path: "bU"}, # Some arbitrary minified name + "states": {state_path: "bU"}, # codespell:ignore "events": {state_path: {"handler_a": "k"}}, # Another arbitrary name } new_config = sync_minify_config(existing_config, TestState) # Existing IDs should be preserved - assert new_config["states"][state_path] == "bU" + assert new_config["states"][state_path] == "bU" # codespell:ignore assert new_config["events"][state_path]["handler_a"] == "k" # New handler should be added with next ID (k=10, so next is l=11) assert "handler_b" in new_config["events"][state_path] From bcc4b499485a0d74de7660085c952b88fc6b373d Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 23:06:36 +0100 Subject: [PATCH 15/74] add support for dynamic event handlers, streamline event handler formatting --- reflex/compiler/templates.py | 12 ++-- reflex/constants/event.py | 3 + reflex/state.py | 48 +++++++++---- reflex/utils/format.py | 13 +++- tests/units/test_minification.py | 113 +++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 20 deletions(-) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 7eccca3e6ea..5facfd73cf8 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -9,7 +9,7 @@ from reflex import constants from reflex.constants import Hooks from reflex.constants.state import CAMEL_CASE_MEMO_MARKER -from reflex.utils.format import format_state_name, json_dumps +from reflex.utils.format import format_event_handler, format_state_name, json_dumps from reflex.vars.base import VarData if TYPE_CHECKING: @@ -284,8 +284,10 @@ def context_template( # Compute dynamic state names that respect minification settings main_state_name = State.get_name() - on_load_internal = f"{OnLoadInternalState.get_name()}.on_load_internal" - update_vars_internal = f"{UpdateVarsInternalState.get_name()}.update_vars_internal" + on_load_internal = format_event_handler(OnLoadInternalState.on_load_internal) + update_vars_internal = format_event_handler( + UpdateVarsInternalState.update_vars_internal + ) exception_state_full = FrontendEventExceptionState.get_full_name() initial_state = initial_state or {} @@ -314,7 +316,7 @@ def context_template( if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{ internal_events.push( ReflexEvent( - '{state_name}.{update_vars_internal}', + '{update_vars_internal}', {{vars: client_storage_vars}}, ), ); @@ -322,7 +324,7 @@ def context_template( // `on_load_internal` triggers the correct on_load event(s) for the current page. // If the page does not define any on_load event, this will just set `is_hydrated = true`. - internal_events.push(ReflexEvent('{state_name}.{on_load_internal}')); + internal_events.push(ReflexEvent('{on_load_internal}')); return internal_events; }} diff --git a/reflex/constants/event.py b/reflex/constants/event.py index 6a0f71ec161..55751c4a41f 100644 --- a/reflex/constants/event.py +++ b/reflex/constants/event.py @@ -3,6 +3,9 @@ from enum import Enum from types import SimpleNamespace +# The name of the setvar event handler. +SETVAR = "setvar" + class Endpoint(Enum): """Endpoints for the reflex backend API.""" diff --git a/reflex/state.py b/reflex/state.py index 95265142727..f0187decad6 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -609,6 +609,7 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): **cls.computed_vars, } cls.event_handlers = {} + cls._event_id_to_name = {} # Setup the base vars at the class level. for name, prop in cls.base_vars.items(): @@ -651,16 +652,9 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): cls.event_handlers[name] = handler setattr(cls, name, handler) - # Build event_id registry from minify.json configuration - from reflex.minify import get_event_id, get_state_full_path, is_minify_enabled - - cls._event_id_to_name = {} - if is_minify_enabled(): - state_path = get_state_full_path(cls) - for handler_name in events: - event_id = get_event_id(state_path, handler_name) - if event_id is not None: - cls._event_id_to_name[event_id] = handler_name + # Register user-defined event handlers for minification + for handler_name in events: + cls._register_event_handler_for_minify(handler_name) # Initialize per-class var dependency tracking. cls._var_dependencies = {} @@ -673,16 +667,42 @@ def _add_event_handler( cls, name: str, fn: Callable, - ): + ) -> EventHandler: """Add an event handler dynamically to the state. Args: name: The name of the event handler. fn: The function to call when the event is triggered. + + Returns: + The created EventHandler instance. """ handler = cls._create_event_handler(fn) cls.event_handlers[name] = handler setattr(cls, name, handler) + cls._register_event_handler_for_minify(name) + return handler + + @classmethod + def _register_event_handler_for_minify(cls, handler_name: str) -> None: + """Register an event handler for minification if applicable. + + Called when an event handler is added to event_handlers dict. + Updates _event_id_to_name if minification is enabled and the handler + has a minified ID in the config. + + Args: + handler_name: The original name of the event handler. + """ + from reflex.minify import get_event_id, get_minify_config, get_state_full_path + + if get_minify_config() is None: + return + + state_path = get_state_full_path(cls) + event_id = get_event_id(state_path, handler_name) + if event_id is not None: + cls._event_id_to_name[event_id] = handler_name @staticmethod def _copy_fn(fn: Callable) -> Callable: @@ -1204,7 +1224,10 @@ def _create_event_handler( @classmethod def _create_setvar(cls): """Create the setvar method for the state.""" - cls.setvar = cls.event_handlers["setvar"] = EventHandlerSetVar(state_cls=cls) + cls.setvar = cls.event_handlers[constants.event.SETVAR] = EventHandlerSetVar( + state_cls=cls + ) + cls._register_event_handler_for_minify(constants.event.SETVAR) @classmethod def _create_setter(cls, name: str, prop: Var): @@ -1244,6 +1267,7 @@ def __call__(self, *args, **kwargs): ) cls.event_handlers[setter_name] = event_handler setattr(cls, setter_name, event_handler) + cls._register_event_handler_for_minify(setter_name) @classmethod def _set_default_value(cls, name: str, prop: Var): diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 62680716cda..5ccdb86d810 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -6,7 +6,8 @@ import json import os import re -from typing import TYPE_CHECKING, Any +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, cast from reflex import constants from reflex.constants.state import FRONTEND_EVENT_STATE @@ -439,7 +440,9 @@ def format_props(*single_props, **key_value_props) -> list[str]: ] + [(f"...{LiteralVar.create(prop)!s}") for prop in single_props] -def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: +def get_event_handler_parts( + handler: EventHandler | Callable[..., Any], +) -> tuple[str, str]: """Get the state and function name of an event handler. Args: @@ -448,9 +451,13 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: Returns: The state and function name (possibly minified based on minify.json). """ + from reflex.event import EventHandler from reflex.minify import is_minify_enabled from reflex.state import State + # Cast for type checker - at runtime this is always an EventHandler + handler = cast(EventHandler, handler) + # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") @@ -486,7 +493,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: return (state_full_name, name) -def format_event_handler(handler: EventHandler) -> str: +def format_event_handler(handler: EventHandler | Callable[..., Any]) -> str: """Format an event handler. Args: diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 41dbede7167..db9591f5386 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -419,3 +419,116 @@ def my_handler(self): # Should be the minified name directly assert event_name == "d" + + +class TestDynamicHandlerMinification: + """Tests for dynamic event handler minification (setvar, auto-setters).""" + + def test_setvar_registered_with_config(self, temp_minify_json): + """Test that setvar is registered in _event_id_to_name when config exists.""" + expected_module = "tests.units.test_minification" + expected_state_path = f"{expected_module}.State.TestStateWithSetvar" + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + expected_state_path: "b", + }, + "events": { + expected_state_path: {"setvar": "s"}, + }, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + + class TestStateWithSetvar(State): + pass + + # Verify setvar is registered for minification + assert "s" in TestStateWithSetvar._event_id_to_name + assert TestStateWithSetvar._event_id_to_name["s"] == "setvar" + + def test_auto_setter_registered_with_config(self, temp_minify_json): + """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" + expected_module = "tests.units.test_minification" + expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + expected_state_path: "b", + }, + "events": { + expected_state_path: {"set_count": "c", "setvar": "v"}, + }, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + + class TestStateWithAutoSetter(State): + count: int = 0 + + # Verify auto-setter is registered for minification + assert "c" in TestStateWithAutoSetter._event_id_to_name + assert TestStateWithAutoSetter._event_id_to_name["c"] == "set_count" + + def test_dynamic_handlers_not_registered_without_config(self, temp_minify_json): + """Test that dynamic handlers are NOT registered when no config exists.""" + # No config saved - temp_minify_json fixture ensures clean state + + class TestStateNoConfig(State): + count: int = 0 + + # Without config, _event_id_to_name should be empty + assert TestStateNoConfig._event_id_to_name == {} + + def test_add_event_handler_registered_with_config(self, temp_minify_json): + """Test that dynamically added event handlers via _add_event_handler are registered.""" + import reflex as rx + + expected_module = "tests.units.test_minification" + expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler" + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + expected_state_path: "b", + }, + "events": { + expected_state_path: {"dynamic_handler": "d", "setvar": "v"}, + }, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + + class TestStateWithDynamicHandler(State): + pass + + # Dynamically add an event handler after class creation + @rx.event + def dynamic_handler(self): + pass + + from reflex.event import EventHandler + + handler = EventHandler( + fn=dynamic_handler, + state_full_name=TestStateWithDynamicHandler.get_full_name(), + ) + TestStateWithDynamicHandler._add_event_handler("dynamic_handler", handler) + + # Verify dynamic handler is registered for minification + assert "d" in TestStateWithDynamicHandler._event_id_to_name + assert TestStateWithDynamicHandler._event_id_to_name["d"] == "dynamic_handler" From fc787e6afa79141bd27c1de0d0d87640e074aa94 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 23:18:12 +0100 Subject: [PATCH 16/74] forgot to remove this state prefix --- reflex/.templates/web/utils/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index a3a76fe60ae..2124d802d6d 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -1046,7 +1046,7 @@ export const useEventLoop = ( if (storage_to_state_map[e.key]) { const vars = {}; vars[storage_to_state_map[e.key]] = e.newValue; - const event = ReflexEvent(`${state_name}.${update_vars_internal}`, { + const event = ReflexEvent(update_vars_internal, { vars: vars, }); addEvents([event], e); From 5b6e124de6dbeb8aed18c1282f20fc0fe72bef9a Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 1 Feb 2026 23:24:13 +0100 Subject: [PATCH 17/74] more streamline with format event handler --- reflex/.templates/web/utils/state.js | 6 +++--- reflex/compiler/templates.py | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 2124d802d6d..1affca62073 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -16,7 +16,7 @@ import { initialState, onLoadInternalEvent, state_name, - exception_state_name, + handle_frontend_exception, main_state_name, update_vars_internal, } from "$/utils/context"; @@ -969,7 +969,7 @@ export const useEventLoop = ( window.onerror = function (msg, url, lineNo, columnNo, error) { addEvents([ - ReflexEvent(`${exception_state_name}.handle_frontend_exception`, { + ReflexEvent(handle_frontend_exception, { info: error.name + ": " + error.message + "\n" + error.stack, component_stack: "", }), @@ -981,7 +981,7 @@ export const useEventLoop = ( //https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events window.onunhandledrejection = function (event) { addEvents([ - ReflexEvent(`${exception_state_name}.handle_frontend_exception`, { + ReflexEvent(handle_frontend_exception, { info: event.reason?.name + ": " + diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 5facfd73cf8..452f09a4b46 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -288,7 +288,9 @@ def context_template( update_vars_internal = format_event_handler( UpdateVarsInternalState.update_vars_internal ) - exception_state_full = FrontendEventExceptionState.get_full_name() + handle_frontend_exception = format_event_handler( + FrontendEventExceptionState.handle_frontend_exception + ) initial_state = initial_state or {} state_contexts_str = "".join([ @@ -304,7 +306,7 @@ def context_template( export const update_vars_internal = "{update_vars_internal}" -export const exception_state_name = "{exception_state_full}" +export const handle_frontend_exception = "{handle_frontend_exception}" // These events are triggered on initial load and each page navigation. export const onLoadInternalEvent = () => {{ @@ -343,7 +345,7 @@ def context_template( export const update_vars_internal = undefined -export const exception_state_name = undefined +export const handle_frontend_exception = undefined export const onLoadInternalEvent = () => [] From ae477aef996d80657763d64ff5f18422fa8ca68e Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:02:10 +0100 Subject: [PATCH 18/74] allow to enable/disable state and event ID minification independently via env vars --- reflex/environment.py | 13 ++ reflex/minify.py | 42 +++++- reflex/reflex.py | 5 +- reflex/state.py | 18 ++- reflex/utils/format.py | 4 +- tests/units/test_minification.py | 223 +++++++++++++++++++++++++++++-- 6 files changed, 281 insertions(+), 24 deletions(-) diff --git a/reflex/environment.py b/reflex/environment.py index 279fc5f60c1..5424b71f281 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -478,6 +478,13 @@ class PathExistsFlag: ExistingPath = Annotated[Path, PathExistsFlag] +class MinifyMode(enum.Enum): + """Mode for minification of state/event IDs.""" + + ENABLED = "enabled" + DISABLED = "disabled" + + class PerformanceMode(enum.Enum): """Performance mode for the app.""" @@ -762,6 +769,12 @@ class EnvironmentVariables: # How long to opportunistically hold the redis lock in milliseconds (must be less than the token expiration). REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0) + # Whether to enable state ID minification (requires minify.json). + REFLEX_MINIFY_STATE: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + + # Whether to enable event ID minification (requires minify.json). + REFLEX_MINIFY_EVENTS: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + environment = EnvironmentVariables() diff --git a/reflex/minify.py b/reflex/minify.py index cc9a9acdc00..6b3fbbfedb6 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -115,12 +115,46 @@ def get_minify_config() -> MinifyConfig | None: def is_minify_enabled() -> bool: - """Check if minification is enabled. + """Check if any minification is enabled (state or event). Returns: - True if minify.json exists and is valid. + True if either state or event minification is enabled. """ - return get_minify_config() is not None + return is_state_minify_enabled() or is_event_minify_enabled() + + +@functools.cache +def is_state_minify_enabled() -> bool: + """Check if state ID minification is enabled. + + Requires both REFLEX_MINIFY_STATE=enabled and minify.json to exist. + + Returns: + True if state minification is enabled. + """ + from reflex.environment import MinifyMode, environment + + return ( + environment.REFLEX_MINIFY_STATE.get() == MinifyMode.ENABLED + and get_minify_config() is not None + ) + + +@functools.cache +def is_event_minify_enabled() -> bool: + """Check if event ID minification is enabled. + + Requires both REFLEX_MINIFY_EVENTS=enabled and minify.json to exist. + + Returns: + True if event minification is enabled. + """ + from reflex.environment import MinifyMode, environment + + return ( + environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED + and get_minify_config() is not None + ) def get_state_id(state_full_path: str) -> str | None: @@ -175,6 +209,8 @@ def clear_config_cache() -> None: This should be called after modifying minify.json programmatically. """ get_minify_config.cache_clear() + is_state_minify_enabled.cache_clear() + is_event_minify_enabled.cache_clear() # Base-54 encoding for minified names diff --git a/reflex/reflex.py b/reflex/reflex.py index f68865c2def..c5190dd847c 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -1017,9 +1017,9 @@ def minify_list(output_json: bool): from reflex.minify import ( get_event_id, + get_minify_config, get_state_full_path, get_state_id, - is_minify_enabled, ) from reflex.state import BaseState, State from reflex.utils import prerequisites @@ -1042,7 +1042,8 @@ class StateTreeData(TypedDict): # Load the user's app to register all state classes prerequisites.get_app() - minify_enabled = is_minify_enabled() + # CLI inspection always shows config contents regardless of env var settings + minify_enabled = get_minify_config() is not None def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: """Recursively build state tree data. diff --git a/reflex/state.py b/reflex/state.py index f0187decad6..a5671d398c3 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -694,9 +694,13 @@ def _register_event_handler_for_minify(cls, handler_name: str) -> None: Args: handler_name: The original name of the event handler. """ - from reflex.minify import get_event_id, get_minify_config, get_state_full_path + from reflex.minify import ( + get_event_id, + get_state_full_path, + is_event_minify_enabled, + ) - if get_minify_config() is None: + if not is_event_minify_enabled(): return state_path = get_state_full_path(cls) @@ -1030,13 +1034,17 @@ def get_name(cls) -> str: Returns: The name of the state (minified if configured in minify.json). """ - from reflex.minify import get_state_full_path, get_state_id, is_minify_enabled + from reflex.minify import ( + get_state_full_path, + get_state_id, + is_state_minify_enabled, + ) module = cls.__module__.replace(".", "___") full_name = format.to_snake_case(f"{module}___{cls.__name__}") - # If minification is enabled, look up the state ID from minify.json - if is_minify_enabled(): + # If state minification is enabled, look up the state ID from minify.json + if is_state_minify_enabled(): state_path = get_state_full_path(cls) state_id = get_state_id(state_path) if state_id is not None: diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 5ccdb86d810..4d5804dc8cb 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -452,7 +452,7 @@ def get_event_handler_parts( The state and function name (possibly minified based on minify.json). """ from reflex.event import EventHandler - from reflex.minify import is_minify_enabled + from reflex.minify import is_event_minify_enabled from reflex.state import State # Cast for type checker - at runtime this is always an EventHandler @@ -477,7 +477,7 @@ def get_event_handler_parts( # Check for event_id minification from minify.json # The state class stores its event ID mapping in _event_id_to_name # where key is minified_name and value is original_handler_name - if is_minify_enabled(): + if is_event_minify_enabled(): try: # Get the state class using the path state_cls = State.get_class_substate(state_full_name) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index db9591f5386..4d0eedb4bff 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -16,7 +16,9 @@ get_state_full_path, get_state_id, int_to_minified_name, + is_event_minify_enabled, is_minify_enabled, + is_state_minify_enabled, minified_name_to_int, save_minify_config, sync_minify_config, @@ -135,8 +137,10 @@ def test_no_config_returns_none(self, temp_minify_json): assert get_state_id("any.path") is None assert get_event_id("any.path", "handler") is None - def test_save_and_load_config(self, temp_minify_json): + def test_save_and_load_config(self, temp_minify_json, monkeypatch): """Test saving and loading a config.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -151,8 +155,9 @@ def test_save_and_load_config(self, temp_minify_json): assert get_state_id("test.module.MyState") == "a" assert get_event_id("test.module.MyState", "handler") == "a" - def test_invalid_version_raises(self, temp_minify_json): + def test_invalid_version_raises(self, temp_minify_json, monkeypatch): """Test that invalid version raises ValueError.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") config = {"version": 999, "states": {}, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -161,10 +166,11 @@ def test_invalid_version_raises(self, temp_minify_json): clear_config_cache() with pytest.raises(ValueError, match=r"Unsupported.*version"): - is_minify_enabled() + is_state_minify_enabled() - def test_missing_states_raises(self, temp_minify_json): + def test_missing_states_raises(self, temp_minify_json, monkeypatch): """Test that missing 'states' key raises ValueError.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") config = {"version": SCHEMA_VERSION, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -173,7 +179,7 @@ def test_missing_states_raises(self, temp_minify_json): clear_config_cache() with pytest.raises(ValueError, match="'states' must be"): - is_minify_enabled() + is_state_minify_enabled() class TestGenerateMinifyConfig: @@ -319,8 +325,9 @@ class TestState(BaseState): # Should be the full name (snake_case module___class) assert "test_state" in name.lower() - def test_state_uses_minified_name_with_config(self, temp_minify_json): - """Test that states use minified names when minify.json exists.""" + def test_state_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): + """Test that states use minified names when minify.json exists and env var is enabled.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") class TestState(BaseState): pass @@ -340,6 +347,31 @@ class TestState(BaseState): # Should be the minified name directly assert name == "f" + def test_state_uses_full_name_when_env_disabled( + self, temp_minify_json, monkeypatch + ): + """Test that states use full names when env var is disabled even with minify.json.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "disabled") + + class TestState(BaseState): + pass + + state_path = get_state_full_path(TestState) + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {state_path: "f"}, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + TestState.get_name.cache_clear() + + name = TestState.get_name() + + # Should be the full name, not minified + assert name != "f" + assert "test_state" in name.lower() + class TestEventMinification: """Tests for event handler name minification with minify.json.""" @@ -361,11 +393,13 @@ def my_handler(self): # Should use full name assert event_name == "my_handler" - def test_event_uses_minified_name_with_config(self, temp_minify_json): - """Test that event handlers use minified names when minify.json exists.""" + def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): + """Test that event handlers use minified names when minify.json exists and env var is enabled.""" import reflex as rx from reflex.utils.format import get_event_handler_parts + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + # First, set up the config BEFORE creating the state class # The event_id_to_name registry is built during __init_subclass__ # so the config must exist before the class is defined @@ -420,12 +454,57 @@ def my_handler(self): # Should be the minified name directly assert event_name == "d" + def test_event_uses_full_name_when_env_disabled( + self, temp_minify_json, monkeypatch + ): + """Test that event handlers use full names when env var is disabled even with minify.json.""" + import reflex as rx + from reflex.utils.format import get_event_handler_parts + + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "disabled") + + expected_module = "tests.units.test_minification" + expected_state_path = ( + f"{expected_module}.State.TestStateWithMinifiedEventDisabled" + ) + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + expected_state_path: "b", + }, + "events": { + expected_state_path: {"my_handler": "d"}, + }, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + + class TestStateWithMinifiedEventDisabled(State): + @rx.event + def my_handler(self): + pass + + # The state's _event_id_to_name should be empty when env var is disabled + assert TestStateWithMinifiedEventDisabled._event_id_to_name == {} + + handler = TestStateWithMinifiedEventDisabled.event_handlers["my_handler"] + _, event_name = get_event_handler_parts(handler) + + # Should use full name + assert event_name == "my_handler" + class TestDynamicHandlerMinification: """Tests for dynamic event handler minification (setvar, auto-setters).""" - def test_setvar_registered_with_config(self, temp_minify_json): + def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): """Test that setvar is registered in _event_id_to_name when config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithSetvar" @@ -452,8 +531,9 @@ class TestStateWithSetvar(State): assert "s" in TestStateWithSetvar._event_id_to_name assert TestStateWithSetvar._event_id_to_name["s"] == "setvar" - def test_auto_setter_registered_with_config(self, temp_minify_json): + def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" @@ -490,10 +570,13 @@ class TestStateNoConfig(State): # Without config, _event_id_to_name should be empty assert TestStateNoConfig._event_id_to_name == {} - def test_add_event_handler_registered_with_config(self, temp_minify_json): + def test_add_event_handler_registered_with_config( + self, temp_minify_json, monkeypatch + ): """Test that dynamically added event handlers via _add_event_handler are registered.""" import reflex as rx + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler" @@ -532,3 +615,119 @@ def dynamic_handler(self): # Verify dynamic handler is registered for minification assert "d" in TestStateWithDynamicHandler._event_id_to_name assert TestStateWithDynamicHandler._event_id_to_name["d"] == "dynamic_handler" + + +class TestMinifyModeEnvVars: + """Tests for REFLEX_MINIFY_STATE and REFLEX_MINIFY_EVENTS env vars.""" + + def test_state_minify_disabled_by_default(self, temp_minify_json): + """Test that state minification is disabled by default.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + + assert is_state_minify_enabled() is False + + def test_event_minify_disabled_by_default(self, temp_minify_json): + """Test that event minification is disabled by default.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) + clear_config_cache() + + assert is_event_minify_enabled() is False + + def test_state_minify_enabled_with_env_and_config( + self, temp_minify_json, monkeypatch + ): + """Test that state minification is enabled when env var is enabled and config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + + assert is_state_minify_enabled() is True + + def test_event_minify_enabled_with_env_and_config( + self, temp_minify_json, monkeypatch + ): + """Test that event minification is enabled when env var is enabled and config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) + clear_config_cache() + + assert is_event_minify_enabled() is True + + def test_state_minify_disabled_without_config(self, temp_minify_json, monkeypatch): + """Test that state minification is disabled when env var is enabled but no config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + clear_config_cache() + + assert is_state_minify_enabled() is False + + def test_event_minify_disabled_without_config(self, temp_minify_json, monkeypatch): + """Test that event minification is disabled when env var is enabled but no config exists.""" + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + clear_config_cache() + + assert is_event_minify_enabled() is False + + def test_independent_state_and_event_toggles(self, temp_minify_json, monkeypatch): + """Test that state and event minification can be toggled independently.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "disabled") + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) + clear_config_cache() + + assert is_state_minify_enabled() is True + assert is_event_minify_enabled() is False + assert is_minify_enabled() is True + + def test_is_minify_enabled_true_when_either_enabled( + self, temp_minify_json, monkeypatch + ): + """Test that is_minify_enabled returns True when either state or event is enabled.""" + monkeypatch.setenv("REFLEX_MINIFY_STATE", "disabled") + monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) + clear_config_cache() + + assert is_minify_enabled() is True + + def test_is_minify_enabled_false_when_both_disabled(self, temp_minify_json): + """Test that is_minify_enabled returns False when both are disabled (default).""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {"test.module.MyState": {"handler": "a"}}, + } + save_minify_config(config) + clear_config_cache() + + assert is_minify_enabled() is False From e13073b844c11d69362e4781b61c755d261ea25e Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:20:32 +0100 Subject: [PATCH 19/74] fix: prevent parent-child minified name collision in substate resolution When a parent and child state have the same minified name, substate resolution can fail because the leading segment is stripped incorrectly. This change adds a flag to skip stripping only on the initial recursive call, ensuring correct resolution even in name collision scenarios. --- reflex/state.py | 22 +++++-- tests/units/test_minification.py | 101 +++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index a5671d398c3..e3b96dec216 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1068,11 +1068,17 @@ def get_full_name(cls) -> str: @classmethod @functools.lru_cache - def get_class_substate(cls, path: Sequence[str] | str) -> type[BaseState]: + def get_class_substate( + cls, path: Sequence[str] | str, _skip_self: bool = True + ) -> type[BaseState]: """Get the class substate. Args: path: The path to the substate. + _skip_self: If True, strip the leading segment when it matches this + state's name. Only the initial (root) call should use True; + recursive calls pass False so that a child whose minified name + collides with its parent is resolved correctly. Returns: The class substate. @@ -1085,13 +1091,13 @@ def get_class_substate(cls, path: Sequence[str] | str) -> type[BaseState]: if len(path) == 0: return cls - if path[0] == cls.get_name(): + if _skip_self and path[0] == cls.get_name(): if len(path) == 1: return cls path = path[1:] for substate in cls.get_substates(): if path[0] == substate.get_name(): - return substate.get_class_substate(path[1:]) + return substate.get_class_substate(path[1:], _skip_self=False) msg = f"Invalid path: {path}" raise ValueError(msg) @@ -1605,11 +1611,15 @@ def _reset_client_storage(self): for substate in self.substates.values(): substate._reset_client_storage() - def get_substate(self, path: Sequence[str]) -> BaseState: + def get_substate(self, path: Sequence[str], _skip_self: bool = True) -> BaseState: """Get the substate. Args: path: The path to the substate. + _skip_self: If True, strip the leading segment when it matches this + state's name. Only the initial (root) call should use True; + recursive calls pass False so that a child whose minified name + collides with its parent is resolved correctly. Returns: The substate. @@ -1619,14 +1629,14 @@ def get_substate(self, path: Sequence[str]) -> BaseState: """ if len(path) == 0: return self - if path[0] == self.get_name(): + if _skip_self and path[0] == self.get_name(): if len(path) == 1: return self path = path[1:] if path[0] not in self.substates: msg = f"Invalid path: {path}" raise ValueError(msg) - return self.substates[path[0]].get_substate(path[1:]) + return self.substates[path[0]].get_substate(path[1:], _skip_self=False) @classmethod def _get_potentially_dirty_states(cls) -> set[type[BaseState]]: diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 4d0eedb4bff..b8954760a5a 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -731,3 +731,104 @@ def test_is_minify_enabled_false_when_both_disabled(self, temp_minify_json): clear_config_cache() assert is_minify_enabled() is False + + +class TestMinifiedNameCollision: + """Tests for parent-child minified name collision in substate resolution.""" + + def test_get_class_substate_with_parent_child_name_collision( + self, temp_minify_json, monkeypatch + ): + """Test that get_class_substate resolves correctly when parent and child + share the same minified name (IDs are only sibling-unique). + """ + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + + # Build a hierarchy: State -> ParentState -> ChildState + # where ParentState and ChildState both get minified name "b" + + class ParentState(State): + pass + + class ChildState(ParentState): + pass + + parent_path = get_state_full_path(ParentState) + child_path = get_state_full_path(ChildState) + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + parent_path: "b", + child_path: "b", # Same minified name as parent + }, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + ParentState.get_name.cache_clear() + ParentState.get_full_name.cache_clear() + ChildState.get_name.cache_clear() + ChildState.get_full_name.cache_clear() + + # Verify both get the same minified name + assert ParentState.get_name() == "b" + assert ChildState.get_name() == "b" + + # Full path should be a.b.b + assert ChildState.get_full_name() == "a.b.b" + + # get_class_substate should resolve a.b.b to ChildState, not ParentState + resolved = State.get_class_substate("a.b.b") + assert resolved is ChildState + + def test_get_substate_with_parent_child_name_collision( + self, temp_minify_json, monkeypatch + ): + """Test that get_substate (instance method) resolves correctly when parent + and child share the same minified name. + """ + import reflex as rx + + monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + + class ParentState2(State): + pass + + class ChildState2(ParentState2): + @rx.event + def my_handler(self): + pass + + parent_path = get_state_full_path(ParentState2) + child_path = get_state_full_path(ChildState2) + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + parent_path: "b", + child_path: "b", # Same minified name as parent + }, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + State.get_name.cache_clear() + State.get_full_name.cache_clear() + State.get_class_substate.cache_clear() + ParentState2.get_name.cache_clear() + ParentState2.get_full_name.cache_clear() + ChildState2.get_name.cache_clear() + ChildState2.get_full_name.cache_clear() + + # Create a state instance tree + root = State(_reflex_internal_init=True) # type: ignore[call-arg] + + # Instance get_substate should resolve a.b.b to ChildState2 + resolved = root.get_substate(["a", "b", "b"]) + assert type(resolved) is ChildState2 From d669571abc11b703b277b2e59af496182e32e277 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:42:30 +0100 Subject: [PATCH 20/74] fix: consistent plural naming for state minification env vars --- reflex/environment.py | 2 +- reflex/minify.py | 2 +- tests/units/test_minification.py | 45 ++++++++++++++++---------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/reflex/environment.py b/reflex/environment.py index 5424b71f281..1b074b47b26 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -770,7 +770,7 @@ class EnvironmentVariables: REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0) # Whether to enable state ID minification (requires minify.json). - REFLEX_MINIFY_STATE: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) + REFLEX_MINIFY_STATES: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) # Whether to enable event ID minification (requires minify.json). REFLEX_MINIFY_EVENTS: EnvVar[MinifyMode] = env_var(MinifyMode.DISABLED) diff --git a/reflex/minify.py b/reflex/minify.py index 6b3fbbfedb6..7794d6198c3 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -135,7 +135,7 @@ def is_state_minify_enabled() -> bool: from reflex.environment import MinifyMode, environment return ( - environment.REFLEX_MINIFY_STATE.get() == MinifyMode.ENABLED + environment.REFLEX_MINIFY_STATES.get() == MinifyMode.ENABLED and get_minify_config() is not None ) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index b8954760a5a..197b2942441 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -6,6 +6,7 @@ import pytest +from reflex.environment import environment from reflex.minify import ( MINIFY_JSON, SCHEMA_VERSION, @@ -139,8 +140,8 @@ def test_no_config_returns_none(self, temp_minify_json): def test_save_and_load_config(self, temp_minify_json, monkeypatch): """Test saving and loading a config.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -157,7 +158,7 @@ def test_save_and_load_config(self, temp_minify_json, monkeypatch): def test_invalid_version_raises(self, temp_minify_json, monkeypatch): """Test that invalid version raises ValueError.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") config = {"version": 999, "states": {}, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -170,7 +171,7 @@ def test_invalid_version_raises(self, temp_minify_json, monkeypatch): def test_missing_states_raises(self, temp_minify_json, monkeypatch): """Test that missing 'states' key raises ValueError.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") config = {"version": SCHEMA_VERSION, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -327,7 +328,7 @@ class TestState(BaseState): def test_state_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): """Test that states use minified names when minify.json exists and env var is enabled.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") class TestState(BaseState): pass @@ -351,7 +352,7 @@ def test_state_uses_full_name_when_env_disabled( self, temp_minify_json, monkeypatch ): """Test that states use full names when env var is disabled even with minify.json.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "disabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "disabled") class TestState(BaseState): pass @@ -398,7 +399,7 @@ def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatc import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") # First, set up the config BEFORE creating the state class # The event_id_to_name registry is built during __init_subclass__ @@ -461,7 +462,7 @@ def test_event_uses_full_name_when_env_disabled( import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "disabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "disabled") expected_module = "tests.units.test_minification" expected_state_path = ( @@ -504,7 +505,7 @@ class TestDynamicHandlerMinification: def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): """Test that setvar is registered in _event_id_to_name when config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithSetvar" @@ -533,7 +534,7 @@ class TestStateWithSetvar(State): def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" @@ -576,7 +577,7 @@ def test_add_event_handler_registered_with_config( """Test that dynamically added event handlers via _add_event_handler are registered.""" import reflex as rx - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler" @@ -618,7 +619,7 @@ def dynamic_handler(self): class TestMinifyModeEnvVars: - """Tests for REFLEX_MINIFY_STATE and REFLEX_MINIFY_EVENTS env vars.""" + """Tests for REFLEX_MINIFY_STATES and REFLEX_MINIFY_EVENTS env vars.""" def test_state_minify_disabled_by_default(self, temp_minify_json): """Test that state minification is disabled by default.""" @@ -648,7 +649,7 @@ def test_state_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that state minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -663,7 +664,7 @@ def test_event_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that event minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {}, @@ -676,22 +677,22 @@ def test_event_minify_enabled_with_env_and_config( def test_state_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that state minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") clear_config_cache() assert is_state_minify_enabled() is False def test_event_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that event minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") clear_config_cache() assert is_event_minify_enabled() is False def test_independent_state_and_event_toggles(self, temp_minify_json, monkeypatch): """Test that state and event minification can be toggled independently.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "disabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "disabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -708,8 +709,8 @@ def test_is_minify_enabled_true_when_either_enabled( self, temp_minify_json, monkeypatch ): """Test that is_minify_enabled returns True when either state or event is enabled.""" - monkeypatch.setenv("REFLEX_MINIFY_STATE", "disabled") - monkeypatch.setenv("REFLEX_MINIFY_EVENTS", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "disabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {}, @@ -742,7 +743,7 @@ def test_get_class_substate_with_parent_child_name_collision( """Test that get_class_substate resolves correctly when parent and child share the same minified name (IDs are only sibling-unique). """ - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") # Build a hierarchy: State -> ParentState -> ChildState # where ParentState and ChildState both get minified name "b" @@ -794,7 +795,7 @@ def test_get_substate_with_parent_child_name_collision( """ import reflex as rx - monkeypatch.setenv("REFLEX_MINIFY_STATE", "enabled") + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") class ParentState2(State): pass From ed085c064ed8515efd706f5affa5e8a61ffcfa1a Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:45:27 +0100 Subject: [PATCH 21/74] make integration tests use our typed env helpers --- tests/integration/test_minification.py | 9 ++- tests/units/test_minification.py | 86 +++++++++++++++++++------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 8a4d057d85e..ad2e94e5684 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -9,6 +9,7 @@ import pytest from selenium.webdriver.common.by import By +from reflex.environment import MinifyMode, environment from reflex.minify import MINIFY_JSON, clear_config_cache, int_to_minified_name from reflex.testing import AppHarness @@ -119,16 +120,22 @@ def minify_disabled_app( def minify_enabled_app( app_harness_env: type[AppHarness], tmp_path_factory: pytest.TempPathFactory, + monkeypatch: pytest.MonkeyPatch, ) -> Generator[AppHarness, None, None]: - """Start app WITH minify.json (minified names). + """Start app WITH minify.json and env vars enabled (minified names). Args: app_harness_env: AppHarness or AppHarnessProd tmp_path_factory: pytest tmp_path_factory fixture + monkeypatch: pytest monkeypatch fixture Yields: Running AppHarness instance """ + # Enable minification via env vars (required in addition to minify.json) + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value) + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value) + # Clear minify config cache to ensure clean state clear_config_cache() diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 197b2942441..ca40d2a0be3 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -6,7 +6,7 @@ import pytest -from reflex.environment import environment +from reflex.environment import MinifyMode, environment from reflex.minify import ( MINIFY_JSON, SCHEMA_VERSION, @@ -140,8 +140,12 @@ def test_no_config_returns_none(self, temp_minify_json): def test_save_and_load_config(self, temp_minify_json, monkeypatch): """Test saving and loading a config.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -158,7 +162,9 @@ def test_save_and_load_config(self, temp_minify_json, monkeypatch): def test_invalid_version_raises(self, temp_minify_json, monkeypatch): """Test that invalid version raises ValueError.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) config = {"version": 999, "states": {}, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -171,7 +177,9 @@ def test_invalid_version_raises(self, temp_minify_json, monkeypatch): def test_missing_states_raises(self, temp_minify_json, monkeypatch): """Test that missing 'states' key raises ValueError.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) config = {"version": SCHEMA_VERSION, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -328,7 +336,9 @@ class TestState(BaseState): def test_state_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): """Test that states use minified names when minify.json exists and env var is enabled.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) class TestState(BaseState): pass @@ -352,7 +362,9 @@ def test_state_uses_full_name_when_env_disabled( self, temp_minify_json, monkeypatch ): """Test that states use full names when env var is disabled even with minify.json.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "disabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.DISABLED.value + ) class TestState(BaseState): pass @@ -399,7 +411,9 @@ def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatc import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) # First, set up the config BEFORE creating the state class # The event_id_to_name registry is built during __init_subclass__ @@ -462,7 +476,9 @@ def test_event_uses_full_name_when_env_disabled( import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "disabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.DISABLED.value + ) expected_module = "tests.units.test_minification" expected_state_path = ( @@ -505,7 +521,9 @@ class TestDynamicHandlerMinification: def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): """Test that setvar is registered in _event_id_to_name when config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithSetvar" @@ -534,7 +552,9 @@ class TestStateWithSetvar(State): def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" @@ -577,7 +597,9 @@ def test_add_event_handler_registered_with_config( """Test that dynamically added event handlers via _add_event_handler are registered.""" import reflex as rx - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler" @@ -649,7 +671,9 @@ def test_state_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that state minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -664,7 +688,9 @@ def test_event_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that event minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {}, @@ -677,22 +703,30 @@ def test_event_minify_enabled_with_env_and_config( def test_state_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that state minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) clear_config_cache() assert is_state_minify_enabled() is False def test_event_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that event minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) clear_config_cache() assert is_event_minify_enabled() is False def test_independent_state_and_event_toggles(self, temp_minify_json, monkeypatch): """Test that state and event minification can be toggled independently.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "disabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.DISABLED.value + ) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {"test.module.MyState": "a"}, @@ -709,8 +743,12 @@ def test_is_minify_enabled_true_when_either_enabled( self, temp_minify_json, monkeypatch ): """Test that is_minify_enabled returns True when either state or event is enabled.""" - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "disabled") - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.DISABLED.value + ) + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {}, @@ -743,7 +781,9 @@ def test_get_class_substate_with_parent_child_name_collision( """Test that get_class_substate resolves correctly when parent and child share the same minified name (IDs are only sibling-unique). """ - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) # Build a hierarchy: State -> ParentState -> ChildState # where ParentState and ChildState both get minified name "b" @@ -795,7 +835,9 @@ def test_get_substate_with_parent_child_name_collision( """ import reflex as rx - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, "enabled") + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) class ParentState2(State): pass From b2c2ff064fdaffd58d3fe5c361930d66e5c8c5a0 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:47:55 +0100 Subject: [PATCH 22/74] prefer assert isinstance over cast. --- reflex/utils/format.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 4d5804dc8cb..64289a7c65d 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -7,7 +7,7 @@ import os import re from collections.abc import Callable -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from reflex import constants from reflex.constants.state import FRONTEND_EVENT_STATE @@ -455,8 +455,9 @@ def get_event_handler_parts( from reflex.minify import is_event_minify_enabled from reflex.state import State - # Cast for type checker - at runtime this is always an EventHandler - handler = cast(EventHandler, handler) + assert isinstance(handler, EventHandler), ( + f"Expected EventHandler, got {type(handler)}" + ) # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") From 8f44d49b86adaa83b040335fcef523fa4e843e84 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:51:00 +0100 Subject: [PATCH 23/74] drop old state_id parameter from BaseStateMeta --- reflex/vars/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 549b4896d57..8a8f0e754d1 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -3540,7 +3540,6 @@ def __new__( bases: tuple[type], namespace: dict[str, Any], mixin: bool = False, - state_id: int | None = None, ) -> type: """Create a new class. @@ -3549,7 +3548,6 @@ def __new__( bases: The bases of the class. namespace: The namespace of the class. mixin: Whether the class is a mixin and should not be instantiated. - state_id: Explicit state ID for minified state names. Returns: The new class. @@ -3650,9 +3648,6 @@ def __new__( namespace["__inherited_fields__"] = inherited_fields namespace["__fields__"] = inherited_fields | own_fields namespace["_mixin"] = mixin - # Pass state_id to __init_subclass__ if provided (for BaseState subclasses) - if state_id is not None: - return super().__new__(cls, name, bases, namespace, state_id=state_id) return super().__new__(cls, name, bases, namespace) From cf7b614d9fc1dc0e3b45211132d12506c95436ae Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 21:51:59 +0100 Subject: [PATCH 24/74] fix: drop unused EVENT_ID_MARKER constant --- reflex/event.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 825370cfa2f..ff75e3bd3cb 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -89,7 +89,6 @@ def substate_token(self) -> str: _EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} BACKGROUND_TASK_MARKER = "_reflex_background_task" -EVENT_ID_MARKER = "_rx_event_id" EVENT_ACTIONS_MARKER = "_rx_event_actions" @@ -2313,7 +2312,6 @@ class EventNamespace: # Constants BACKGROUND_TASK_MARKER = BACKGROUND_TASK_MARKER - EVENT_ID_MARKER = EVENT_ID_MARKER EVENT_ACTIONS_MARKER = EVENT_ACTIONS_MARKER _EVENT_FIELDS = _EVENT_FIELDS FORM_DATA = FORM_DATA From 3074c84e5badad414a8cb5e6993521925dcadbaa Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 4 Feb 2026 22:35:05 +0100 Subject: [PATCH 25/74] fix: correctly count events in minification cli --- reflex/reflex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index c5190dd847c..6800f5243c4 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -881,7 +881,7 @@ def minify_init(): save_minify_config(config) num_states = len(config["states"]) - num_events = len(config["events"]) + num_events = sum(len(handlers) for handlers in config["events"].values()) console.log( f"Created {MINIFY_JSON} with {num_states} states and {num_events} events." ) @@ -931,7 +931,7 @@ def minify_sync(reassign_deleted: bool, prune: bool): raise SystemExit(1) old_states = len(existing_config["states"]) - old_events = len(existing_config["events"]) + old_events = sum(len(handlers) for handlers in existing_config["events"].values()) # Sync the configuration new_config = sync_minify_config( @@ -940,7 +940,7 @@ def minify_sync(reassign_deleted: bool, prune: bool): save_minify_config(new_config) new_states = len(new_config["states"]) - new_events = len(new_config["events"]) + new_events = sum(len(handlers) for handlers in new_config["events"].values()) console.log(f"Updated {MINIFY_JSON}:") console.log(f" States: {old_states} -> {new_states}") From ca73b45a597c2e09844f1d6d5347d926ed8423da Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 6 Feb 2026 01:22:09 +0100 Subject: [PATCH 26/74] fix: adjust minify lookup cli for new minify.json, add tests --- reflex/reflex.py | 89 +++++++++++++------ tests/units/test_minification.py | 145 +++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 25 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 6800f5243c4..64cf161701a 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -1160,49 +1160,88 @@ def minify_lookup(output_json: bool, minified_path: str): Walks the state tree from the root to resolve each segment. """ - from reflex.minify import get_state_full_path, get_state_id - from reflex.state import State + from reflex.minify import MINIFY_JSON, get_minify_config, get_state_full_path + from reflex.state import BaseState, State from reflex.utils import prerequisites # Load the user's app to register all state classes prerequisites.get_app() - try: - State.get_class_substate(minified_path) - except ValueError: - msg = f"No state found for path: {minified_path}" - console.error(msg) - raise ValueError(msg) from None + config = get_minify_config() + if config is None: + console.error( + f"{MINIFY_JSON} not found. Run 'reflex minify init' to create it." + ) + raise SystemExit(1) + + def collect_states( + state_cls: type[BaseState], + ) -> list[type[BaseState]]: + """Recursively collect all states. + + Args: + state_cls: The state class to start from. - # Build info for each ancestor segment + Returns: + List of all state classes in the hierarchy. + """ + result = [state_cls] + for sub in state_cls.class_subclasses: + result.extend(collect_states(sub)) + return result + + # Build lookup: full_path -> (state_class, minified_id) + all_states = collect_states(State) + path_to_info: dict[str, tuple[type[BaseState], str | None]] = {} + for state_cls in all_states: + full_path = get_state_full_path(state_cls) + minified_id = config["states"].get(full_path) + path_to_info[full_path] = (state_cls, minified_id) + + # Walk the minified path parts = minified_path.split(".") result_parts = [] current = State - state_path = get_state_full_path(current) - state_id = get_state_id(state_path) - result_parts.append({ - "minified": parts[0], - "state_id": state_id, - "module": current.__module__, - "class": current.__name__, - "full_path": state_path, - }) - for part in parts[1:]: - current = current.get_class_substate(part) - state_path = get_state_full_path(current) - state_id = get_state_id(state_path) + + for i, part in enumerate(parts): + # Find state whose minified ID matches 'part' + found = None + if i == 0: + # First segment should match root state + state_path = get_state_full_path(current) + _, state_id = path_to_info.get(state_path, (None, None)) + if state_id == part: + found = current + else: + # Find among children of current + for child in current.class_subclasses: + child_path = get_state_full_path(child) + _, child_id = path_to_info.get(child_path, (None, None)) + if child_id == part: + found = child + break + + if found is None: + console.error( + f"No state found for minified segment '{part}' in path '{minified_path}'" + ) + raise SystemExit(1) + + state_path = get_state_full_path(found) + _, state_id = path_to_info.get(state_path, (None, None)) result_parts.append({ "minified": part, "state_id": state_id, - "module": current.__module__, - "class": current.__name__, + "module": found.__module__, + "class": found.__name__, "full_path": state_path, }) + current = found if output_json: import json - console.log(json.dumps(result_parts, indent=2)) + click.echo(json.dumps(result_parts, indent=2)) else: # Simple output: module.ClassName for each part for info in result_parts: diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index ca40d2a0be3..a941d175bc8 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -875,3 +875,148 @@ def my_handler(self): # Instance get_substate should resolve a.b.b to ChildState2 resolved = root.get_substate(["a", "b", "b"]) assert type(resolved) is ChildState2 + + +class TestMinifyLookupCLI: + """Tests for the 'reflex minify lookup' CLI command.""" + + def test_lookup_resolves_minified_path(self, temp_minify_json, monkeypatch): + """Test that lookup resolves a minified path to full state info.""" + from unittest import mock + + from click.testing import CliRunner + + from reflex.reflex import cli + from reflex.utils import prerequisites + + # Create test states + class AppState(State): + pass + + class ChildState(AppState): + pass + + app_state_path = get_state_full_path(AppState) + child_state_path = get_state_full_path(ChildState) + + # Create minify.json with known mappings + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + app_state_path: "b", + child_state_path: "c", + }, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + + # Mock prerequisites.get_app to avoid needing a real app + app_module_mock = mock.Mock() + monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + + runner = CliRunner() + result = runner.invoke(cli, ["minify", "lookup", "a.b.c"]) + + assert result.exit_code == 0, result.output + # Output should include the module and class names + assert "State" in result.output + assert "AppState" in result.output + assert "ChildState" in result.output + + def test_lookup_fails_without_minify_json(self, temp_minify_json, monkeypatch): + """Test that lookup fails gracefully when minify.json is missing.""" + from unittest import mock + + from click.testing import CliRunner + + from reflex.reflex import cli + from reflex.utils import prerequisites + + # Mock prerequisites.get_app + app_module_mock = mock.Mock() + monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + + # Don't create minify.json + clear_config_cache() + + runner = CliRunner() + result = runner.invoke(cli, ["minify", "lookup", "a.b"]) + + assert result.exit_code == 1 + assert "minify.json not found" in result.output + + def test_lookup_fails_for_invalid_path(self, temp_minify_json, monkeypatch): + """Test that lookup fails for non-existent minified path.""" + from unittest import mock + + from click.testing import CliRunner + + from reflex.reflex import cli + from reflex.utils import prerequisites + + # Create minify.json with only root state + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + }, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + + # Mock prerequisites.get_app + app_module_mock = mock.Mock() + monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + + runner = CliRunner() + # Try to lookup a path that doesn't exist + result = runner.invoke(cli, ["minify", "lookup", "a.xyz"]) + + assert result.exit_code == 1 + assert "No state found" in result.output + + def test_lookup_with_json_output(self, temp_minify_json, monkeypatch): + """Test that lookup with --json flag outputs valid JSON.""" + from unittest import mock + + from click.testing import CliRunner + + from reflex.reflex import cli + from reflex.utils import prerequisites + + # Create test state + class JsonTestState(State): + pass + + state_path = get_state_full_path(JsonTestState) + + # Create minify.json + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + state_path: "b", + }, + "events": {}, + } + save_minify_config(config) + clear_config_cache() + + # Mock prerequisites.get_app + app_module_mock = mock.Mock() + monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + + runner = CliRunner() + result = runner.invoke(cli, ["minify", "lookup", "--json", "a.b"]) + + assert result.exit_code == 0, result.output + # Parse output as JSON to verify it's valid + output_data = json.loads(result.output) + assert isinstance(output_data, list) + assert len(output_data) == 2 # Root state + JsonTestState + assert output_data[0]["class"] == "State" + assert output_data[1]["class"] == "JsonTestState" + assert output_data[1]["state_id"] == "b" From d5f159ca8ae2ef81e5c2e06c623fee4a165bafc8 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 6 Feb 2026 01:58:31 +0100 Subject: [PATCH 27/74] fix: reflex minify validate - warn and exit 1 if missing entries --- reflex/reflex.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 64cf161701a..f54ccbdce2e 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -995,12 +995,11 @@ def minify_validate(): if missing: console.info("Missing entries (in code but not in config):") for entry in missing: - console.info(f" - {entry}") + console.warn(f" - {entry}") - if not errors and not warnings and not missing: - console.log(f"{MINIFY_JSON} is valid and up-to-date.") - elif errors: + if errors: raise SystemExit(1) + console.log(f"{MINIFY_JSON} is valid and up-to-date.") @minify.command(name="list") From f964c7aa78b41cfcba64c9c0f4a2e0d051f99aa1 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 7 Feb 2026 21:47:51 +0100 Subject: [PATCH 28/74] fix: use get_event_handler in upload --- reflex/app.py | 21 ++++++++------------- reflex/state.py | 17 ++++++++--------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4ff412ef863..6245a9f0d1d 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -77,13 +77,13 @@ from reflex.event import ( _EVENT_FIELDS, Event, - EventHandler, EventSpec, EventType, IndividualEventType, get_hydrate_event, noop, ) +from reflex.istate.proxy import StateProxy from reflex.page import DECORATED_PAGES from reflex.route import ( get_route_args, @@ -1619,6 +1619,8 @@ def _process_background( if not handler.is_background: return None + substate = StateProxy(substate) + async def _coro(): """Coroutine to process the event and emit updates inside an asyncio.Task. @@ -1934,21 +1936,14 @@ async def upload_file(request: Request): substate_token = _substate_key(token, handler.rpartition(".")[0]) state = await app.state_manager.get_state(substate_token) - # get the current session ID - # get the current state(parent state/substate) - path = handler.split(".")[:-1] - current_state = state.get_substate(path) handler_upload_param = () - # get handler function - func = getattr(type(current_state), handler.split(".")[-1]) + _current_state, event_handler = state._get_event_handler(handler) - # check if there exists any handler args with annotation, list[UploadFile] - if isinstance(func, EventHandler): - if func.is_background: - msg = f"@rx.event(background=True) is not supported for upload handler `{handler}`." - raise UploadTypeError(msg) - func = func.fn + if event_handler.is_background: + msg = f"@rx.event(background=True) is not supported for upload handler `{handler}`." + raise UploadTypeError(msg) + func = event_handler.fn if isinstance(func, functools.partial): func = func.func for k, v in get_type_hints(func).items(): diff --git a/reflex/state.py b/reflex/state.py index e3b96dec216..22c420ca49a 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1795,13 +1795,11 @@ def _get_original_event_name(cls, minified_name: str) -> str | None: # Direct lookup: _event_id_to_name maps minified_name -> original_name return cls._event_id_to_name.get(minified_name) - def _get_event_handler( - self, event: Event - ) -> tuple[BaseState | StateProxy, EventHandler]: + def _get_event_handler(self, event: Event | str) -> tuple[BaseState, EventHandler]: """Get the event handler for the given event. Args: - event: The event to get the handler for. + event: The event to get the handler for, or a dotted handler name string. Returns: @@ -1811,7 +1809,8 @@ def _get_event_handler( ValueError: If the event handler or substate is not found. """ # Get the event handler. - path = event.name.split(".") + name = event.name if isinstance(event, Event) else event + path = name.split(".") path, name = path[:-1], path[-1] substate = self.get_substate(path) if not substate: @@ -1829,10 +1828,6 @@ def _get_event_handler( msg = f"Event handler '{name}' not found in state '{type(substate).__name__}'" raise KeyError(msg) - # For background tasks, proxy the state - if handler.is_background: - substate = StateProxy(substate) - return substate, handler async def _process(self, event: Event) -> AsyncIterator[StateUpdate]: @@ -1847,6 +1842,10 @@ async def _process(self, event: Event) -> AsyncIterator[StateUpdate]: # Get the event handler. substate, handler = self._get_event_handler(event) + # For background tasks, proxy the state. + if handler.is_background: + substate = StateProxy(substate) + # Run the event generator and yield state updates. async for update in self._process_event( handler=handler, From 019e6f3cbae8a4ff50a0c5d3ff615f66abc928bb Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 12 Feb 2026 20:19:46 +0100 Subject: [PATCH 29/74] fix: avoid assertion in runtime, prefer raising TypeError --- reflex/utils/format.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 64289a7c65d..a01727476ca 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -455,9 +455,9 @@ def get_event_handler_parts( from reflex.minify import is_event_minify_enabled from reflex.state import State - assert isinstance(handler, EventHandler), ( - f"Expected EventHandler, got {type(handler)}" - ) + if not isinstance(handler, EventHandler): + msg = f"Expected EventHandler, got {type(handler)}" + raise TypeError(msg) # Get the class that defines the event handler. parts = handler.fn.__qualname__.split(".") From e2ca73eda6497e2a3de641c8564131290e63ee66 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Mon, 26 Jan 2026 17:39:07 -0800 Subject: [PATCH 30/74] bump packaging upper-bound to <27 (#6102) allow newer version of packaging to avoid conflict with hatchling build system requirements --- pyproject.toml | 2 +- uv.lock | 557 ++++++++++++++++++++++++++++--------------------- 2 files changed, 325 insertions(+), 234 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02c0c76122c..e56b97ceccd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "click >=8.2", "granian[reload] >=2.5.5", "httpx >=0.23.3,<1.0", - "packaging >=24.2,<26", + "packaging >=24.2,<27", "platformdirs >=4.3.7,<5.0", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "pydantic >=1.10.21,<3.0", diff --git a/uv.lock b/uv.lock index 1ee25e6f7d6..04c0eb503e6 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.10, <4.0" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -320,101 +324,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -581,57 +585,62 @@ reload = [ [[package]] name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, - { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/0ae86792fb212e4384041e0ef8e7bc66f59a54912ce407d26a966ed2914d/greenlet-3.3.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b", size = 597403, upload-time = "2025-12-04T15:07:10.831Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, - { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, - { url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, - { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" }, - { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, + { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, ] [[package]] @@ -966,8 +975,12 @@ name = "numpy" version = "2.4.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } wheels = [ @@ -1058,23 +1071,25 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1127,6 +1142,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pandas" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, + { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, + { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, + { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, + { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, +] + [[package]] name = "pathspec" version = "1.0.3" @@ -1419,11 +1502,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -1582,14 +1665,14 @@ wheels = [ [[package]] name = "pyleak" -version = "0.1.15" +version = "0.1.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/99/50d6185729946c296fda5e4a02a7bb25840322eb8355028597dc8accde0c/pyleak-0.1.15.tar.gz", hash = "sha256:3abec927e3ab03a8c8bd026b8af59e8d6ac9494a9f1fac89a71f47189a2cdfd1", size = 321772, upload-time = "2026-01-19T14:45:54.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/32/2fd980ef56e7ed61de7d1628f8260aa50599dacf0c02e3da542e7e15f407/pyleak-0.1.17.tar.gz", hash = "sha256:739481d5f977da4a7063785273c6bad7904d0828d6d6ecd77a64da73a2eea138", size = 322076, upload-time = "2026-01-21T13:07:45.599Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/d0/bbd15640d011c490a87b7685c9c21d6968ac87f7ce9538c450b1d2fcbb76/pyleak-0.1.15-py3-none-any.whl", hash = "sha256:295b6015f7e7090a803e3661653c86f4b6517d458652ad832ee0ecebcd9929fc", size = 25180, upload-time = "2026-01-19T14:45:55.932Z" }, + { url = "https://files.pythonhosted.org/packages/51/86/40b5dc4b74dc4a9b2ffa64d72a59cdd40dfa788c15c5e4eb62a9b88e5aec/pyleak-0.1.17-py3-none-any.whl", hash = "sha256:9b230762d33ee8d71824bee38e3a71ddf90e76ac22d2055c795e0ea188a76c62", size = 25260, upload-time = "2026-01-21T13:07:43.961Z" }, ] [[package]] @@ -1799,11 +1882,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -1960,7 +2043,8 @@ dev = [ { name = "libsass" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pandas" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "playwright" }, { name = "plotly" }, @@ -1993,7 +2077,7 @@ requires-dist = [ { name = "click", specifier = ">=8.2" }, { name = "granian", extras = ["reload"], specifier = ">=2.5.5" }, { name = "httpx", specifier = ">=0.23.3,<1.0" }, - { name = "packaging", specifier = ">=24.2,<26" }, + { name = "packaging", specifier = ">=24.2,<27" }, { name = "platformdirs", specifier = ">=4.3.7,<5.0" }, { name = "psutil", marker = "sys_platform == 'win32'", specifier = ">=7.0.0,<8.0" }, { name = "pydantic", specifier = ">=1.10.21,<3.0" }, @@ -2080,41 +2164,41 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] name = "ruff" -version = "0.14.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -2178,51 +2262,58 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [[package]] From 9201f0e17de9fff960d5d0a031aafc46a786d25e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:40:05 -0800 Subject: [PATCH 31/74] Bump python-multipart in the uv group across 1 directory (#6103) Bumps the uv group with 1 update in the / directory: [python-multipart](https://github.com/Kludex/python-multipart). Updates `python-multipart` from 0.0.21 to 0.0.22 - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.21...0.0.22) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.22 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From f2e4e5aa6765c8dcf30f9a81fec75ef79daa9cb3 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:03:20 +0100 Subject: [PATCH 32/74] fix: Preserve event_actions from @rx.event decorator in mixin event handlers (#6099) * fix: Preserve event_actions from @rx.event decorator in mixin event handlers * add constant --- reflex/event.py | 4 +++- reflex/state.py | 6 +++++- tests/units/test_state.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/reflex/event.py b/reflex/event.py index 748fcd27802..ff75e3bd3cb 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -89,6 +89,7 @@ def substate_token(self) -> str: _EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} BACKGROUND_TASK_MARKER = "_reflex_background_task" +EVENT_ACTIONS_MARKER = "_rx_event_actions" @dataclasses.dataclass( @@ -2311,6 +2312,7 @@ class EventNamespace: # Constants BACKGROUND_TASK_MARKER = BACKGROUND_TASK_MARKER + EVENT_ACTIONS_MARKER = EVENT_ACTIONS_MARKER _EVENT_FIELDS = _EVENT_FIELDS FORM_DATA = FORM_DATA upload_files = upload_files @@ -2461,7 +2463,7 @@ def wrapper( # Store decorator event actions on the function for later processing event_actions = _build_event_actions() if event_actions: - func._rx_event_actions = event_actions # pyright: ignore [reportFunctionMemberAccess] + setattr(func, EVENT_ACTIONS_MARKER, event_actions) return func # pyright: ignore [reportReturnType] if func is not None: diff --git a/reflex/state.py b/reflex/state.py index 1795b98b719..44f75a5281c 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -42,6 +42,7 @@ from reflex.environment import PerformanceMode, environment from reflex.event import ( BACKGROUND_TASK_MARKER, + EVENT_ACTIONS_MARKER, Event, EventHandler, EventSpec, @@ -753,6 +754,9 @@ def _copy_fn(fn: Callable) -> Callable: newfn.__annotations__ = fn.__annotations__ if mark := getattr(fn, BACKGROUND_TASK_MARKER, None): setattr(newfn, BACKGROUND_TASK_MARKER, mark) + # Preserve event_actions from @rx.event decorator + if event_actions := getattr(fn, EVENT_ACTIONS_MARKER, None): + object.__setattr__(newfn, EVENT_ACTIONS_MARKER, event_actions) return newfn @staticmethod @@ -1255,7 +1259,7 @@ def _create_event_handler( The event handler. """ # Check if function has stored event_actions from decorator - event_actions = getattr(fn, "_rx_event_actions", {}) + event_actions = getattr(fn, EVENT_ACTIONS_MARKER, {}) return event_handler_cls( fn=fn, state_full_name=cls.get_full_name(), event_actions=event_actions diff --git a/tests/units/test_state.py b/tests/units/test_state.py index ca41ac37abf..2984a0aeb4d 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -3737,6 +3737,21 @@ def test_bare_mixin_state() -> None: assert ChildBareMixinState.get_root_state() == State +def test_mixin_event_handler_preserves_event_actions() -> None: + """Test that event_actions from @rx.event decorator are preserved when inherited from mixins.""" + + class EventActionsMixin(BaseState, mixin=True): + @rx.event(prevent_default=True, stop_propagation=True) + def handle_with_actions(self): + pass + + class UsesEventActionsMixin(EventActionsMixin, State): + pass + + handler = UsesEventActionsMixin.handle_with_actions + assert handler.event_actions == {"preventDefault": True, "stopPropagation": True} + + def test_assignment_to_undeclared_vars(): """Test that an attribute error is thrown when undeclared vars are set.""" From e2e6d142f297a8cb427f7929841e8f5253b67669 Mon Sep 17 00:00:00 2001 From: Deen Adzemovic Date: Tue, 27 Jan 2026 14:04:15 -0800 Subject: [PATCH 33/74] Fix socket reference shape mismatch in queueEvents (#6094) * Fix socket shape mismatch in queueEvents Add resolveSocket() helper to handle both ref objects ({ current: Socket }) and raw sockets, fixing a bug where queueEvents would call socket.current on an already-unwrapped socket, causing processEvent to bail out. * Address feedback and add JSDoc comment Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- reflex/.templates/web/utils/state.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index e45857357ba..9e937ed62cd 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -446,6 +446,16 @@ export const applyRestEvent = async (event, socket, navigate, params) => { return eventSent; }; +/** + * Resolve a socket reference to the actual socket object. + * Handles both ref objects ({ current: Socket }) and raw sockets. + * @param socket Either a ref object or raw socket. + * @returns The actual socket object. + */ +const resolveSocket = (socket) => { + return socket?.current ?? socket; +}; + /** * Queue events to be processed and trigger processing of queue. * @param events Array of events to queue. @@ -471,7 +481,7 @@ export const queueEvents = async ( ]; } event_queue.push(...events.filter((e) => e !== undefined && e !== null)); - await processEvent(socket.current, navigate, params); + await processEvent(resolveSocket(socket), navigate, params); }; /** From b132303a06d6dfab8d99af891345a5c6922ffab0 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:41:15 +0100 Subject: [PATCH 34/74] fix: Preserve object identity when pickling MutableProxy (#6097) * fix: Preserve object identity when pickling MutableProxy * thanks greptile, it wasn't a dataclass once --- reflex/istate/proxy.py | 17 +++++++++------ tests/units/istate/test_proxy.py | 37 ++++++++++++++++++++++++++++++++ tests/units/test_state.py | 8 ++----- 3 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 tests/units/istate/test_proxy.py diff --git a/reflex/istate/proxy.py b/reflex/istate/proxy.py index d937d5ff236..80c3f83e1fe 100644 --- a/reflex/istate/proxy.py +++ b/reflex/istate/proxy.py @@ -26,6 +26,7 @@ from reflex.state import BaseState, StateUpdate T_STATE = TypeVar("T_STATE", bound="BaseState") +T = TypeVar("T") class StateProxy(wrapt.ObjectProxy): @@ -671,19 +672,23 @@ def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Any: return copy.deepcopy(self.__wrapped__, memo=memo) def __reduce_ex__(self, protocol_version: SupportsIndex): - """Get the state for redis serialization. + """Serialize the wrapped object for pickle, stripping off the proxy. - This method is called by cloudpickle to serialize the object. - - It explicitly serializes the wrapped object, stripping off the mutable proxy. + Returns a function that reconstructs to the wrapped object directly, + ensuring pickle's memo system correctly tracks object identity. Args: protocol_version: The protocol version. Returns: - Tuple of (wrapped class, empty args, class __getstate__) + Tuple that reconstructs to the wrapped object. """ - return self.__wrapped__.__reduce_ex__(protocol_version) + return (_unwrap_for_pickle, (self.__wrapped__,)) + + +def _unwrap_for_pickle(obj: T) -> T: + """Return the object unchanged. Used by MutableProxy.__reduce_ex__.""" + return obj @serializer diff --git a/tests/units/istate/test_proxy.py b/tests/units/istate/test_proxy.py new file mode 100644 index 00000000000..b71d91e619d --- /dev/null +++ b/tests/units/istate/test_proxy.py @@ -0,0 +1,37 @@ +"""Tests for MutableProxy pickle behavior.""" + +import dataclasses +import pickle + +import reflex as rx +from reflex.istate.proxy import MutableProxy + + +@dataclasses.dataclass +class Item: + """Simple picklable object for testing.""" + + id: int + + +class ProxyTestState(rx.State): + """Test state with a list field.""" + + items: list[Item] = [] + + +def test_mutable_proxy_pickle_preserves_object_identity(): + """Test that same object referenced directly and via proxy maintains identity.""" + state = ProxyTestState() + obj = Item(1) + + data = { + "direct": [obj], + "proxied": [MutableProxy(obj, state, "items")], + } + + unpickled = pickle.loads(pickle.dumps(data)) + + assert unpickled["direct"][0].id == 1 + assert unpickled["proxied"][0].id == 1 + assert unpickled["direct"][0] is unpickled["proxied"][0] diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 2984a0aeb4d..5802cfa8b6f 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -4468,9 +4468,5 @@ async def test_rebind_mutable_proxy(mock_app: rx.App, token: str) -> None: ) as state: assert isinstance(state, MutableProxyState) assert state.data["a"] == [2, 3] - if isinstance(mock_app.state_manager, StateManagerRedis): - # In redis mode, the object identity does not persist across async with self calls. - assert state.data["b"] == [2] - else: - # In disk/memory mode, the fact that data["b"] was mutated via data["a"] persists. - assert state.data["b"] == [2, 3] + # Object identity persists across serialization, so data["b"] is also mutated. + assert state.data["b"] == [2, 3] From 413901b8238cab03a1753a74cad9e15c481678f6 Mon Sep 17 00:00:00 2001 From: abulvenz Date: Fri, 30 Jan 2026 00:50:45 +0100 Subject: [PATCH 35/74] fix: Copy ID even when it is a var. (#6105) --- reflex/components/core/debounce.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reflex/components/core/debounce.py b/reflex/components/core/debounce.py index ab0adfc9714..1d42dad2ff5 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -111,6 +111,7 @@ def create(cls, *children: Component, **props: Any) -> Component: child_ref = child.get_ref() if props.get("input_ref") is None and child_ref: props["input_ref"] = Var(_js_expr=child_ref, _var_type=str) + if child.id is not None: props["id"] = child.id # Set the child element to wrap, including any imports/hooks from the child. From 8d9ed97a01136e910f55da19c1074baa74d801ca Mon Sep 17 00:00:00 2001 From: MatthewCollinsNZ Date: Fri, 13 Feb 2026 04:32:43 +1030 Subject: [PATCH 36/74] Origin/matthew collins nz/adddataeditorrowcheckboxvisible (#6082) * Adding checkbox_visible to rowmarker for data editor * Update dataeditor.py * pre-commit --------- Co-authored-by: Masen Furer --- pyi_hashes.json | 2 +- reflex/components/datadisplay/dataeditor.py | 2 +- reflex/components/literals.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 185f64eccae..8958c7e97ce 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -23,7 +23,7 @@ "reflex/components/core/window_events.pyi": "af33ccec866b9540ee7fbec6dbfbd151", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", "reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd", - "reflex/components/datadisplay/dataeditor.pyi": "fb26f3e702fcb885539d1cf82a854be3", + "reflex/components/datadisplay/dataeditor.pyi": "d2a749db7e279972d4bc1f4de63a7c41", "reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd", "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index fd854cb5601..4689766ddd8 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -240,7 +240,7 @@ class DataEditor(NoSSRComponent): # Determines the height of each row. row_height: Var[int] - # Kind of row markers. + # Kind of row markers. Options are: "none", "number", "checkbox", "both", "checkbox-visible", "clickable-number". row_markers: Var[LiteralRowMarker] # Changes the starting index for row markers. diff --git a/reflex/components/literals.py b/reflex/components/literals.py index df5e05c3f2f..0616677acdf 100644 --- a/reflex/components/literals.py +++ b/reflex/components/literals.py @@ -29,4 +29,6 @@ ] -LiteralRowMarker = Literal["none", "number", "checkbox", "both", "clickable-number"] +LiteralRowMarker = Literal[ + "none", "number", "checkbox", "both", "checkbox-visible", "clickable-number" +] From 29965cf5fba68274ba218eb5ab1bd7a6f6d3ea47 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 12 Feb 2026 10:42:34 -0800 Subject: [PATCH 37/74] Catch all exceptions in health checks (#6089) * Catch all exceptions in health checks * log exceptions once in failed health check avoid spamming the "Database is not initialized, ..." message --- reflex/model.py | 12 +++++++++--- reflex/utils/prerequisites.py | 11 +++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/reflex/model.py b/reflex/model.py index acfa8779882..0ae874a8a39 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -111,7 +111,8 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: if not environment.ALEMBIC_CONFIG.get().exists(): console.warn( - "Database is not initialized, run [bold]reflex db init[/bold] first." + "Database is not initialized, run [bold]reflex db init[/bold] first.", + dedupe=True, ) _ENGINE[url] = sqlalchemy.engine.create_engine( url, @@ -153,7 +154,8 @@ def get_async_engine(url: str | None) -> sqlalchemy.ext.asyncio.AsyncEngine: if not environment.ALEMBIC_CONFIG.get().exists(): console.warn( - "Database is not initialized, run [bold]reflex db init[/bold] first." + "Database is not initialized, run [bold]reflex db init[/bold] first.", + dedupe=True, ) _ASYNC_ENGINE[url] = sqlalchemy.ext.asyncio.create_async_engine( url, @@ -316,8 +318,12 @@ def get_db_status() -> dict[str, bool]: engine = get_engine() with engine.connect() as connection: connection.execute(sqlalchemy.text("SELECT 1")) - except sqlalchemy.exc.OperationalError: + except Exception as exc: status = False + console.error( + f"Database health check failed: {exc} (subsequent errors will not be logged)", + dedupe=True, + ) return {"db": status} diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 886d1dead96..6e9353d9633 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -431,8 +431,6 @@ async def get_redis_status() -> dict[str, bool | None]: Returns: The status of the Redis connection. """ - from redis.exceptions import RedisError - try: status = True redis_client = get_redis() @@ -442,8 +440,12 @@ async def get_redis_status() -> dict[str, bool | None]: await ping_command else: status = None - except RedisError: + except Exception as exc: status = False + console.error( + f"Redis health check failed: {exc} (subsequent errors will not be logged)", + dedupe=True, + ) return {"redis": status} @@ -645,7 +647,8 @@ def check_db_initialized() -> bool: and not environment.ALEMBIC_CONFIG.get().exists() ): console.error( - "Database is not initialized. Run [bold]reflex db init[/bold] first." + "Database is not initialized. Run [bold]reflex db init[/bold] first.", + dedupe=True, ) return False return True From 73901cdd5dc4f4de9ad4c5fe4bf0a76020b3079d Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 12 Feb 2026 10:42:47 -0800 Subject: [PATCH 38/74] Run external servers with `sys.executable -m` (#6095) This ensures that if the environment path is not correct, we should still be able to resolve the granian, gunicorn, and uvicorn modules when executing the server in a subprocess. --- reflex/utils/exec.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index ac6f720cda8..ac70d62ea3b 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -612,6 +612,8 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): if constants.IS_WINDOWS: command = [ + sys.executable, + "-m", "uvicorn", *("--host", host), *("--port", str(port)), @@ -627,6 +629,8 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): # Our default args, then env args (env args win on conflicts) command = [ + sys.executable, + "-m", "gunicorn", "--preload", *("--worker-class", "uvicorn.workers.UvicornH11Worker"), @@ -663,6 +667,8 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel): from reflex.utils import processes command = [ + sys.executable, + "-m", "granian", *("--log-level", "critical"), *("--host", host), From 3934bc46d1623dfc8f289fa45b86c12435b342c7 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 12 Feb 2026 10:43:06 -0800 Subject: [PATCH 39/74] Use homogenous tuple annotation in BaseStateMeta.__new__ (#6096) * Use homogenous tuple annotation in BaseStateMeta.__new__ * update tuple[type, ...] in other places it was used * precommit --- reflex/components/field.py | 6 ++++-- reflex/vars/base.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/reflex/components/field.py b/reflex/components/field.py index ab3116d6ab3..43944485e2b 100644 --- a/reflex/components/field.py +++ b/reflex/components/field.py @@ -67,7 +67,9 @@ class FieldBasedMeta(type): PropsBaseMeta and BaseComponentMeta. """ - def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> type: + def __new__( + cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any] + ) -> type: """Create a new field-based class. Args: @@ -100,7 +102,7 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty return super().__new__(cls, name, bases, namespace) @classmethod - def _collect_inherited_fields(cls, bases: tuple[type]) -> dict[str, Any]: + def _collect_inherited_fields(cls, bases: tuple[type, ...]) -> dict[str, Any]: inherited_fields: dict[str, Any] = {} # Collect inherited fields from base classes diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 549b4896d57..0cf9fca79ec 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -3537,7 +3537,7 @@ class BaseStateMeta(ABCMeta): def __new__( cls, name: str, - bases: tuple[type], + bases: tuple[type, ...], namespace: dict[str, Any], mixin: bool = False, state_id: int | None = None, From 087c0247d5f25bb7fcab90be85e768931b092f21 Mon Sep 17 00:00:00 2001 From: MatthewCollinsNZ Date: Fri, 13 Feb 2026 05:33:18 +1030 Subject: [PATCH 40/74] adding row selection mode (#6090) * adding row selection mode * Update pyi hashes --------- Co-authored-by: Masen Furer --- pyi_hashes.json | 2 +- reflex/components/datadisplay/dataeditor.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 8958c7e97ce..d179d76dc59 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -23,7 +23,7 @@ "reflex/components/core/window_events.pyi": "af33ccec866b9540ee7fbec6dbfbd151", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", "reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd", - "reflex/components/datadisplay/dataeditor.pyi": "d2a749db7e279972d4bc1f4de63a7c41", + "reflex/components/datadisplay/dataeditor.pyi": "dc4327bd23addced344d9850a959aa21", "reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd", "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 4689766ddd8..6107c98467a 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -291,6 +291,9 @@ class DataEditor(NoSSRComponent): # Controls which types of row selections can exist at the same time. ("exclusive", "mixed"). row_selection_blending: Var[Literal["exclusive", "mixed"]] + # Controls row marker selection behavior. "auto" adapts to touch/mouse, "multi" acts as if Ctrl is pressed. ("auto", "multi"). + row_selection_mode: Var[Literal["auto", "multi"]] + # Controls how spans are handled in selections. ("default", "allowPartial"). span_range_behavior: Var[Literal["default", "allowPartial"]] From 0580d84aa08626f17b5459273aac4bad59668b01 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:44:34 +0100 Subject: [PATCH 41/74] use get_event_handler in upload, cherry-pick from #6100 (#6130) --- reflex/app.py | 21 ++++++++------------- reflex/state.py | 17 ++++++++--------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4ff412ef863..6245a9f0d1d 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -77,13 +77,13 @@ from reflex.event import ( _EVENT_FIELDS, Event, - EventHandler, EventSpec, EventType, IndividualEventType, get_hydrate_event, noop, ) +from reflex.istate.proxy import StateProxy from reflex.page import DECORATED_PAGES from reflex.route import ( get_route_args, @@ -1619,6 +1619,8 @@ def _process_background( if not handler.is_background: return None + substate = StateProxy(substate) + async def _coro(): """Coroutine to process the event and emit updates inside an asyncio.Task. @@ -1934,21 +1936,14 @@ async def upload_file(request: Request): substate_token = _substate_key(token, handler.rpartition(".")[0]) state = await app.state_manager.get_state(substate_token) - # get the current session ID - # get the current state(parent state/substate) - path = handler.split(".")[:-1] - current_state = state.get_substate(path) handler_upload_param = () - # get handler function - func = getattr(type(current_state), handler.split(".")[-1]) + _current_state, event_handler = state._get_event_handler(handler) - # check if there exists any handler args with annotation, list[UploadFile] - if isinstance(func, EventHandler): - if func.is_background: - msg = f"@rx.event(background=True) is not supported for upload handler `{handler}`." - raise UploadTypeError(msg) - func = func.fn + if event_handler.is_background: + msg = f"@rx.event(background=True) is not supported for upload handler `{handler}`." + raise UploadTypeError(msg) + func = event_handler.fn if isinstance(func, functools.partial): func = func.func for k, v in get_type_hints(func).items(): diff --git a/reflex/state.py b/reflex/state.py index 44f75a5281c..832219ad82d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1801,13 +1801,11 @@ async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE: ) return getattr(other_state, var_data.field_name) - def _get_event_handler( - self, event: Event - ) -> tuple[BaseState | StateProxy, EventHandler]: + def _get_event_handler(self, event: Event | str) -> tuple[BaseState, EventHandler]: """Get the event handler for the given event. Args: - event: The event to get the handler for. + event: The event to get the handler for, or a dotted handler name string. Returns: @@ -1817,7 +1815,8 @@ def _get_event_handler( ValueError: If the event handler or substate is not found. """ # Get the event handler. - path = event.name.split(".") + name = event.name if isinstance(event, Event) else event + path = name.split(".") path, name = path[:-1], path[-1] substate = self.get_substate(path) if not substate: @@ -1825,10 +1824,6 @@ def _get_event_handler( raise ValueError(msg) handler = substate.event_handlers[name] - # For background tasks, proxy the state - if handler.is_background: - substate = StateProxy(substate) - return substate, handler async def _process(self, event: Event) -> AsyncIterator[StateUpdate]: @@ -1843,6 +1838,10 @@ async def _process(self, event: Event) -> AsyncIterator[StateUpdate]: # Get the event handler. substate, handler = self._get_event_handler(event) + # For background tasks, proxy the state. + if handler.is_background: + substate = StateProxy(substate) + # Run the event generator and yield state updates. async for update in self._process_event( handler=handler, From 7405808c98c64eb82ddf41c8c44cc4650ea9153c Mon Sep 17 00:00:00 2001 From: MatthewCollinsNZ Date: Fri, 13 Feb 2026 07:02:01 +1030 Subject: [PATCH 42/74] Data editor search props (#6113) * Update dataeditor.py * pre commit --------- Co-authored-by: Masen Furer --- pyi_hashes.json | 2 +- reflex/components/datadisplay/dataeditor.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index d179d76dc59..681a2d1e3d9 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -23,7 +23,7 @@ "reflex/components/core/window_events.pyi": "af33ccec866b9540ee7fbec6dbfbd151", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", "reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd", - "reflex/components/datadisplay/dataeditor.pyi": "dc4327bd23addced344d9850a959aa21", + "reflex/components/datadisplay/dataeditor.pyi": "f8c1e816c9f22f4a7429f812214407f2", "reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd", "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 6107c98467a..e5fa555bdab 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -360,6 +360,12 @@ class DataEditor(NoSSRComponent): # Fired when a column is resized. on_column_resize: EventHandler[passthrough_event_spec(GridColumn, int)] + # Shows search bar. + show_search: Var[bool] + + # Fired when the search close button is clicked. + on_search_close: EventHandler[no_args_event_spec] + def add_imports(self) -> ImportDict: """Add imports for the component. From 7c0b6c2dd3c32e246692897148200f7f65662799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:04:39 -0800 Subject: [PATCH 43/74] Bump pillow from 12.1.0 to 12.1.1 in the uv group across 1 directory (#6127) Bumps the uv group with 1 update in the / directory: [pillow](https://github.com/python-pillow/Pillow). Updates `pillow` from 12.1.0 to 12.1.1 - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.1.1 dependency-type: direct:development dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 188 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/uv.lock b/uv.lock index 04c0eb503e6..892dd759116 100644 --- a/uv.lock +++ b/uv.lock @@ -1221,100 +1221,100 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] From 64da0616b0dd9a2da2b697386e3495af07866eb3 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 12 Feb 2026 15:49:39 -0800 Subject: [PATCH 44/74] Allow granian log level to be set (#6131) User can set GRANIAN_LOG_LEVEL environment variable to override the default "critical" value. --- reflex/utils/exec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index ac70d62ea3b..c67976b23b2 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -670,7 +670,6 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel): sys.executable, "-m", "granian", - *("--log-level", "critical"), *("--host", host), *("--port", str(port)), *("--interface", str(Interfaces.ASGI)), @@ -683,6 +682,8 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel): if "GRANIAN_WORKERS" not in os.environ: extra_env["GRANIAN_WORKERS"] = str(_get_backend_workers()) + if "GRANIAN_LOG_LEVEL" not in os.environ: + extra_env["GRANIAN_LOG_LEVEL"] = "critical" processes.new_process( command, From 63121f6f06e4a0b97fa57e7a3f8618c06a68b3a1 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:15:13 +0100 Subject: [PATCH 45/74] cache get_type_hints for heavy init subclass stuff - improve import performance (#6118) * fix: cache get_type_hints for heavy init subclass stuff to improve import performance * fix: prefer tuple and improve mixin typing --- reflex/state.py | 16 +++++++++------- reflex/utils/types.py | 11 ++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 832219ad82d..402c84bea6b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -525,7 +525,7 @@ def _get_computed_vars(cls) -> list[tuple[str, ComputedVar]]: """ return [ (name, v) - for mixin in [*cls._mixins(), cls] + for mixin in (*cls._mixins(), cls) for name, v in mixin.__dict__.items() if is_computed_var(v) and name not in cls.inherited_vars ] @@ -636,14 +636,14 @@ def __init_subclass__( new_backend_vars = { name: value if not isinstance(value, Field) else value.default_value() - for mixin_cls in [*cls._mixins(), cls] + for mixin_cls in (*cls._mixins(), cls) for name, value in list(mixin_cls.__dict__.items()) if types.is_backend_base_variable(name, mixin_cls) } # Add annotated backend vars that may not have a default value. new_backend_vars.update({ name: cls._get_var_default(name, annotation_value) - for mixin_cls in [*cls._mixins(), cls] + for mixin_cls in (*cls._mixins(), cls) for name, annotation_value in mixin_cls._get_type_hints().items() if name not in new_backend_vars and types.is_backend_base_variable(name, mixin_cls) @@ -831,13 +831,13 @@ def computed_var_func(state: Self): return getattr(cls, unique_var_name) @classmethod - def _mixins(cls) -> list[type]: + def _mixins(cls) -> tuple[type[BaseState], ...]: """Get the mixin classes of the state. Returns: The mixin classes of the state. """ - return [ + return tuple( mixin for mixin in cls.__mro__ if ( @@ -845,7 +845,7 @@ def _mixins(cls) -> list[type]: and issubclass(mixin, BaseState) and mixin._mixin is True ) - ] + ) @classmethod def _handle_local_def(cls): @@ -863,6 +863,7 @@ def _handle_local_def(cls): cls.__module__ = reflex.istate.dynamic.__name__ @classmethod + @functools.cache def _get_type_hints(cls) -> dict[str, Any]: """Get the type hints for this class. @@ -965,8 +966,9 @@ def _check_overridden_basevars(cls): Raises: ComputedVarShadowsBaseVarsError: When a computed var shadows a base var. """ + hints = cls._get_type_hints() for name, computed_var_ in cls._get_computed_vars(): - if name in get_type_hints(cls): + if name in hints: msg = f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead" raise ComputedVarShadowsBaseVarsError(msg) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 455107b1b6d..b375157d556 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -57,6 +57,7 @@ StateVarTypes = (*PrimitiveTypes, Base, type(None)) if TYPE_CHECKING: + from reflex.state import BaseState from reflex.vars.base import Var VAR1 = TypeVar("VAR1", bound="Var") @@ -902,12 +903,12 @@ def is_valid_var_type(type_: type) -> bool: ) -def is_backend_base_variable(name: str, cls: type) -> bool: +def is_backend_base_variable(name: str, cls: type[BaseState]) -> bool: """Check if this variable name correspond to a backend variable. Args: name: The name of the variable to check - cls: The class of the variable to check + cls: The class of the variable to check (must be a BaseState subclass) Returns: bool: The result of the check @@ -924,11 +925,7 @@ def is_backend_base_variable(name: str, cls: type) -> bool: if name.startswith(f"_{cls.__name__}__"): return False - # Extract the namespace of the original module if defined (dynamic substates). - if callable(getattr(cls, "_get_type_hints", None)): - hints = cls._get_type_hints() - else: - hints = get_type_hints(cls) + hints = cls._get_type_hints() if name in hints: hint = get_origin(hints[name]) if hint == ClassVar: From c8c2ab16b6180a1c9559e8a84642829402e130d7 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 23 Feb 2026 14:37:18 -0800 Subject: [PATCH 46/74] 0828dev (#6136) * 0828dev * does 15 fix this * ok it seems there are css issues * is it autoprefixer?? * idk * wtf is wrong with this * idk even * is it granian? * ok * vite * fix types * bump granian --- pyi_hashes.json | 8 +- pyproject.toml | 4 +- reflex/components/base/error_boundary.py | 2 +- reflex/components/core/upload.py | 2 +- reflex/components/datadisplay/dataeditor.py | 2 +- reflex/components/lucide/icon.py | 20 +- reflex/components/moment/moment.py | 5 +- reflex/components/radix/themes/base.py | 2 +- reflex/components/recharts/cartesian.py | 6 - reflex/components/recharts/recharts.py | 6 +- reflex/constants/installer.py | 8 +- reflex/testing.py | 12 +- tests/integration/test_client_storage.py | 2 +- tests/units/test_model.py | 3 +- tests/units/test_sqlalchemy.py | 3 +- tests/units/test_state.py | 9 +- uv.lock | 1357 +++++++++---------- 17 files changed, 696 insertions(+), 755 deletions(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 681a2d1e3d9..ec9bbff8850 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -39,7 +39,7 @@ "reflex/components/el/elements/tables.pyi": "686eb70ea7d8c4dafb0cc5c284e76184", "reflex/components/el/elements/typography.pyi": "684e83dde887dba12badd0fb75c87c04", "reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42", - "reflex/components/lucide/icon.pyi": "9cdd1107295f5c4b6d5d6516f487f237", + "reflex/components/lucide/icon.pyi": "dcb8773ef162f3ec5759efe11374cf5e", "reflex/components/markdown/markdown.pyi": "dd74e8e9665b2a813ff799a7aa190b44", "reflex/components/moment/moment.pyi": "e1952f1c2c82cef85d91e970d1be64ab", "reflex/components/plotly/plotly.pyi": "4311a0aae2abcc9226abb6a273f96372", @@ -113,10 +113,10 @@ "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", "reflex/components/react_router/dom.pyi": "3042fa630b7e26a7378fe045d7fbf4af", "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", - "reflex/components/recharts/cartesian.pyi": "cfca4f880239ffaecdf9fb4c7c8caed5", + "reflex/components/recharts/cartesian.pyi": "d138261ab8259d5208c2f028b9f708bd", "reflex/components/recharts/charts.pyi": "013036b9c00ad85a570efdb813c1bc40", "reflex/components/recharts/general.pyi": "d87ff9b85b2a204be01753690df4fb11", - "reflex/components/recharts/polar.pyi": "ad24bd37c6acc0bc9bd4ac01af3ffe49", - "reflex/components/recharts/recharts.pyi": "c41d19ab67972246c574098929bea7ea", + "reflex/components/recharts/polar.pyi": "b8b1a3e996e066facdf4f8c9eb363137", + "reflex/components/recharts/recharts.pyi": "d5c9fc57a03b419748f0408c23319eee", "reflex/components/sonner/toast.pyi": "3c27bad1aaeb5183eaa6a41e77e8d7f0" } diff --git a/pyproject.toml b/pyproject.toml index e56b97ceccd..24f9f3cdd7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "reflex" -version = "0.8.27dev1" +version = "0.8.28dev1" description = "Web apps in pure Python." license.text = "Apache-2.0" authors = [ @@ -244,7 +244,7 @@ fail_fast = true [[tool.pre-commit.repos]] repo = "https://github.com/astral-sh/ruff-pre-commit" -rev = "v0.14.13" +rev = "v0.15.1" hooks = [ { id = "ruff-format", args = [ "reflex", diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py index e5423a4e690..cc80b906e94 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -36,7 +36,7 @@ def on_error_spec( class ErrorBoundary(Component): """A React Error Boundary component that catches unhandled frontend exceptions.""" - library = "react-error-boundary@6.1.0" + library = "react-error-boundary@6.1.1" tag = "ErrorBoundary" # Fired when the boundary catches an error. diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index 8ffa77ea649..670112fad33 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -221,7 +221,7 @@ class GhostUpload(Fragment): class Upload(MemoizationLeaf): """A file upload component.""" - library = "react-dropzone@14.3.8" + library = "react-dropzone@15.0.0" tag = "" diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index e5fa555bdab..09289dba1b2 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -182,7 +182,7 @@ class DataEditor(NoSSRComponent): is_default = True library: str | None = "@glideapps/glide-data-grid@6.0.3" lib_dependencies: list[str] = [ - "lodash@4.17.21", + "lodash@4.17.23", "react-responsive-carousel@3.2.23", ] diff --git a/reflex/components/lucide/icon.py b/reflex/components/lucide/icon.py index e64f1bb5e55..5b925ba3a75 100644 --- a/reflex/components/lucide/icon.py +++ b/reflex/components/lucide/icon.py @@ -6,7 +6,7 @@ from reflex.vars.base import LiteralVar, Var from reflex.vars.sequence import LiteralStringVar, StringVar -LUCIDE_LIBRARY = "lucide-react@0.562.0" +LUCIDE_LIBRARY = "lucide-react@0.574.0" class LucideIconComponent(Component): @@ -627,6 +627,7 @@ def _get_imports(self): "cylinder", "dam", "database_backup", + "database_search", "database_zap", "database", "decimals_arrow_left", @@ -864,6 +865,7 @@ def _get_imports(self): "git_compare", "git_fork", "git_graph", + "git_merge_conflict", "git_merge", "git_pull_request_arrow", "git_pull_request_closed", @@ -876,6 +878,8 @@ def _get_imports(self): "glass_water", "glasses", "globe_lock", + "globe_off", + "globe_x", "globe", "goal", "gpu", @@ -1012,6 +1016,8 @@ def _get_imports(self): "leaf", "leafy_green", "lectern", + "lens_concave", + "lens_convex", "letter_text", "library_big", "library", @@ -1020,6 +1026,7 @@ def _get_imports(self): "lightbulb_off", "lightbulb", "line_chart", + "line_dot_right_horizontal", "line_squiggle", "link_2_off", "link_2", @@ -1097,6 +1104,7 @@ def _get_imports(self): "memory_stick", "menu", "merge", + "message_circle_check", "message_circle_code", "message_circle_dashed", "message_circle_heart", @@ -1137,6 +1145,8 @@ def _get_imports(self): "minimize_2", "minimize", "minus", + "mirror_rectangular", + "mirror_round", "monitor_check", "monitor_cloud", "monitor_cog", @@ -1156,6 +1166,7 @@ def _get_imports(self): "motorbike", "mountain_snow", "mountain", + "mouse_left", "mouse_off", "mouse_pointer_2_off", "mouse_pointer_2", @@ -1305,6 +1316,7 @@ def _get_imports(self): "power", "presentation", "printer_check", + "printer_x", "printer", "projector", "proportions", @@ -1432,6 +1444,7 @@ def _get_imports(self): "share", "sheet", "shell", + "shelving_unit", "shield_alert", "shield_ban", "shield_check", @@ -1660,6 +1673,7 @@ def _get_imports(self): "torus", "touchpad_off", "touchpad", + "towel_rack", "tower_control", "toy_brick", "tractor", @@ -1714,12 +1728,14 @@ def _get_imports(self): "usb", "user_check", "user_cog", + "user_key", "user_lock", "user_minus", "user_pen", "user_plus", "user_round_check", "user_round_cog", + "user_round_key", "user_round_minus", "user_round_pen", "user_round_plus", @@ -1795,12 +1811,14 @@ def _get_imports(self): "worm", "wrap_text", "wrench", + "x_line_top", "x", "youtube", "zap_off", "zap", "zoom_in", "zoom_out", + # "mouse_right", ] # The default transformation of some icon names doesn't match how the diff --git a/reflex/components/moment/moment.py b/reflex/components/moment/moment.py index 703146aad1c..def1b6551d6 100644 --- a/reflex/components/moment/moment.py +++ b/reflex/components/moment/moment.py @@ -29,7 +29,7 @@ class Moment(NoSSRComponent): tag: str | None = "Moment" is_default = True - library: str | None = "react-moment@1.2.0" + library: str | None = "react-moment@1.2.2" lib_dependencies: list[str] = ["moment@2.30.1"] # How often the date update (how often time update / 0 to disable). @@ -53,6 +53,9 @@ class Moment(NoSSRComponent): # Displays the date as the time from now, e.g. "5 minutes ago". from_now: Var[bool] + # Displays the relative time in a short format using abbreviated units (e.g., "1h", "2d", "3mo", "1y" instead of "1 hour ago", "2 days ago", etc.). + from_now_short: Var[bool] + # Setting fromNowDuring will display the relative time as with fromNow but just during its value in milliseconds, after that format will be used instead. from_now_during: Var[int] diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index 96ba7a66391..002996b0743 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -109,7 +109,7 @@ class RadixLoadingProp(Component): class RadixThemesComponent(Component): """Base class for all @radix-ui/themes components.""" - library = "@radix-ui/themes@3.2.1" + library = "@radix-ui/themes@3.3.0" # "Fake" prop color_scheme is used to avoid shadowing CSS prop "color". _rename_props: ClassVar[dict[str, str]] = {"colorScheme": "color"} diff --git a/reflex/components/recharts/cartesian.py b/reflex/components/recharts/cartesian.py index c17f135506e..4d6eff6010f 100644 --- a/reflex/components/recharts/cartesian.py +++ b/reflex/components/recharts/cartesian.py @@ -241,12 +241,6 @@ class Brush(Recharts): # The default end index of brush. If the option is not set, the end index will be calculated by the length of data. end_index: Var[int] - # The fill color of brush - fill: Var[str | Color] - - # The stroke color of brush - stroke: Var[str | Color] - @classmethod def get_event_triggers(cls) -> dict[str, Var | Any]: """Get the event triggers that pass the component's value to the handler. diff --git a/reflex/components/recharts/recharts.py b/reflex/components/recharts/recharts.py index b9ec8b5fe2b..ef1650c4b01 100644 --- a/reflex/components/recharts/recharts.py +++ b/reflex/components/recharts/recharts.py @@ -8,7 +8,7 @@ class Recharts(Component): """A component that wraps a recharts lib.""" - library = "recharts@3.6.0" + library = "recharts@3.7.0" def _get_style(self) -> dict: return {"wrapperStyle": self.style} @@ -17,7 +17,7 @@ def _get_style(self) -> dict: class RechartsCharts(NoSSRComponent, MemoizationLeaf): """A component that wraps a recharts lib.""" - library = "recharts@3.6.0" + library = "recharts@3.7.0" LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"] @@ -50,7 +50,7 @@ class RechartsCharts(NoSSRComponent, MemoizationLeaf): ] LiteralTextAnchor = Literal["start", "middle", "end"] LiteralLayout = Literal["horizontal", "vertical"] -LiteralPolarRadiusType = Literal["number", "category"] +LiteralPolarRadiusType = Literal["auto", "number", "category"] LiteralGridType = Literal["polygon", "circle"] LiteralPosition = Literal[ "top", diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index d6131340bda..12edb911397 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -14,7 +14,7 @@ class Bun(SimpleNamespace): """Bun constants.""" # The Bun version. - VERSION = "1.3.6" + VERSION = "1.3.9" # Min Bun Version MIN_VERSION = "1.3.0" @@ -75,7 +75,7 @@ class Node(SimpleNamespace): def _determine_react_router_version() -> str: - default_version = "7.12.0" + default_version = "7.13.0" if (version := os.getenv("REACT_ROUTER_VERSION")) and version != default_version: from reflex.utils import console @@ -131,14 +131,14 @@ def DEPENDENCIES(cls) -> dict[str, str]: "react": cls._react_version, "react-helmet": "6.1.0", "react-dom": cls._react_version, - "isbot": "5.1.33", + "isbot": "5.1.35", "socket.io-client": "4.8.3", "universal-cookie": "7.2.2", } DEV_DEPENDENCIES = { "@emotion/react": "11.14.0", - "autoprefixer": "10.4.23", + "autoprefixer": "10.4.24", "postcss": "8.5.6", "postcss-import": "16.1.1", "@react-router/dev": _react_router_version, diff --git a/reflex/testing.py b/reflex/testing.py index b2d74fe7868..4ab72602334 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -673,18 +673,24 @@ def frontend( driver_options = getattr(webdriver, f"{requested_driver}Options")() # pyright: ignore [reportPossiblyUnboundVariable] if driver_clz is webdriver.Chrome: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.ChromeOptions() # pyright: ignore [reportPossiblyUnboundVariable] + from selenium.webdriver.chrome.options import Options + + driver_options = Options() # pyright: ignore [reportPossiblyUnboundVariable] driver_options.add_argument("--class=AppHarness") if want_headless: driver_options.add_argument("--headless=new") elif driver_clz is webdriver.Firefox: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.FirefoxOptions() # pyright: ignore [reportPossiblyUnboundVariable] + from selenium.webdriver.firefox.options import Options + + driver_options = Options() # pyright: ignore [reportPossiblyUnboundVariable] if want_headless: driver_options.add_argument("-headless") elif driver_clz is webdriver.Edge: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.EdgeOptions() # pyright: ignore [reportPossiblyUnboundVariable] + from selenium.webdriver.edge.options import Options + + driver_options = Options() # pyright: ignore [reportPossiblyUnboundVariable] if want_headless: driver_options.add_argument("headless") if driver_options is None: diff --git a/tests/integration/test_client_storage.py b/tests/integration/test_client_storage.py index 481e0c62d4f..80766407d31 100644 --- a/tests/integration/test_client_storage.py +++ b/tests/integration/test_client_storage.py @@ -6,8 +6,8 @@ from collections.abc import Generator import pytest -from selenium.webdriver import Firefox from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.webdriver import WebDriver as Firefox from selenium.webdriver.remote.webdriver import WebDriver from reflex.constants.state import FIELD_MARKER diff --git a/tests/units/test_model.py b/tests/units/test_model.py index 2f1453c9cb5..51b8982d926 100644 --- a/tests/units/test_model.py +++ b/tests/units/test_model.py @@ -1,3 +1,4 @@ +import math from pathlib import Path from unittest import mock @@ -161,7 +162,7 @@ class AlembicSecond(Model, table=True): result = session.exec(sqlmodel.select(AlembicSecond)).all() assert len(result) == 1 assert result[0].a == 42 - assert result[0].b == 4.2 + assert math.isclose(result[0].b, 4.2) # No-op assert Model.migrate(autogenerate=True) diff --git a/tests/units/test_sqlalchemy.py b/tests/units/test_sqlalchemy.py index 19aa90d62b5..ca8e663cdfa 100644 --- a/tests/units/test_sqlalchemy.py +++ b/tests/units/test_sqlalchemy.py @@ -1,3 +1,4 @@ +import math from pathlib import Path from unittest import mock @@ -248,7 +249,7 @@ class AlembicSecond(ModelBase): result = session.scalars(select(AlembicSecond)).all() assert len(result) == 1 assert result[0].a == 42 - assert result[0].b == 4.2 + assert math.isclose(result[0].b, 4.2) # No-op # assert Model.migrate(autogenerate=True) #noqa: ERA001 diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 5802cfa8b6f..0775dd000e5 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -6,6 +6,7 @@ import datetime import functools import json +import math import os import sys import threading @@ -387,9 +388,9 @@ def test_default_value(test_state: TestState): test_state: A state. """ assert test_state.num1 == 0 - assert test_state.num2 == 3.15 + assert math.isclose(test_state.num2, 3.15) assert test_state.key == "" - assert test_state.sum == 3.15 + assert math.isclose(test_state.sum, 3.15) assert test_state.upper == "" assert test_state._backend == 0 assert test_state.mixin == "mixin_value" @@ -770,7 +771,7 @@ def test_reset(test_state: TestState, child_state: ChildState): # The values should be reset. assert test_state.num1 == 0 - assert test_state.num2 == 3.15 + assert math.isclose(test_state.num2, 3.15) assert test_state._backend == 0 assert child_state.value == "" @@ -3461,7 +3462,7 @@ async def test_setvar(mock_app: rx.App, token: str): async for update in state._process(event): print(update) assert state.num1 == 42 - assert state.num2 == 4.2 + assert math.isclose(state.num2, 4.2) # Set Var in parent state for event in rx.event.fix_events([GrandchildState.setvar("array", [43])], token): diff --git a/uv.lock b/uv.lock index 892dd759116..6b1631fd7de 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ [[package]] name = "alembic" -version = "1.18.1" +version = "1.18.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, @@ -21,9 +21,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/cc/aca263693b2ece99fa99a09b6d092acb89973eb2bb575faef1777e04f8b4/alembic-1.18.1.tar.gz", hash = "sha256:83ac6b81359596816fb3b893099841a0862f2117b2963258e965d70dc62fb866", size = 2044319, upload-time = "2026-01-14T18:53:14.907Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/36/cd9cb6101e81e39076b2fbe303bfa3c85ca34e55142b0324fcbf22c5c6e2/alembic-1.18.1-py3-none-any.whl", hash = "sha256:f1c3b0920b87134e851c25f1f7f236d8a332c34b75416802d06971df5d1b7810", size = 260973, upload-time = "2026-01-14T18:53:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] [[package]] @@ -58,15 +58,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "async-generator" -version = "1.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" }, -] - [[package]] name = "async-timeout" version = "5.0.1" @@ -324,101 +315,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, - { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, - { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, - { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, - { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, - { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, - { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, - { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, - { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, - { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, - { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, - { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, - { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, - { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, - { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, - { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, - { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, - { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, - { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, - { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, - { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, - { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, - { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, - { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, - { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, - { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, - { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, - { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, - { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, - { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, - { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, - { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, - { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, - { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, - { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, - { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, - { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, - { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, - { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, - { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, - { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, - { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, - { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, - { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, - { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, - { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, - { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, - { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, - { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, - { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, - { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, - { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, - { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -467,115 +472,116 @@ wheels = [ [[package]] name = "fastapi" -version = "0.128.0" +version = "0.132.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/55/f1b4d4e478a0a1b4b1113d0f610a1b08e539b69900f97fdc97155d62fdee/fastapi-0.132.0.tar.gz", hash = "sha256:ef687847936d8a57ea6ea04cf9a85fe5f2c6ba64e22bfa721467094b69d48d92", size = 372422, upload-time = "2026-02-23T17:56:22.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6171c3363bbc5e01686e200e0880647c9270daa476d91030435cf14d32f5/fastapi-0.132.0-py3-none-any.whl", hash = "sha256:3c487d5afce196fa8ea509ae1531e96ccd5cdd2fd6eae78b73e2c20fba706689", size = 104652, upload-time = "2026-02-23T17:56:20.836Z" }, ] [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] [[package]] name = "granian" -version = "2.6.1" +version = "2.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/22/93016f4f9e9115ba981f51fc17c7c369a34772f11a93043320a2a3d5c6ea/granian-2.6.1.tar.gz", hash = "sha256:d209065b12f18b6d7e78f1c16ff9444e5367dddeb41e3225c2cf024762740590", size = 115480, upload-time = "2026-01-07T11:08:55.927Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/3f/9a78d70beaa2dafc54c2147dd03ce1b75a97d12b61e9319eacb6ad536e30/granian-2.6.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b8a9f6006142ed64082ec726a9de40f4eb2ebe65f842c199f254b253362b8ab4", size = 3073952, upload-time = "2026-01-07T11:06:51.277Z" }, - { url = "https://files.pythonhosted.org/packages/dd/40/52e01a382d58ba416d12e165a2ac1e3270367267ced12ddfe1dd1fb03b65/granian-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:811a8eec0f96c7a1c9f991a2922f7c21561ecb2c52263666e14474aeea37ff6b", size = 2827739, upload-time = "2026-01-07T11:06:52.637Z" }, - { url = "https://files.pythonhosted.org/packages/7f/65/439a866076b378611eeebc3ecb285cc606aad4046b3f3b26035cc82d8c8b/granian-2.6.1-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:3b78caa06c44d73551038aba9918d03d59f4d37e2bf39623e5572800d110e121", size = 3327232, upload-time = "2026-01-07T11:06:54.813Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/e9611920b7a4c3fe4881fa204322eb3944decd57ce3b38db2a3da00bb876/granian-2.6.1-cp310-cp310-manylinux_2_24_i686.whl", hash = "sha256:7cce9865bab5c2ca29d80224caad390b15c385ec23903bf0781f4cc6cee006c6", size = 3140534, upload-time = "2026-01-07T11:06:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/46/a7/3840c2a9ed7fcf1c606dc8796aa3c4e43bcc4169ba04d2f840d576d8a238/granian-2.6.1-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:b6df9ffdcbd85e4151661e878127186a6a54a8ba4048cf5f883d9093ae628b99", size = 3372279, upload-time = "2026-01-07T11:06:57.483Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/4b78ed5fab45e83c69f6e1ea8805dbc638e7694d7946e97495c507026e6b/granian-2.6.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:66e5e25bc901c4bd54ab0f6449318c678e49ef6dcfd4fd62f15188680ed9a24d", size = 3239371, upload-time = "2026-01-07T11:06:59.49Z" }, - { url = "https://files.pythonhosted.org/packages/76/7d/a9e70763e9c99158edcdaca74c4b4fd4d57a46d2ea39a0ef32df4e8262f3/granian-2.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5429fb374accfa48f6c6459b3e0278f3ad58bba701127b62163086b70c076d11", size = 3309145, upload-time = "2026-01-07T11:07:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/95/07/c899ba39d1be5810e25981d7c72a53437e2d099393fad9c8e6c273690f05/granian-2.6.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:8276b24da71771bc282057f9d144452695d54a99522adcf3a02175e613929d09", size = 3492752, upload-time = "2026-01-07T11:07:03.018Z" }, - { url = "https://files.pythonhosted.org/packages/1c/99/590ad3ad2f97c0e2f32558f0cf630ab72aa7c71f94e865084f3fb89b7cc5/granian-2.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6a3248c965f6ca71dca1285c7f5e16e4209a779a218fc2f64fe2f6cbe5ace709", size = 3498818, upload-time = "2026-01-07T11:07:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/e7834fef65b64f99cbf68d785540eb621d5e7f83651e5134389cd04fcb03/granian-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:e6b5cecd96b0f9dade619ec673c648c0b31f75e0adde262f09ae97f1631fac7b", size = 2345367, upload-time = "2026-01-07T11:07:06.206Z" }, - { url = "https://files.pythonhosted.org/packages/66/a3/12e30c4a16761f6db3cff71a93351678dca535c6348d3c1f65f6461c8848/granian-2.6.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66cb53293a721bf2da651150cb5ba501d5536c3bec284dcbcb10a9f044a4b23e", size = 3073572, upload-time = "2026-01-07T11:07:07.447Z" }, - { url = "https://files.pythonhosted.org/packages/11/c0/0582a42e5b3e3c9e34eb9060585ed6cd11807852d645c19a0a79953be571/granian-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fef1c4b75d3827101a103b134abf43076703b6143b788f022e821dc4180b602", size = 2827569, upload-time = "2026-01-07T11:07:08.964Z" }, - { url = "https://files.pythonhosted.org/packages/db/b8/306dad81288330c5c1043434ac57a246a7cd3a70cd5877570efcd7888879/granian-2.6.1-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:2375c346cafd2afd944a8b014f7dd882b416161ffe321c126d6d87984499396c", size = 3326925, upload-time = "2026-01-07T11:07:10.288Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bb/f76654e4e5679d000335101922653c809adacaa675f861646aef95e9673c/granian-2.6.1-cp311-cp311-manylinux_2_24_i686.whl", hash = "sha256:6c0e9367956c1cdd23b41d571159e59b5530c8f44ff4c340fe69065ffd1bfe70", size = 3140557, upload-time = "2026-01-07T11:07:11.764Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0d/2e6ab1ce28fbb45f8e747d33db06ea870c1eee580c584592a8ceb49c0a59/granian-2.6.1-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:4eacfe0bf355a88486933e6f846c2ecc0c2b0cf020a989750765294da4216b0c", size = 3372055, upload-time = "2026-01-07T11:07:13.584Z" }, - { url = "https://files.pythonhosted.org/packages/5d/05/9f104225ef0ceef6770e12d476077656c7930cde84474797c4a9807a4d3d/granian-2.6.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c6ac45432028c7799a083917cda567e377cf42dbcad45c24b66dd03b72b1e1d6", size = 3239306, upload-time = "2026-01-07T11:07:15.01Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ec/73ead13fe326ac548fda5f85f471e16015629672e8acc3d4ccc07e9b313a/granian-2.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aaac0304c7c68e6b798d15dd96a7b6ae477ab89d519c398d470da460f7ddda0", size = 3309025, upload-time = "2026-01-07T11:07:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fb/0f474c6d437464d440924f3261295c046365dc9514cdd898d152b5a6c0bd/granian-2.6.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:a356685207987c07fb19f76a04d7bac6da0322584ede37adb1af7a607f8c8e35", size = 3492393, upload-time = "2026-01-07T11:07:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/30/92/dbd3793e3b02d0a09422dacd456d739039eba4147d2c716e601f87287fde/granian-2.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2928057de92ef90c2d87e3de5e34af4e50d746c5adfb035a6bbef490ec465af", size = 3498644, upload-time = "2026-01-07T11:07:19.943Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f4/d105d1d25ca91ccc37441678be088e4f0c9f787c17935cb216bd7db7001e/granian-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:1681662bf2e61d2fe703e6c5056d2a1f3d2129161e46d138c873d90ab013e1e7", size = 2345360, upload-time = "2026-01-07T11:07:21.303Z" }, - { url = "https://files.pythonhosted.org/packages/50/d1/9d191ea0b4f01a0d2437600b32a025e687189bae072878ec161f358eb465/granian-2.6.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:801bcf7efc3fdd12a08016ed94b1a386480c9a5185eb8e017fd83db1b2d210b4", size = 3070339, upload-time = "2026-01-07T11:07:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1e/be0ba55a2b21aeadeb8774721964740130fdd3dd7337d8a5ec130a0c48c0/granian-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:853fb869a50d742576bb4f974f321242a71a4d8eed918939397b317ab32c6a2d", size = 2819049, upload-time = "2026-01-07T11:07:23.877Z" }, - { url = "https://files.pythonhosted.org/packages/78/c7/d8adb472dc71b212281a82d3ea00858809f2844a79b45e63bbb3a09921b7/granian-2.6.1-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:327a6090496c1deebd9e315f973bdbfc5c927e5574588bba918bfe2127bbd578", size = 3322325, upload-time = "2026-01-07T11:07:25.304Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/c3ce9e4f19163f35c5c57c45af2ad353abcc6091a44625caec56e065ca4a/granian-2.6.1-cp312-cp312-manylinux_2_24_i686.whl", hash = "sha256:4c91f0eefc34d809773762a9b81c1c48e20ff74c0f1be876d1132d82c0f74609", size = 3136460, upload-time = "2026-01-07T11:07:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/3d/87/91b57eb5407a12bfe779acfa3fbb2be329aec14e6d88acf293fe910c19e5/granian-2.6.1-cp312-cp312-manylinux_2_24_x86_64.whl", hash = "sha256:c5754de57b56597d5998b7bb40aa9d0dc4e1dbeb5aea3309945126ed71b41c6d", size = 3386850, upload-time = "2026-01-07T11:07:27.989Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/b61a6f3bfc2f35e504e42789776a269cbdc0cdafdb10597bd6534e93ba3d/granian-2.6.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e849d6467ebe77d0a75eb4175f7cc06b1150dbfce0259932a4270c765b4de6c4", size = 3240693, upload-time = "2026-01-07T11:07:29.52Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1d/c40bd8dd99b855190d67127e0610f082cfbc7898dbd41f1ade015c2041f7/granian-2.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5a265867203e30d3c54d9d99783346040681ba2aaec70fcbe63de0e295e7882f", size = 3312703, upload-time = "2026-01-07T11:07:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ca/589c042afc3287b36dfeed6df56074cc831a94e5217bcbd7c1af20812fe2/granian-2.6.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:03f0a1505e7862183203d7d7c1e2b29349bd63a18858ced49aec4d7aadb98fc8", size = 3483737, upload-time = "2026-01-07T11:07:32.726Z" }, - { url = "https://files.pythonhosted.org/packages/6f/51/72eb037bac01db9623fa5fb128739bfb5679fb90e6da2645c5a3d8a4168d/granian-2.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:703ed57ba134ab16f15d49f7d644329db1cb0f7f8114ec3f08fb8039850e308a", size = 3514745, upload-time = "2026-01-07T11:07:34.706Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/be9d5e97d3775dfc0f98b56a85ad6c73d7b0ac4cfc452558696e061d038d/granian-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c771949707118116fa78b03511e690cb6c3bd94e9d84db7c2bdfe0250fecc80", size = 2349022, upload-time = "2026-01-07T11:07:36.484Z" }, - { url = "https://files.pythonhosted.org/packages/4b/3f/40975a573dc9a80382121694d71379fffab568012f411038043ed454cdd0/granian-2.6.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8af4c75ffa2c8c77a3f5558b5ff71a9d97a57e08387ef954f560f2412a0b3db9", size = 3069408, upload-time = "2026-01-07T11:07:38.4Z" }, - { url = "https://files.pythonhosted.org/packages/a0/2f/64b0d58344eedfb7f27c48d7a40e840cd714a8898bcaf3289cecad02f030/granian-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b473cdaf3f19ddc16e511b2c2d1e98b9ce7c13fd105e9095ecb268a6a5286a32", size = 2818749, upload-time = "2026-01-07T11:07:39.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1e/e33ae736adbef0633307f13092490021688df33885c9a320b50b83dbc076/granian-2.6.1-cp313-cp313-manylinux_2_24_armv7l.whl", hash = "sha256:b8ca2ac7261bcb8e57a35f8e7202aa891807420d51e3e61fd0913088d379e0fd", size = 3321824, upload-time = "2026-01-07T11:07:41.379Z" }, - { url = "https://files.pythonhosted.org/packages/59/3c/ef25189d251d14c81b11514e8d0a9a3cd8f9a467df423fb14362d95c7d6a/granian-2.6.1-cp313-cp313-manylinux_2_24_i686.whl", hash = "sha256:1eca9cfcf05dc54ffb21b14797ed7718707f7d26e7c5274722212e491eb8a4a6", size = 3136201, upload-time = "2026-01-07T11:07:42.703Z" }, - { url = "https://files.pythonhosted.org/packages/4f/31/a5621235cd26e7cb78d57ce9a3baeb837885dc7ebf5c2c907f2bf319002a/granian-2.6.1-cp313-cp313-manylinux_2_24_x86_64.whl", hash = "sha256:7722373629ab423835eb25015223f59788aa077036ea9ac3a4bddce43b0eb9c9", size = 3386378, upload-time = "2026-01-07T11:07:44.289Z" }, - { url = "https://files.pythonhosted.org/packages/22/ce/134a494be1c4d8a602cc03298e3f961d66e4a2b97c974403ffce50c09965/granian-2.6.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7de143cf76934cfc63cc8cf296af69f750e4e3799ec0700d5da8254202aad12a", size = 3240009, upload-time = "2026-01-07T11:07:46.18Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f5/48f2fff5effee50dce30d726874936d582e83a690ccdc87cc2ca15b9accf/granian-2.6.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b0e7d6ee92962544a16039e1d5f36c5f43cd0a538541e7a3e5337147c701539", size = 3312533, upload-time = "2026-01-07T11:07:48.176Z" }, - { url = "https://files.pythonhosted.org/packages/a2/90/0590100351bf2b99ff95b0ac34c8c14f61c13f8417c16b93860fe8b1619a/granian-2.6.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:d9ca799472c72cb378192d49004abe98e387c1719378b01d7dc85ab293fa680e", size = 3482213, upload-time = "2026-01-07T11:07:49.471Z" }, - { url = "https://files.pythonhosted.org/packages/22/12/0f8f1367ebc4bbd3e17bed842ad21cf9691d828b8c89029dfcf9f1448fcd/granian-2.6.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:616c3213d2ffe638e49a3578185cbe9d0009635949e7ac083275942a2cbbee0c", size = 3513942, upload-time = "2026-01-07T11:07:51.011Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9d/7322fc9b4809d848cad18f0837bf773539cacbdd82d7f18f426c77a2b8ae/granian-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:2bab7e5c8c4f13ba78bbab66977b77bcb5b351c68c44b7770bcadde11222d784", size = 2348487, upload-time = "2026-01-07T11:07:52.508Z" }, - { url = "https://files.pythonhosted.org/packages/64/ec/49ada0ed6861db9ba127c26058cc1a8451af891922cb2673b9e88e847cf6/granian-2.6.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:fd3151933d867352b6e240b0e57d97b96cd6e0fa010c9e3503f4cb83e6815f6b", size = 3030683, upload-time = "2026-01-07T11:07:53.803Z" }, - { url = "https://files.pythonhosted.org/packages/71/7e/4c8b9eb66555e95798b8cf5e18be910519daf6cdf3c8cac90333ffe8e031/granian-2.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1d11bb744878fa73197a1af99b324b6ccd9368f2c73b82a6c4cfcc696c14dcde", size = 2772412, upload-time = "2026-01-07T11:07:55.605Z" }, - { url = "https://files.pythonhosted.org/packages/15/20/013495365505da26d45721b670114855a8969f1d3784ecdc60a124330075/granian-2.6.1-cp313-cp313t-manylinux_2_24_armv7l.whl", hash = "sha256:fe1103a49cdb75bbac47005f0a70353aa575b6ac052e9dc932b39b644358c43a", size = 3317657, upload-time = "2026-01-07T11:07:57.472Z" }, - { url = "https://files.pythonhosted.org/packages/63/1e/4070bb26876c12b1304da9d27d6309e3dc53bbdf5928d732ba9a87fe561a/granian-2.6.1-cp313-cp313t-manylinux_2_24_i686.whl", hash = "sha256:e965d85634e02fbf97960e4ba8ef5290d37c094ad089a89fb19b68958888297a", size = 2969120, upload-time = "2026-01-07T11:07:58.778Z" }, - { url = "https://files.pythonhosted.org/packages/08/3e/dd29f04737a554acd3309372c8d2c6901a99cac3f9fcdee73bbe5a9bf0aa/granian-2.6.1-cp313-cp313t-manylinux_2_24_x86_64.whl", hash = "sha256:570c509cf608d77f0a32d66a51c9e4e9aba064a0a388776c41398392cc5e58d3", size = 3263555, upload-time = "2026-01-07T11:08:00.133Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b6/ad4c439a8a20bebbed5066d051150ed0ad32160aee89c0cbdcf49fb1b092/granian-2.6.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3a105aa2f9b6dba037f81bc36b49a61c1648b60a82020e5c34394ce0940cdaef", size = 3112547, upload-time = "2026-01-07T11:08:01.453Z" }, - { url = "https://files.pythonhosted.org/packages/b8/92/58b5c7ca48540232034f2fa8be839e2ec997ec723489ff704a02c5a19945/granian-2.6.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:a4d90804f8d7c61e01741d9ccd400257763670f0e52a3cb59829fe2465b2b4a1", size = 3304759, upload-time = "2026-01-07T11:08:02.967Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e0/fdcf7d91937acf6bfbe6c459820ffc847840d0375624daf1fcd3e2acea50/granian-2.6.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:647a818814c6c2a4bcd5757509596d9d5a0e10fbe96d52acb4c992f56134ae27", size = 3479270, upload-time = "2026-01-07T11:08:04.337Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/d40f9e8699c9bb6ebf923e6baee6c9f90b9ff997c075173f67ffdee9aefc/granian-2.6.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:20639cec0106f047147c6b873bce6aaa275fb71456810ce3c0124f35b149e408", size = 3509699, upload-time = "2026-01-07T11:08:06.215Z" }, - { url = "https://files.pythonhosted.org/packages/10/8a/c1eeffa51896eea15b437aa2e01996889ca355abc9eabecdd905f1fea5e6/granian-2.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:70dcb7651eff9211da5b06477dc80a1f73a4eb1bc11656cfe7576066ef061c9f", size = 2347317, upload-time = "2026-01-07T11:08:07.805Z" }, - { url = "https://files.pythonhosted.org/packages/55/49/2a3f993a2ee5e9876811a9d5fc41f47582da9a6873b02f214271e68f539b/granian-2.6.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:021ed3abb6805be450136f73ddc15283fe83714135d4481f5c3363c12109ef43", size = 3067322, upload-time = "2026-01-07T11:08:09.136Z" }, - { url = "https://files.pythonhosted.org/packages/04/86/ad139301cc61c97ef8d8ec9a96e796350f108fa434c088ea14faa5e43e81/granian-2.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:df10c8fb56e00fd51aaccb639a34d067ec43cfbf1241d57d93ac67f0dbebd33d", size = 2818936, upload-time = "2026-01-07T11:08:11.091Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a7/5223956aed93671dd83094cb4883e6431701e07a7641382b8cde6f4328d3/granian-2.6.1-cp314-cp314-manylinux_2_24_armv7l.whl", hash = "sha256:8f0eaacf8c6f1be67b38022d9839b35040e5a23df6117a08be5fc661ca404200", size = 3319401, upload-time = "2026-01-07T11:08:12.817Z" }, - { url = "https://files.pythonhosted.org/packages/90/ab/cbd1e9d9d0419b8d55043c821ea5b431d9cd43b35928c4c9f2034f9abf34/granian-2.6.1-cp314-cp314-manylinux_2_24_i686.whl", hash = "sha256:2f987bdb76a78a78dee56e9b4e44862471dcdbc3549152e35c64b19ba09c4dd0", size = 3132180, upload-time = "2026-01-07T11:08:14.215Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cb/4e4aa44e696c10a701c1366b9882561d6ba5caa2e3c00e3716a57da40c18/granian-2.6.1-cp314-cp314-manylinux_2_24_x86_64.whl", hash = "sha256:ffb814d4f9df8f04759c4c0fc1c903b607d363496e9d5fcb29596ab7f82bfae0", size = 3383751, upload-time = "2026-01-07T11:08:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/72/10/f37497b41c8f2a49fbc7f17dafdb979c84084ea127e27bfe92fe28dcd229/granian-2.6.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:450d45418116766cce8d2ef138eeea59e74d15da3759ae87af9732a8896ca3ef", size = 3239746, upload-time = "2026-01-07T11:08:17.75Z" }, - { url = "https://files.pythonhosted.org/packages/33/c5/842eb481cba2dc49374ffabb5a0ffeb8e61017da10a5084764369b1ac452/granian-2.6.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6e85ca2e6c83b5d70801068fbdca8acf733e12e35cee782ee94554f417971aff", size = 3311896, upload-time = "2026-01-07T11:08:19.3Z" }, - { url = "https://files.pythonhosted.org/packages/9b/6e/db253ae3ab220a394ffcd0cc7c3d7752ba20757bf37270af8f7b6813cd59/granian-2.6.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:aa4d187fd52e2873ef714c1d8aee097d88c141ac850f5bf04dbd37b513a3d67b", size = 3480487, upload-time = "2026-01-07T11:08:21.037Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1d/0a78570d2c1fcccd5df93ff18e2cf00a708a75b9e935c038d973554f5f87/granian-2.6.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:53e2380f2b522082d5587c5dc05ad80ae0463dc751655d7cfb19d7ed3dbc48d6", size = 3509926, upload-time = "2026-01-07T11:08:23.067Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/5b00a83a02af67cc7e9f1503edb5a926a05a81d55fbc8f7a1a79a8122e71/granian-2.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:079e03f0b80f4373b1b3aef5ef7a20691dfa9bf254219f58b8ac99e91e003c8b", size = 2347446, upload-time = "2026-01-07T11:08:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ed/f1130abe166f5a9bb4c032bae94b4cbc1b04179703a98906493e3b329974/granian-2.6.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:997a2a45b14e583504b920abfbcaf605d3ee9a4e7e0d3599b41f2091b388bd81", size = 3029240, upload-time = "2026-01-07T11:08:26.17Z" }, - { url = "https://files.pythonhosted.org/packages/cb/40/f5ec53011f3d91c14d6be604b02df930069cdb2dddbeb6aabda0ce265fc4/granian-2.6.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ee9ac64109dd54ff6773a82e9dc302843079edd7d9fcca4a72d9286918ae2c03", size = 2771824, upload-time = "2026-01-07T11:08:28.267Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/a75d6d3b47d0a44e7af0ee7733ea295c01fb0070a636cb93290a6075da35/granian-2.6.1-cp314-cp314t-manylinux_2_24_armv7l.whl", hash = "sha256:4a76bbfcfea1b5114bda720fd9dbc11e584e513c351c2ec399cd3e2dcb1f8fd2", size = 3313753, upload-time = "2026-01-07T11:08:30.556Z" }, - { url = "https://files.pythonhosted.org/packages/3d/33/c37f0c5dd649ff97bed0b70d36b8f02fd488d926c30b272f6d09618ccde7/granian-2.6.1-cp314-cp314t-manylinux_2_24_i686.whl", hash = "sha256:5f3c6bab1e443b2277923b0f836141584b0f56b6db74904d4436d6031118a3fa", size = 2966697, upload-time = "2026-01-07T11:08:32.52Z" }, - { url = "https://files.pythonhosted.org/packages/5a/71/92dd02f5b53d28478906fc374a91337e65efeae04be9f392bdcb4a313906/granian-2.6.1-cp314-cp314t-manylinux_2_24_x86_64.whl", hash = "sha256:bbc0cac57a8719ae0d96ee250c539872becdd941795ab4590dca49cba240c809", size = 3261860, upload-time = "2026-01-07T11:08:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ca/abab95b8b3541a5e540a3b89a0b2805cdd1b064c9083cd4ada71c7d1ac76/granian-2.6.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dac537c663ec9b2f1f8bd3c0c6fd0c3f749d91b4f35f918df9ea1357528360dd", size = 3111002, upload-time = "2026-01-07T11:08:35.355Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3f/b872cca9f39b98aaeccf95f4c5b10fca21562c623bb3f7115de4506e9e1b/granian-2.6.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:9d6a3ac7234708fb2cce1fec6dd0941458c45e26b98b377b4a08d715fe9a9bd2", size = 3303389, upload-time = "2026-01-07T11:08:36.767Z" }, - { url = "https://files.pythonhosted.org/packages/5d/39/2224e99dbd2c61aed585550f5a624b653a5c90b6ea7821c7a1e1c187d4cb/granian-2.6.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:75bb8af9306aaceade90ec91cd9f5d4bdb5537b221b31e5a358b1eadb57279ad", size = 3475366, upload-time = "2026-01-07T11:08:38.29Z" }, - { url = "https://files.pythonhosted.org/packages/72/01/5077972a90cee7ef28c03a02b35c15d111416b25ccf8f8fda8df9972b6ce/granian-2.6.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:b7c1a0f24bd079bab8f6550fbb277e09c187dd5fe15e24ea3f2ace78453e5b7b", size = 3507600, upload-time = "2026-01-07T11:08:39.691Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/9674065b762bde8db4e5d332c3603a881dc63de39a6d519333566bb9c34a/granian-2.6.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8b1d20e8825e4eb25fb85f022dbba1f83725ce90a5f478d463dcb9ed6fccb36e", size = 2346412, upload-time = "2026-01-07T11:08:41.601Z" }, - { url = "https://files.pythonhosted.org/packages/e1/eb/8965002085f93cc1a9887414f43aed0d23025a7d4a4965c27d23d2d9c3c6/granian-2.6.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c14a8708066e396a56673cc23acff8174fff613c433f1da0746c903341e4c22", size = 3077208, upload-time = "2026-01-07T11:08:43.752Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8b/5e08ad10d2cfde71cc438bc3887f168f7845e195f67656d726c36bfbfa0f/granian-2.6.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4e3041bc47b6add143e3944c5bb0d14cd94b5b9722812319d73c24d24816b039", size = 2813151, upload-time = "2026-01-07T11:08:45.094Z" }, - { url = "https://files.pythonhosted.org/packages/82/1b/dcb6c44a059b0a6571da1d4abe329a30c7e40c49e7a108e963a7b8c61a4c/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:9635f707119a8bdc22ebfd70b50944a72a7e791905b544ac64938f4e87a8060f", size = 3357183, upload-time = "2026-01-07T11:08:46.952Z" }, - { url = "https://files.pythonhosted.org/packages/a3/18/b8e976b0bec47edb5d469a3c4e44d8cad3383ffb6b8202eba35249a23845/granian-2.6.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:566941333bed75345583c4ff0a72783812c68c5f87df3f9974e847bfcfb78d3e", size = 3233117, upload-time = "2026-01-07T11:08:48.354Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e7/939eb934e4de6faa3b60448bf233610aec39e6186b0da212179cedce3baf/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4a85fa1f42fd54feda9c70a7ef349259da6c5d81f9d5918633c677b7be8238ba", size = 3306125, upload-time = "2026-01-07T11:08:49.848Z" }, - { url = "https://files.pythonhosted.org/packages/0f/30/b99516161444c2528b9ab9e2633060c226f7f6ddf2d24464fb0d3b3f86ce/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:7787cfc9b79a298289ff728d26937eac95482fcb468ef912d9178e707889c276", size = 3485546, upload-time = "2026-01-07T11:08:51.479Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/30b0db6729767eac2249856b563a9f71a04b7eb8ce44321b7472834dcb19/granian-2.6.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d3fd613953ea080f911d66bbebd46c8c4b3e66fbb293f78b13c86fb3ec0202ae", size = 3497427, upload-time = "2026-01-07T11:08:53.065Z" }, - { url = "https://files.pythonhosted.org/packages/00/26/4fc11dd4462638207d09b810761a1962bef6c5fdac4bc65a0a5a57a37d75/granian-2.6.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:36c8624bcdbc94b40ef03bae868485963a529d25a702c28f90e160c213cbf730", size = 2341895, upload-time = "2026-01-07T11:08:54.664Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e5/e5/c3a745a2c60cba6e67c5607fe6e18883fd2b7800fd7215511c526fab3872/granian-2.7.1.tar.gz", hash = "sha256:cc79292b24895db9441d32c3a9f11a4e19805d566bc77f9deb7ef18daac62e16", size = 128508, upload-time = "2026-02-08T20:02:31.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/0b/50d9f2c911742140fc719db522f20ac16b8d33e845704cb9f80948af8596/granian-2.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:800140b3cfd80360e82f5fca8e3846deecbe8f8accb2fc4a6ff7af73dcfda001", size = 6405858, upload-time = "2026-02-08T20:00:23.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/50e910c7b8cc922393f921ef41ca33e3574ccd55d29625eb04177ede8c8d/granian-2.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5597681fb4273f71f48e7c7c639f36fa9b66a8431fccd21f56a723890e624dc4", size = 6107724, upload-time = "2026-02-08T20:00:25.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/b0/2d5471893d7c35f1b85854b27ddab65743eec66fa94fd80daa7b71ae0330/granian-2.7.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abad081f70f09b42fdf4d6063e61e388b1b4051c40103129d6fb649cb2903e3c", size = 7144534, upload-time = "2026-02-08T20:00:27.691Z" }, + { url = "https://files.pythonhosted.org/packages/2f/cd/11c1eb0d2a17ecedecc03d503b23011d2ad88e99678edcf1ae80a4961f7d/granian-2.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0b168da29737c1c46b5949189c9b35a177d58dce31ae4834870395990350fc", size = 6353619, upload-time = "2026-02-08T20:00:29.102Z" }, + { url = "https://files.pythonhosted.org/packages/76/6d/6d1826de26f1810b9288b961de68032083a067b880029f04ea446241ca52/granian-2.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3135a9b895585f28ae09162e581f36322adf9d7505e3dad4b705211760f9c9c", size = 6906618, upload-time = "2026-02-08T20:00:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f1/27f220de51e3cb37d5e2a0cdfdbe6075a23787d62e9d911eb2786ab14953/granian-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9cf191fec7e91287dc7a885edfc82b40cf3b2bef0c4b5aa2209a0fcc31b68e01", size = 6974809, upload-time = "2026-02-08T20:00:32.451Z" }, + { url = "https://files.pythonhosted.org/packages/a5/98/778018938e51e2e43eeaded3cefc5ef595db084a1a24965702ca555ce468/granian-2.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0eba7a11d2d7283ab59ae2b8fefcc45ff46949313e2f46ff8fbf8d7a6c0115b9", size = 7031723, upload-time = "2026-02-08T20:00:33.819Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b3/f53f1160d5e4c0c00dd38aad793f5b2c1fae191cc180f355be92e39a6485/granian-2.7.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f7133e8553e3b91c9484a69f3d2e040b618ba9fe71e8d0df9781084b41827aca", size = 7312565, upload-time = "2026-02-08T20:00:35.557Z" }, + { url = "https://files.pythonhosted.org/packages/59/d4/89cb7484e1910f77b51be41c8f5d5709dea00d82b6b63bbe15b6ff8f4974/granian-2.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4008dca32d36089060cfdcf1973db01ee373ffd639433a760cb597a0ea1a29a", size = 7013843, upload-time = "2026-02-08T20:00:39.171Z" }, + { url = "https://files.pythonhosted.org/packages/e0/11/3c7fd79b99a12d5505385a3dcfe899e01ebc46b0219fd319c0716719251b/granian-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d7ff9ebd4736da8597c642b3df0e4087aaf0c64f0d15bbe5415e7c2811b599", size = 4059013, upload-time = "2026-02-08T20:00:40.526Z" }, + { url = "https://files.pythonhosted.org/packages/27/fd/44b8027007de2558d09ff7ee688229ad5d4f368bb166589a2547926057e4/granian-2.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bbcdea802c5a594d204b807de6829a7d4b723c397087857ca4d3a3cf2ac1d16e", size = 6447686, upload-time = "2026-02-08T20:00:41.829Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/db0b26c9226490fb42d51fa70fd08e8daf5ad9747d60d2dc143dd2517b3d/granian-2.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1abc6dfe5d5fb1f2e863200ee9edf749ed82ff9c1361c21483b214a91654879", size = 6154446, upload-time = "2026-02-08T20:00:44.1Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1b/44d8acdfda1a1af2c4fa8ba215912bd78318b59f195c5b7831dab69a7719/granian-2.7.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:edf7cbab2c54a3dd10c0f8a737b133cc605b6309acdfe3aa060bc954d7ae13c5", size = 7144519, upload-time = "2026-02-08T20:00:45.504Z" }, + { url = "https://files.pythonhosted.org/packages/be/ac/6e142e3a26c3fe90d7e6592256ed4940e696f4430933d597e4014b5ee441/granian-2.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5240510754712cc802ad5a71507f10efdb83a043dbccd351662897f58916a76a", size = 6353689, upload-time = "2026-02-08T20:00:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/37/49/1836d259060ceae6cf1dc7d0c424864786ac028c93aaeed07f6ea9dfcafc/granian-2.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2c445c13fa6fc7235f95c28f2d203369d0c516aba15ba24faad08ca0a095bd0", size = 6906248, upload-time = "2026-02-08T20:00:48.15Z" }, + { url = "https://files.pythonhosted.org/packages/5c/84/0d18018b05652991c8502da2cbab6b9b8c234926870d0458d2d7c5124a65/granian-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71776d7319906cfc78f723cc38f927ffaf58bcb9b1707fe5d88c3662827aa1f7", size = 6974742, upload-time = "2026-02-08T20:00:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/19/83/f9c3685681aa4b41feb73def9ef63800b6f639629e9b083a0c279583fb92/granian-2.7.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab6da78f0fcecf9a9177db2d716e50214b540cb1ea77dafc88e35184ca901266", size = 7030837, upload-time = "2026-02-08T20:00:51.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/62/c445c0c96552f11dee49d002d4af32adbeca19b7e8064a1d106952810345/granian-2.7.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:025218f8ccc5907bc8277b0df9a60927a5862ee607606cfc970cc404d5346af6", size = 7313823, upload-time = "2026-02-08T20:00:53.787Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bc/c9d1dce0b2d11bf76aadd06608d3b01a2b697c030c5ea01474d15e36e2af/granian-2.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98ab412772f2c66260a3535da4101ccc6dd20de30e74a87b32fd7abc729cc14f", size = 7014570, upload-time = "2026-02-08T20:00:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/51/2abe731a4ec42038a0ea24695bd6fd79d4b340797115bd1af40c21cfd1a3/granian-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:ba65410b56d951d9aa2e8b0b0f7796431052c43eca2bb8a526a743d2f8aa539f", size = 4058148, upload-time = "2026-02-08T20:00:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/4a29e3b654ad38b0a7b1fb477a20a1d03b36a40060d15bd98f43654aac3a/granian-2.7.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d603c53a8d7e6243a5c4b9749116143f4a6184033777451ba376b038905ac57f", size = 6390662, upload-time = "2026-02-08T20:00:57.999Z" }, + { url = "https://files.pythonhosted.org/packages/ab/38/bf86291a04d1d4fd7b469b0134224cdb0cafa4e7cc8de5744f79d045ff5c/granian-2.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:df3e8f617dc3e21e3a4e543678993e855fb1d008f1207c646d27efd45e45161b", size = 6126936, upload-time = "2026-02-08T20:00:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/69/09eea196a4f9883dad20d4acd645be35242c0004ba4a698f73f9e0fe8291/granian-2.7.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1007f1b58e4ace682d424789dd34b63526a482ba3efc01ca18098b65420d6d", size = 7120523, upload-time = "2026-02-08T20:01:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/db6b3504a41e222a1d94417995f73fa17a27dc2fc664c29295dfc34bd64b/granian-2.7.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76b1751c5d5dcc93803e37baf68396dba22d809001037faec4b2df8fdc52af7e", size = 6420419, upload-time = "2026-02-08T20:01:02.189Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ef/5d6712ad81e85841d4fd5436f5cbfcdb3ac3ddeb9e75953fd6b323bfff64/granian-2.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a02a150c6a1ba8a7123634a22c0352a116ea2211e634479e9f64409db72d4489", size = 6895176, upload-time = "2026-02-08T20:01:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/90/f5/bd0fafc93f01f345ad1ecc70fbb459e452c777fe8b4958020399332b7f03/granian-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:24f09f5dbb9105498e521733e5993135fb276e346ce8f04cead2f4113ca51bba", size = 7002315, upload-time = "2026-02-08T20:01:05.071Z" }, + { url = "https://files.pythonhosted.org/packages/af/ff/b17d357d4f1eff19ff45257ea924bb571d4cf2caefccdc8aca8c0b1a3c7a/granian-2.7.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cc435c5d1881554bf7eb4e2fe8d2ad7e5052a0bacc7195c477bfc97544c7bf46", size = 7018969, upload-time = "2026-02-08T20:01:06.564Z" }, + { url = "https://files.pythonhosted.org/packages/d4/68/e0e24673e943fbb2540a7cd68dd3ea10a4cd9db6f538de9cec26b1c54133/granian-2.7.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:68a136b5d7ade34f3ee5ee743b2bdd55d6c1f0249c6bfdc8e038c6d0846de61e", size = 7274801, upload-time = "2026-02-08T20:01:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/cda1e8eb3e7025d82b6594814fc2f95ce252f638691240e4bc523924e204/granian-2.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93100bd3185e653c482c2996e11a7ece58ea28e355ef335bb0a30e4851c3ae8c", size = 7032826, upload-time = "2026-02-08T20:01:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/2c89fa53f5cdbc8495f55d587f3fa24f9ff984a8c572dd8930aa991e4301/granian-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:6cab79a863ccf6f18aa8b5e9261865d87c28574cd85174e8bb1bab873220077d", size = 4076284, upload-time = "2026-02-08T20:01:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/e8ae26e248daaa8e782c0e6bce1350759da262f8aa637b8a0036c5455376/granian-2.7.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b4f0c807fefedfa58d07c2751cc40471765387d331e70ea7ebd2a2ff5d492ca0", size = 6384691, upload-time = "2026-02-08T20:01:12.389Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/32f933dac26835ad2f8bc9b4f5762be8f8340318a9bbeca75b32fa6f6195/granian-2.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:78ce501ec337b7db52ba1773c0acf0abd72b3fac71b6b747fe4ae6f38cca0a6b", size = 6128567, upload-time = "2026-02-08T20:01:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/432b73f713ebb102e1585f5abec9cb2284d76f4d16df73c24f2e4dcc9cbd/granian-2.7.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2079c9c29b65404283ef61ced11905c8491e4bc68a4e3b56c684fe2dab8cf8c2", size = 7129893, upload-time = "2026-02-08T20:01:16.526Z" }, + { url = "https://files.pythonhosted.org/packages/63/5e/fdd4e42c800804cc277f12a3eba51747d100739b8beb0c1a909837670d86/granian-2.7.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a68bf02c93c2137c68e2acd1dc68e871f49ce2e61b042fec9a145104daf3d5b", size = 6428486, upload-time = "2026-02-08T20:01:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/7a5632e1a206e11ac3470f9ef79b2aadce67d1dfc5cdf75a5fd9795ae0fa/granian-2.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3f44b244600103a3ad6358937a42370b8cc518b7754c740620be681272e0bd", size = 6888218, upload-time = "2026-02-08T20:01:20.393Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ef/379b77fc6f8909ffc4d9397135b122d93446f303f52e428aca1120d79b08/granian-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7681e76c61af0dd1e135139f5fa9561ec16fdbac19d0a9fbf4617079b822bf21", size = 7007452, upload-time = "2026-02-08T20:01:21.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/49/6849f1f784186f41551ceba040e4402d7daa7a9c5c89e0b4c0fb7df5d73e/granian-2.7.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bc56766803ec0f958f4f2e3be9f4cb2385f9d6970e34ade6ff5c0ba751a3ce9c", size = 7024506, upload-time = "2026-02-08T20:01:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/dcbc5b860697e1ebf9fa4206d3fba931a2ea2547fb8d2638ad392f4d5a90/granian-2.7.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a20eaf1b756981caa8c0d6c19c5467e03386aadb07f854b88243218c9db9513b", size = 7289505, upload-time = "2026-02-08T20:01:24.59Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0f/3ddd893a4582943ab21c59853b7a6adae837130445ad64964cd73ea77ce4/granian-2.7.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:af842b07f14d7433774627c16fb0fbcdc9e60587d2d684636d2eba446c343297", size = 7022894, upload-time = "2026-02-08T20:01:25.973Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/52173568f8da3a2d50f48eabe1cc19d857586e0878009477ed0c196ebebd/granian-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:861d69fc3504c891f152585c2109d1eaf791c35392b13ed22c72fb199dc50dfa", size = 4093077, upload-time = "2026-02-08T20:01:27.735Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/3e0ea25a85a05618363ac9f90eb4e504ccc00e48c64f30cd37ef7046097f/granian-2.7.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:33ed73fe753fcae51a647555614fc67013558a654d323115ab0fbf60aca6c47a", size = 6354066, upload-time = "2026-02-08T20:01:29.268Z" }, + { url = "https://files.pythonhosted.org/packages/46/8d/a8965de519507ba5dfa13af4760b3c1b334e46bf3283eab55f171693de0a/granian-2.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de9367e2dca2923bf12b52f004ab975ed0de45c8dedddd87993ed9fffabfb0ce", size = 6049800, upload-time = "2026-02-08T20:01:30.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/ff76aab55b5a7bdbd20f4f73486fcb5a09440f4fd56bd3dc6266e65dee9a/granian-2.7.1-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:38088f6bd4780b280aae8abf15c2205bdf9066def927f8c9690c13a966519286", size = 6219241, upload-time = "2026-02-08T20:01:32.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3a/7aaf34391df169d54bcc3bfc32919b58de9b8a9e28e66b4f3276b910ef68/granian-2.7.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ebbc04483ada6e1a8a89f055de0b4cad2f90b3cbc94a1ae08fc2b140d905f4b", size = 7114695, upload-time = "2026-02-08T20:01:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/540a9f816884abf4da62d2e411455968a1ee8e4685243d3dd7fee1cf375f/granian-2.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2acc29a3eb9b1b9708355abd5438c216caff4ba4536bc77e46b19e44fb1b37ea", size = 6775127, upload-time = "2026-02-08T20:01:35.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/d133c36fdab4552db665d6bb2d53ac4834e41a97d8d0244f1aacc03e188f/granian-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:bec493af655645e58e6d89c7e37eb7751e9bf827506286e765d79a5c4ff10a3f", size = 6847644, upload-time = "2026-02-08T20:01:37.282Z" }, + { url = "https://files.pythonhosted.org/packages/21/4a/619d699acd3cd37de048ab606a85021f5edf42bd54c7f081d20dccd48041/granian-2.7.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:dc51944736d5683b255b7cd33581daf8bc44ae1dab31240e1969eca13d1e75cf", size = 7011427, upload-time = "2026-02-08T20:01:38.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/25/389eea98109e4b85e443fae384b30ff67167f27f4df6fb43d26cd151d0dc/granian-2.7.1-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:f787bbcb06ca605ff4161a04078591b2269b628165214ab913084e7fdb5ab9d8", size = 7261453, upload-time = "2026-02-08T20:01:40.355Z" }, + { url = "https://files.pythonhosted.org/packages/ac/00/75180d71994b87c0b56385c1b60c93b73b8822ed8edba2c63f72b0f836b6/granian-2.7.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:52163a3b609489bcb614e45811e2a66a6780b1459bbbc29504de13c23a115112", size = 7039030, upload-time = "2026-02-08T20:01:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/d3/11/a913af3c65debb5e5d577d3cb5ac988313c05c19fca789e167375ee432df/granian-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b2cd2118353db7f06fee0aefdada9e109434e030ac2fdc8f691b669787680d2e", size = 4066745, upload-time = "2026-02-08T20:01:43.161Z" }, + { url = "https://files.pythonhosted.org/packages/41/c1/cc5c0abc5c573a8832c584f52c98f7882119fe81d52a49285800e25d993f/granian-2.7.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a677972bb9050ec15896452f2c299b56f15e01212c1185d9373b92348fd88930", size = 6397999, upload-time = "2026-02-08T20:01:44.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/77/5248e8cf1c25f080959c0a4e4a8039107b0b2bf67a9fc8904cfe57614a24/granian-2.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ee4b404425a135274ab69513fdc1883ce954beef22113058e6e2a25d89926e68", size = 6108572, upload-time = "2026-02-08T20:01:45.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a0/fa0b961d7c9b1c2f046a58b85ffe1e7bc5d3a7fcc8c947bdd6fd397a312c/granian-2.7.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c4eaf3b0c1602a2ef75a8e418bb6d2867994e7ac246ea6833f7b812289d038c", size = 7101910, upload-time = "2026-02-08T20:01:47.773Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/edd388b12ebecde4edbbf4d62cd78ed6e5ae0f6b834e88de2fe06e6f948e/granian-2.7.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d593d80f568b2025a227a9b0bf664db94c9423069b27c120e288a2350507a4d8", size = 6399861, upload-time = "2026-02-08T20:01:49.594Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/6e8962f1be1a578841e9c68bb8f3a416c30880003c3180a1e6b852ad1717/granian-2.7.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab94be605aaf90968fd04fb527f1b2790f6815dd0e9690586adb4a9be1f25010", size = 6951789, upload-time = "2026-02-08T20:01:51.115Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/9f07664d847653115b196f70594016de8fd7629e5aa1645d6d20f771cf14/granian-2.7.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:392f6cc3eb7a5039a815a823c3f468161b4eb179d061450c0ec843cef0eb1b54", size = 6983541, upload-time = "2026-02-08T20:01:52.693Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c6/08b9203a4f897a31810bb18344b5ecaf26eb34135916c257c14ec762eb51/granian-2.7.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff063c417ee16fadca3c534e2059a6cf47e1df2607f1c6012be4ea6486b814f5", size = 7032652, upload-time = "2026-02-08T20:01:54.336Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/c7a5c595313432a5373e6014980a77d8f028f24f31b68406af97ace94fe6/granian-2.7.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:1a59ed88c40896db96a973e49a5ba2a2f84d7569c1da8cf11c685d11bffc2ef1", size = 7254611, upload-time = "2026-02-08T20:01:55.74Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/fe283eeb7a2f525472bd6ef2b0c6b7fb95d4369902b75d8e7e252628e62e/granian-2.7.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:773ff347d4541634e8c50b82b532eefa68c0043cda100bd44712b88565a5495b", size = 7110307, upload-time = "2026-02-08T20:01:57.117Z" }, + { url = "https://files.pythonhosted.org/packages/61/ea/b6901c64cac1fc3b455acdba279d80454fe963eca314ebfaf4e2eec9933c/granian-2.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:089f8a0d6d6a215f6773aa9dfdb56ec349d28840203517e7a7933485b1a1f404", size = 4122834, upload-time = "2026-02-08T20:01:58.682Z" }, + { url = "https://files.pythonhosted.org/packages/5c/41/bd76745d2fd2e2735390037324cb2d2b2f934473d77fb27f176494f5b2f3/granian-2.7.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e25c7dedd9325e11bda1d9692f25314791d24ae39b8206fb858f18a57087f2ee", size = 6376497, upload-time = "2026-02-08T20:02:00.117Z" }, + { url = "https://files.pythonhosted.org/packages/40/ea/bdb388e3e24308e92c370674d225e819eee6740dd440d6450860039b934a/granian-2.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:387c6032d46191deaf18819f15988e98d0f5c85eef09efb28c4c4b7b8b0dc2d2", size = 6092395, upload-time = "2026-02-08T20:02:01.75Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/438da7d5c66ed2c9df1c5946485e464fd52a420217212e0c9b5bb90f8e93/granian-2.7.1-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d1b5b47a34ab0f47f8bd447894412b4d9bdcb2011fbb9d1b8f7890c8442d233", size = 6226387, upload-time = "2026-02-08T20:02:03.185Z" }, + { url = "https://files.pythonhosted.org/packages/91/1a/f317272d59618a846a0c7ea019ab0352d947e8afdae40faea580b98600c7/granian-2.7.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7f65cd1d46c8ee454b0f29743340bebc170c1da2af83bd759fb02d69c24c7e9", size = 7123367, upload-time = "2026-02-08T20:02:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/d0/63/0c0c0005798c808082ae72b6bc3ccc1282d1b078375b060c5477aabbe407/granian-2.7.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d12b93e6467fc079b38e104154d5e5625a5e7c6a1776a59039c1e5fb57e0fe3", size = 6709311, upload-time = "2026-02-08T20:02:06.266Z" }, + { url = "https://files.pythonhosted.org/packages/e6/27/73655570644b3e727b22e3cf4239eebe90c18d1d3c868fc3d71e4d50dd46/granian-2.7.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ff4aba223bfeca0c6bc8f64ef03d87d04aff36515b2fd91108e5c9f55e67a5ee", size = 6802243, upload-time = "2026-02-08T20:02:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/23/00/2b9655d05f14bee4cd4080f3a18f0f0f4e7014158d7323a1cb0d31ed61cb/granian-2.7.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:2cc036b6f7db04ba6750aa86dff17c7930b7f295e4bfc5f35e9231d9f42e8094", size = 6978785, upload-time = "2026-02-08T20:02:09.269Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/deff2560260ddc9a99315ecb345c93485b0b102708838e7c42837c7a6535/granian-2.7.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:f2834f178ddbe25f077b28eba3b0e3e3814b17a0fc61fe44c17c270eef37ff54", size = 7303589, upload-time = "2026-02-08T20:02:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/d1/52/7fefaf4f1317883e7a5f25a92bca43f914b47d4762ad8f38f48e7e85b2a8/granian-2.7.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:874d4eedc527f9c59dd192e263be8047b86759e71ac9552283d010bcea93993b", size = 6984251, upload-time = "2026-02-08T20:02:12.753Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/d6aa049cdbe15b9ffe7964b01cc50efc8ccc067c3a50da7bc5ced1eaf6a4/granian-2.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:d787d9bf1744c275fa60775629e910305aa6395a88a32eea25b0008652ed9fe9", size = 4051984, upload-time = "2026-02-08T20:02:14.325Z" }, + { url = "https://files.pythonhosted.org/packages/d7/72/36d03ed914f70c79583542a60cedfeb7bc2ab992ee75ca5725612c1191a4/granian-2.7.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:97ebda3ae49c181b25b603d32ace5a8d83880c9c52550d3b66a4bf09f3c1b809", size = 6411236, upload-time = "2026-02-08T20:02:16.136Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/6d734663ea31a1935ae0d835ba12883cdfe63376593918de84ddf1aa26c2/granian-2.7.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8eeb97b4cc403956cdd782da83d30eddbfb90415e850520b6627d207cf06d8db", size = 6120207, upload-time = "2026-02-08T20:02:18.502Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/c6bf30ae2f9feb305b454a2a2118e40bec9dac94cc5c23a9d68f2d054f14/granian-2.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8272952e6c094cdb24e42c9123eb780e789fe28e3f49a80cfce3df1b080ae2d", size = 6926893, upload-time = "2026-02-08T20:02:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/76/00/b2567a14dd68ae1fee1085d60f9ddaa6e93b155c86893804ed2303228f37/granian-2.7.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:32a4414b3ac17eef25d3bc33e2ed4f85150ebed3ef40028d4192bd0a842358c0", size = 7031580, upload-time = "2026-02-08T20:02:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/91/54/4c4aff8f153c3340d0aa26afbeb3db03bc9d7d914905c47705328c2514a8/granian-2.7.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:99b24f1241d142bdcb33c5744e7503b358fbcd899c44e4f48464b2bcaca2bd0f", size = 7097067, upload-time = "2026-02-08T20:02:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/47/82/c1fce66ebeb3d681d4405eee78b9159b230558f8bc99e44456541c03fe7b/granian-2.7.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:f4b715653cc9765c1aea629802c862e030482ff847f5d4c03d5f401830ff617c", size = 7336016, upload-time = "2026-02-08T20:02:26.969Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/6bd215ec3567bcc36defb7cb30a3c03f73f2f56f8a8a34148a24008f94b6/granian-2.7.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:47ef955d06c1cdff1aeb3d4d0ada415359a034295d0f162d7c0a0f98d76d4d6c", size = 7004178, upload-time = "2026-02-08T20:02:28.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/c2480b4b4123e22b41bf82fc49e7a3b28cd2274dfa445959a1805f9a603d/granian-2.7.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a9bb46143d77161065cfe1d662f9a758bb17c7e3a2fde178f0a5aaac3fb3a65b", size = 4081455, upload-time = "2026-02-08T20:02:30.271Z" }, ] [package.optional-dependencies] @@ -585,62 +591,62 @@ reload = [ [[package]] name = "greenlet" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" }, - { url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" }, - { url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" }, - { url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" }, - { url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" }, - { url = "https://files.pythonhosted.org/packages/ff/07/ac9bf1ec008916d1a3373cae212884c1dcff4a4ba0d41127ce81a8deb4e9/greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8", size = 226100, upload-time = "2026-01-23T15:30:56.957Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, - { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, - { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, - { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, - { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, - { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, - { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, - { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, - { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, - { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, - { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, - { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, - { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, + { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/ac/78/f93e840cbaef8becaf6adafbaf1319682a6c2d8c1c20224267a5c6c8c891/greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f", size = 230092, upload-time = "2026-02-20T20:17:09.379Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] @@ -654,7 +660,7 @@ wheels = [ [[package]] name = "hatchling" -version = "1.28.0" +version = "1.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, @@ -663,9 +669,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "trove-classifiers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/8e/e480359492affde4119a131da729dd26da742c2c9b604dff74836e47eef9/hatchling-1.28.0.tar.gz", hash = "sha256:4d50b02aece6892b8cd0b3ce6c82cb218594d3ec5836dbde75bf41a21ab004c8", size = 55365, upload-time = "2025-11-27T00:31:13.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/a5/48cb7efb8b4718b1a4c0c331e3364a3a33f614ff0d6afd2b93ee883d3c47/hatchling-1.28.0-py3-none-any.whl", hash = "sha256:dc48722b68b3f4bbfa3ff618ca07cdea6750e7d03481289ffa8be1521d18a961", size = 76075, upload-time = "2025-11-27T00:31:12.544Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, ] [[package]] @@ -714,18 +720,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -878,22 +872,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "narwhals" -version = "2.15.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/59/81d0f4cad21484083466f278e6b392addd9f4205b48d45b5c8771670ebf8/narwhals-2.17.0.tar.gz", hash = "sha256:ebd5bc95bcfa2f8e89a8ac09e2765a63055162837208e67b42d6eeb6651d5e67", size = 620306, upload-time = "2026-02-23T09:44:34.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, + { url = "https://files.pythonhosted.org/packages/4b/27/20770bd6bf8fbe1e16f848ba21da9df061f38d2e6483952c29d2bb5d1d8b/narwhals-2.17.0-py3-none-any.whl", hash = "sha256:2ac5307b7c2b275a7d66eeda906b8605e3d7a760951e188dcfff86e8ebe083dd", size = 444897, upload-time = "2026-02-23T09:44:32.006Z" }, ] [[package]] @@ -972,7 +957,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.1" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -982,79 +967,79 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, - { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, - { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, - { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, - { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, - { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, - { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, - { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, - { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, - { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, - { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, - { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, - { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, - { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, - { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, - { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, - { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, - { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, - { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, - { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, - { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, - { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, - { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, - { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, - { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] @@ -1144,7 +1129,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.0" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1155,68 +1140,68 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, - { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, - { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, - { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, - { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, - { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, - { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, - { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" }, + { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" }, + { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" }, + { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" }, + { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" }, + { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" }, + { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" }, + { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" }, + { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, ] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -1319,30 +1304,30 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] name = "playwright" -version = "1.57.0" +version = "1.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, - { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" }, ] [[package]] @@ -1385,43 +1370,43 @@ wheels = [ [[package]] name = "psutil" -version = "7.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, - { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, - { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "psycopg" -version = "3.3.2" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, ] [package.optional-dependencies] @@ -1431,64 +1416,64 @@ binary = [ [[package]] name = "psycopg-binary" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" }, - { url = "https://files.pythonhosted.org/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" }, - { url = "https://files.pythonhosted.org/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" }, - { url = "https://files.pythonhosted.org/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" }, - { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, - { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, - { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" }, - { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" }, - { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" }, - { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" }, - { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" }, - { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" }, - { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" }, - { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" }, - { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" }, - { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" }, - { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" }, - { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" }, - { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" }, - { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" }, - { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, - { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d8/a763308a41e2ecfb6256ba0877d340c2f2b124c8b2746401863d96fa2c7a/psycopg_binary-3.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c", size = 4609758, upload-time = "2026-02-18T16:46:33.132Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a9/f8a683e85400c1208685e7c895abc049dc13aa0b6ea989e6adf0a3681fe0/psycopg_binary-3.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84", size = 4676740, upload-time = "2026-02-18T16:46:42.904Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7d/03512c4aaac8a58fc3b1221f38293aa517a1950d10ef8646c72c49addc7d/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b", size = 5496335, upload-time = "2026-02-18T16:46:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/23319b4b1c2c0b810d225e1b6f16efbb16150074fc0ea96bfcabdf59ee09/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51", size = 5172032, upload-time = "2026-02-18T16:47:00.878Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/6d61dc0a56654c558a37b2d9b2094e470aa12621305cc7935fd769122e32/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7", size = 6763107, upload-time = "2026-02-18T16:47:11.784Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b5/e2a3c90aa1059f5b5f593379caad7be3cc3c2ce1ddfc7730e39854e174fe/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e", size = 5006494, upload-time = "2026-02-18T16:47:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3e/bf126e0a1f864e191b7f3eeea667ee2ce13d582b036255fb8b12946d1f7a/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1", size = 4533850, upload-time = "2026-02-18T16:47:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d8/bb5e8d395deb945629aa0c65d12ab90ec3bfcbdf56be89e2a84d001864c9/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e", size = 4223316, upload-time = "2026-02-18T16:47:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/c2/70/33eef61b0f0fd41ebf93b9699f44067313a45016827f67b3c8cc41f0a7ab/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25", size = 3954515, upload-time = "2026-02-18T16:47:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/ea/db/27c2b3b9698e713e83e11e8540daa27516f9e90390ec21a41091cb15fcaf/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f", size = 4260274, upload-time = "2026-02-18T16:47:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3b/71e5d603059bf5474215f573a3e2d357a4e95672b26e04d41674400d4862/psycopg_binary-3.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0", size = 3557375, upload-time = "2026-02-18T16:47:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" }, + { url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, ] [[package]] @@ -1644,14 +1629,14 @@ wheels = [ [[package]] name = "pyee" -version = "13.0.0" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -1665,14 +1650,15 @@ wheels = [ [[package]] name = "pyleak" -version = "0.1.17" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/32/2fd980ef56e7ed61de7d1628f8260aa50599dacf0c02e3da542e7e15f407/pyleak-0.1.17.tar.gz", hash = "sha256:739481d5f977da4a7063785273c6bad7904d0828d6d6ecd77a64da73a2eea138", size = 322076, upload-time = "2026-01-21T13:07:45.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/8c/691f81191ffd004437e3a595211557b3f3b89e74a02577c74f7cdf96d0d4/pyleak-0.2.0.tar.gz", hash = "sha256:a0e8b94d66963358fdb370bade4caac0ce922f173dd89c853e26870b1f7ef520", size = 327265, upload-time = "2026-01-29T09:28:18.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/86/40b5dc4b74dc4a9b2ffa64d72a59cdd40dfa788c15c5e4eb62a9b88e5aec/pyleak-0.1.17-py3-none-any.whl", hash = "sha256:9b230762d33ee8d71824bee38e3a71ddf90e76ac22d2055c795e0ea188a76c62", size = 25260, upload-time = "2026-01-21T13:07:43.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/67/f125f5fd737e8fe0594a818d27d4956a63c62d687fc245e5f44331b175ec/pyleak-0.2.0-py3-none-any.whl", hash = "sha256:87534b46abf95eefcf539a31a5a31cb378484ebdfd32600965a9be2aeac3eb95", size = 25576, upload-time = "2026-01-29T09:28:20.058Z" }, ] [[package]] @@ -1699,7 +1685,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1710,9 +1696,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -1757,28 +1743,28 @@ wheels = [ [[package]] name = "pytest-codspeed" -version = "4.2.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "pytest" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/e8/27fcbe6516a1c956614a4b61a7fccbf3791ea0b992e07416e8948184327d/pytest_codspeed-4.2.0.tar.gz", hash = "sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7", size = 113263, upload-time = "2025-10-24T09:02:55.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ab/eca41967d11c95392829a8b4bfa9220a51cffc4a33ec4653358000356918/pytest_codspeed-4.3.0.tar.gz", hash = "sha256:5230d9d65f39063a313ed1820df775166227ec5c20a1122968f85653d5efee48", size = 124745, upload-time = "2026-02-09T15:23:34.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/b8/d599a466c50af3f04001877ae8b17c12b803f3b358235736b91a0769de0d/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b", size = 261943, upload-time = "2025-10-24T09:02:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/74/19/ccc1a2fcd28357a8db08ba6b60f381832088a3850abc262c8e0b3406491a/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8", size = 250782, upload-time = "2025-10-24T09:02:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2d/f0083a2f14ecf008d961d40439a71da0ae0d568e5f8dc2fccd3e8a2ab3e4/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b", size = 261960, upload-time = "2025-10-24T09:02:40.665Z" }, - { url = "https://files.pythonhosted.org/packages/5f/0c/1f514c553db4ea5a69dfbe2706734129acd0eca8d5101ec16f1dd00dbc0f/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3", size = 250808, upload-time = "2025-10-24T09:02:42.07Z" }, - { url = "https://files.pythonhosted.org/packages/81/04/479905bd6653bc981c0554fcce6df52d7ae1594e1eefd53e6cf31810ec7f/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39", size = 262084, upload-time = "2025-10-24T09:02:43.155Z" }, - { url = "https://files.pythonhosted.org/packages/d2/46/d6f345d7907bac6cbb6224bd697ecbc11cf7427acc9e843c3618f19e3476/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe", size = 251100, upload-time = "2025-10-24T09:02:44.168Z" }, - { url = "https://files.pythonhosted.org/packages/de/dc/e864f45e994a50390ff49792256f1bdcbf42f170e3bc0470ee1a7d2403f3/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5", size = 262057, upload-time = "2025-10-24T09:02:45.791Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1c/f1d2599784486879cf6579d8d94a3e22108f0e1f130033dab8feefd29249/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8", size = 251013, upload-time = "2025-10-24T09:02:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fd/eafd24db5652a94b4d00fe9b309b607de81add0f55f073afb68a378a24b6/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37", size = 262065, upload-time = "2025-10-24T09:02:48.018Z" }, - { url = "https://files.pythonhosted.org/packages/f9/14/8d9340d7dc0ae647991b28a396e16b3403e10def883cde90d6b663d3f7ec/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca", size = 251057, upload-time = "2025-10-24T09:02:49.102Z" }, - { url = "https://files.pythonhosted.org/packages/4b/39/48cf6afbca55bc7c8c93c3d4ae926a1068bcce3f0241709db19b078d5418/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830", size = 267983, upload-time = "2025-10-24T09:02:50.558Z" }, - { url = "https://files.pythonhosted.org/packages/33/86/4407341efb5dceb3e389635749ce1d670542d6ca148bd34f9d5334295faf/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f", size = 256732, upload-time = "2025-10-24T09:02:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/25/0e/8cb71fd3ed4ed08c07aec1245aea7bc1b661ba55fd9c392db76f1978d453/pytest_codspeed-4.2.0-py3-none-any.whl", hash = "sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0", size = 113726, upload-time = "2025-10-24T09:02:54.785Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/800bdaeabd3eb126aff7e3e22dc45b2826305f61cbfd093284caf8d9ca01/pytest_codspeed-4.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2acecc4126658abebc683b38121adec405a46e18a619d49d6154c6e60c5deb2", size = 347077, upload-time = "2026-02-09T15:23:17.2Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/d69707440829adab86d078d5f1c8c070df116b1624f8eae4ff36933ba612/pytest_codspeed-4.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:619120775e92a3f43fb4ff4c256a251b1554c904d95e2154a382484283f0388a", size = 342234, upload-time = "2026-02-09T15:23:18.407Z" }, + { url = "https://files.pythonhosted.org/packages/d9/15/ec0ac1f022173b3134c9638f2a35f21fbb3142c75da066d9e49e5a8bb4bd/pytest_codspeed-4.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbeff1eb2f2e36df088658b556fa993e6937bf64ffb07406de4db16fd2b26874", size = 347076, upload-time = "2026-02-09T15:23:19.989Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e8/1fe375794ad02b7835f378a7bcfa8fbac9acadefe600a782a7c4a7064db7/pytest_codspeed-4.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:878aad5e4bb7b401ad8d82f3af5186030cd2bd0d0446782e10dabb9db8827466", size = 342215, upload-time = "2026-02-09T15:23:20.954Z" }, + { url = "https://files.pythonhosted.org/packages/09/58/50df94e9a78e1c77818a492c90557eeb1309af025120c9a21e6375950c52/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527a3a02eaa3e4d4583adc4ba2327eef79628f3e1c682a4b959439551a72588e", size = 347395, upload-time = "2026-02-09T15:23:21.986Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/7dfbd3eefd112a14e6fb65f9ff31dacf2e9c381cb94b27332b81d2b13f8d/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9858c2a6e1f391d5696757e7b6e9484749a7376c46f8b4dd9aebf093479a9667", size = 342625, upload-time = "2026-02-09T15:23:23.035Z" }, + { url = "https://files.pythonhosted.org/packages/7f/53/7255f6a25bc56ff1745b254b21545dfe0be2268f5b91ce78f7e8a908f0ad/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34f2fd8497456eefbd325673f677ea80d93bb1bc08a578c1fa43a09cec3d1879", size = 347325, upload-time = "2026-02-09T15:23:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f8/82ae570d8b9ad30f33c9d4002a7a1b2740de0e090540c69a28e4f711ebe2/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5", size = 342558, upload-time = "2026-02-09T15:23:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e1/55cfe9474f91d174c7a4b04d257b5fc6d4d06f3d3680f2da672ee59ccc10/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bec30f4fc9c4973143cd80f0d33fa780e9fa3e01e4dbe8cedf229e72f1212c62", size = 347383, upload-time = "2026-02-09T15:23:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/8fd781d959bbe789b3de8ce4c50d5706a684a0df377147dfb27b200c20c1/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4", size = 342481, upload-time = "2026-02-09T15:23:27.686Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0c/368045133c6effa2c665b1634b7b8a9c88b307f877fa31f1f8df47885b51/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0d1f6ea594f29b745c634d66d5f5f1caa1c3abd2af82fea49d656038e8fc77", size = 353680, upload-time = "2026-02-09T15:23:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/59/21/e543abcd72244294e25ae88ec3a9311ade24d6913f8c8f42569d671700bc/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2f5bb6d8898bea7db45e3c8b916ee48e36905b929477bb511b79c5a3ccacda4", size = 347888, upload-time = "2026-02-09T15:23:30.443Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/b8a53c20cf5b41042c205bb9d36d37da00418d30fd1a94bf9eb147820720/pytest_codspeed-4.3.0-py3-none-any.whl", hash = "sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a", size = 125224, upload-time = "2026-02-09T15:23:33.774Z" }, ] [[package]] @@ -1837,14 +1823,14 @@ wheels = [ [[package]] name = "pytest-split" -version = "0.10.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/d7/e30ba44adf83f15aee3f636daea54efadf735769edc0f0a7d98163f61038/pytest_split-0.10.0.tar.gz", hash = "sha256:adf80ba9fef7be89500d571e705b4f963dfa05038edf35e4925817e6b34ea66f", size = 13903, upload-time = "2024-10-16T15:45:19.783Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/16/8af4c5f2ceb3640bb1f78dfdf5c184556b10dfe9369feaaad7ff1c13f329/pytest_split-0.11.0.tar.gz", hash = "sha256:8ebdb29cc72cc962e8eb1ec07db1eeb98ab25e215ed8e3216f6b9fc7ce0ec2b5", size = 13421, upload-time = "2026-02-03T09:14:31.469Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/a7/cad88e9c1109a5c2a320d608daa32e5ee008ccbc766310f54b1cd6b3d69c/pytest_split-0.10.0-py3-none-any.whl", hash = "sha256:466096b086a7147bcd423c6e6c2e57fc62af1c5ea2e256b4ed50fc030fc3dddc", size = 11961, upload-time = "2024-10-16T15:45:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a1/d4423657caaa8be9b31e491592b49cebdcfd434d3e74512ce71f6ec39905/pytest_split-0.11.0-py3-none-any.whl", hash = "sha256:899d7c0f5730da91e2daf283860eb73b503259cb416851a65599368849c7f382", size = 11911, upload-time = "2026-02-03T09:14:33.708Z" }, ] [[package]] @@ -1870,14 +1856,14 @@ wheels = [ [[package]] name = "python-engineio" -version = "4.13.0" +version = "4.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "simple-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/5a/349caac055e03ef9e56ed29fa304846063b1771ee54ab8132bf98b29491e/python_engineio-4.13.0.tar.gz", hash = "sha256:f9c51a8754d2742ba832c24b46ed425fdd3064356914edd5a1e8ffde76ab7709", size = 92194, upload-time = "2025-12-24T22:38:05.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/74/c655a6eda0fd188d490c14142a0f0380655ac7099604e1fbf8fa1a97f0a1/python_engineio-4.13.0-py3-none-any.whl", hash = "sha256:57b94eac094fa07b050c6da59f48b12250ab1cd920765f4849963e3d89ad9de3", size = 59676, upload-time = "2025-12-24T22:38:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, ] [[package]] @@ -1903,15 +1889,15 @@ wheels = [ [[package]] name = "python-socketio" -version = "5.16.0" +version = "5.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bidict" }, { name = "python-engineio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/55/5d8af5884283b58e4405580bcd84af1d898c457173c708736e065f10ca4a/python_socketio-5.16.0.tar.gz", hash = "sha256:f79403c7f1ba8b84460aa8fe4c671414c8145b21a501b46b676f3740286356fd", size = 127120, upload-time = "2025-12-24T23:51:48.826Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl", hash = "sha256:d95802961e15c7bd54ecf884c6e7644f81be8460f0a02ee66b473df58088ee8a", size = 79607, upload-time = "2025-12-24T23:51:47.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, ] [[package]] @@ -1989,19 +1975,19 @@ wheels = [ [[package]] name = "redis" -version = "7.1.0" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/32/6fac13a11e73e1bc67a2ae821a72bfe4c2d8c4c48f0267e4a952be0f1bae/redis-7.2.0.tar.gz", hash = "sha256:4dd5bf4bd4ae80510267f14185a15cba2a38666b941aff68cccf0256b51c1f26", size = 4901247, upload-time = "2026-02-16T17:16:22.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, + { url = "https://files.pythonhosted.org/packages/86/cf/f6180b67f99688d83e15c84c5beda831d1d341e95872d224f87ccafafe61/redis-7.2.0-py3-none-any.whl", hash = "sha256:01f591f8598e483f1842d429e8ae3a820804566f1c73dca1b80e23af9fba0497", size = 394898, upload-time = "2026-02-16T17:16:20.693Z" }, ] [[package]] name = "reflex" -version = "0.8.27.dev1" +version = "0.8.28.dev1" source = { editable = "." } dependencies = [ { name = "alembic" }, @@ -2042,9 +2028,9 @@ dev = [ { name = "hatchling" }, { name = "libsass" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "playwright" }, { name = "plotly" }, @@ -2164,61 +2150,57 @@ wheels = [ [[package]] name = "rich" -version = "14.3.1" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "ruff" -version = "0.14.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, ] [[package]] name = "selenium" -version = "4.40.0" +version = "4.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "trio" }, - { name = "trio-typing" }, { name = "trio-websocket" }, - { name = "types-certifi" }, - { name = "types-urllib3" }, { name = "typing-extensions" }, { name = "urllib3", extra = ["socks"] }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/ef/a5727fa7b33d20d296322adf851b76072d8d3513e1b151969d3228437faf/selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", size = 930444, upload-time = "2026-01-18T23:12:31.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/7c/133d00d6d013a17d3f39199f27f1a780ec2e95d7b9aa997dc1b8ac2e62a7/selenium-4.41.0.tar.gz", hash = "sha256:003e971f805231ad63e671783a2b91a299355d10cefb9de964c36ff3819115aa", size = 937872, upload-time = "2026-02-20T03:42:06.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/74/eb9d6540aca1911106fa0877b8e9ef24171bc18857937a6b0ffe0586c623/selenium-4.40.0-py3-none-any.whl", hash = "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729", size = 9608184, upload-time = "2026-01-18T23:12:29.435Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d6/e4160989ef6b272779af6f3e5c43c3ba9be6687bdc21c68c3fb220e555b3/selenium-4.41.0-py3-none-any.whl", hash = "sha256:b8ccde8d2e7642221ca64af184a92c19eee6accf2e27f20f30472f5efae18eb1", size = 9532858, upload-time = "2026-02-20T03:42:03.218Z" }, ] [[package]] @@ -2318,28 +2300,28 @@ wheels = [ [[package]] name = "sqlmodel" -version = "0.0.31" +version = "0.0.37" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/b8/e7cd6def4a773f25d6e29ffce63ccbfd6cf9488b804ab6fb9b80d334b39d/sqlmodel-0.0.31.tar.gz", hash = "sha256:2d41a8a9ee05e40736e2f9db8ea28cbfe9b5d4e5a18dd139e80605025e0c516c", size = 94952, upload-time = "2025-12-28T12:35:01.436Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/72/5aa5be921800f6418a949a73c9bb7054890881143e6bc604a93d228a95a3/sqlmodel-0.0.31-py3-none-any.whl", hash = "sha256:6d946d56cac4c2db296ba1541357cee2e795d68174e2043cd138b916794b1513", size = 27093, upload-time = "2025-12-28T12:35:00.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/7c8d18e737433f3b5bbe27b56a9072a9fcb36342b48f1bef34b6da1d61f2/sqlmodel-0.0.37-py3-none-any.whl", hash = "sha256:2137a4045ef3fd66a917a7717ada959a1ceb3630d95e1f6aaab39dd2c0aef278", size = 27224, upload-time = "2026-02-21T16:39:47.781Z" }, ] [[package]] name = "starlette" -version = "0.50.0" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] @@ -2430,7 +2412,7 @@ wheels = [ [[package]] name = "trio" -version = "0.32.0" +version = "0.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2441,26 +2423,9 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/b6/c744031c6f89b18b3f5f4f7338603ab381d740a7f45938c4607b2302481f/trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970", size = 605109, upload-time = "2026-02-14T18:40:55.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" }, -] - -[[package]] -name = "trio-typing" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-generator" }, - { name = "importlib-metadata" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "trio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/dab25dc87ac48da0fe0f6419e07d0bfd98799bed4e05e7b9e0f85a1a4b4b/trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", size = 510294, upload-time = "2026-02-14T18:40:53.313Z" }, ] [[package]] @@ -2487,24 +2452,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, ] -[[package]] -name = "types-certifi" -version = "2021.10.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, -] - -[[package]] -name = "types-urllib3" -version = "1.26.25.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -2551,21 +2498,21 @@ socks = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [[package]] name = "virtualenv" -version = "20.36.1" +version = "20.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2573,9 +2520,9 @@ dependencies = [ { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/54/809199edc537dbace273495ac0884d13df26436e910a5ed4d0ec0a69806b/virtualenv-20.39.0.tar.gz", hash = "sha256:a15f0cebd00d50074fd336a169d53422436a12dfe15149efec7072cfe817df8b", size = 5869141, upload-time = "2026-02-23T18:09:13.349Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b4/8268da45f26f4fe84f6eae80a6ca1485ffb490a926afecff75fc48f61979/virtualenv-20.39.0-py3-none-any.whl", hash = "sha256:44888bba3775990a152ea1f73f8e5f566d49f11bbd1de61d426fd7732770043e", size = 5839121, upload-time = "2026-02-23T18:09:11.173Z" }, ] [[package]] @@ -2692,95 +2639,74 @@ wheels = [ [[package]] name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, - { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, - { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" }, - { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, - { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, - { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, - { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, - { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, - { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, - { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, - { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, - { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, - { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, - { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, - { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, - { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, - { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, - { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/37/ae31f40bec90de2f88d9597d0b5281e23ffe85b893a47ca5d9c05c63a4f6/wrapt-2.1.1.tar.gz", hash = "sha256:5fdcb09bf6db023d88f312bd0767594b414655d58090fc1c46b3414415f67fac", size = 81329, upload-time = "2026-02-03T02:12:13.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/21/293b657a27accfbbbb6007ebd78af0efa2083dac83e8f523272ea09b4638/wrapt-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e927375e43fd5a985b27a8992327c22541b6dede1362fc79df337d26e23604f", size = 60554, upload-time = "2026-02-03T02:11:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/25/e9/96dd77728b54a899d4ce2798d7b1296989ce687ed3c0cb917d6b3154bf5d/wrapt-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c99544b6a7d40ca22195563b6d8bc3986ee8bb82f272f31f0670fe9440c869", size = 61496, upload-time = "2026-02-03T02:12:54.732Z" }, + { url = "https://files.pythonhosted.org/packages/44/79/4c755b45df6ef30c0dd628ecfaa0c808854be147ca438429da70a162833c/wrapt-2.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2be3fa5f4efaf16ee7c77d0556abca35f5a18ad4ac06f0ef3904c3399010ce9", size = 113528, upload-time = "2026-02-03T02:12:26.405Z" }, + { url = "https://files.pythonhosted.org/packages/9f/63/23ce28f7b841217d9a6337a340fbb8d4a7fbd67a89d47f377c8550fa34aa/wrapt-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67c90c1ae6489a6cb1a82058902caa8006706f7b4e8ff766f943e9d2c8e608d0", size = 115536, upload-time = "2026-02-03T02:11:54.397Z" }, + { url = "https://files.pythonhosted.org/packages/23/7b/5ca8d3b12768670d16c8329e29960eedd56212770365a02a8de8bf73dc01/wrapt-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05c0db35ccffd7480143e62df1e829d101c7b86944ae3be7e4869a7efa621f53", size = 114716, upload-time = "2026-02-03T02:12:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3a/9789ccb14a096d30bb847bf3ee137bf682cc9750c2ce155f4c5ae1962abf/wrapt-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c2ec9f616755b2e1e0bf4d0961f59bb5c2e7a77407e7e2c38ef4f7d2fdde12c", size = 113200, upload-time = "2026-02-03T02:12:07.688Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/4ec3526ce6ce920b267c8d35d2c2f0874d3fad2744c8b7259353f1132baa/wrapt-2.1.1-cp310-cp310-win32.whl", hash = "sha256:203ba6b3f89e410e27dbd30ff7dccaf54dcf30fda0b22aa1b82d560c7f9fe9a1", size = 57876, upload-time = "2026-02-03T02:11:42.61Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4e/661c7c76ecd85375b2bc03488941a3a1078642af481db24949e2b9de01f4/wrapt-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:6f9426d9cfc2f8732922fc96198052e55c09bb9db3ddaa4323a18e055807410e", size = 60224, upload-time = "2026-02-03T02:11:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b7/53c7252d371efada4cb119e72e774fa2c6b3011fc33e3e552cdf48fb9488/wrapt-2.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:69c26f51b67076b40714cff81bdd5826c0b10c077fb6b0678393a6a2f952a5fc", size = 58645, upload-time = "2026-02-03T02:12:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a8/9254e4da74b30a105935197015b18b31b7a298bf046e67d8952ef74967bd/wrapt-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c366434a7fb914c7a5de508ed735ef9c133367114e1a7cb91dfb5cd806a1549", size = 60554, upload-time = "2026-02-03T02:11:13.038Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a1/378579880cc7af226354054a2c255f69615b379d8adad482bfe2f22a0dc2/wrapt-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d6a2068bd2e1e19e5a317c8c0b288267eec4e7347c36bc68a6e378a39f19ee7", size = 61491, upload-time = "2026-02-03T02:12:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/957b51c56acca35701665878ad31626182199fc4afecfe67dea072210f95/wrapt-2.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:891ab4713419217b2aed7dd106c9200f64e6a82226775a0d2ebd6bef2ebd1747", size = 113949, upload-time = "2026-02-03T02:11:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/cd/74/36bbebb4a3d2ae9c3e6929639721f8606cd0710a82a777c371aa69e36504/wrapt-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef36a0df38d2dc9d907f6617f89e113c5892e0a35f58f45f75901af0ce7d81", size = 115989, upload-time = "2026-02-03T02:12:19.398Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/f1177245a083c7be284bc90bddfe5aece32cdd5b858049cb69ce001a0e8d/wrapt-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76e9af3ebd86f19973143d4d592cbf3e970cf3f66ddee30b16278c26ae34b8ab", size = 115242, upload-time = "2026-02-03T02:11:08.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/3e/3b7cf5da27e59df61b1eae2d07dd03ff5d6f75b5408d694873cca7a8e33c/wrapt-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff562067485ebdeaef2fa3fe9b1876bc4e7b73762e0a01406ad81e2076edcebf", size = 113676, upload-time = "2026-02-03T02:12:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/f7/65/8248d3912c705f2c66f81cb97c77436f37abcbedb16d633b5ab0d795d8cd/wrapt-2.1.1-cp311-cp311-win32.whl", hash = "sha256:9e60a30aa0909435ec4ea2a3c53e8e1b50ac9f640c0e9fe3f21fd248a22f06c5", size = 57863, upload-time = "2026-02-03T02:12:18.112Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/d29310ab335f71f00c50466153b3dc985aaf4a9fc03263e543e136859541/wrapt-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7d79954f51fcf84e5ec4878ab4aea32610d70145c5bbc84b3370eabfb1e096c2", size = 60224, upload-time = "2026-02-03T02:12:29.289Z" }, + { url = "https://files.pythonhosted.org/packages/0c/90/a6ec319affa6e2894962a0cb9d73c67f88af1a726d15314bfb5c88b8a08d/wrapt-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:d3ffc6b0efe79e08fd947605fd598515aebefe45e50432dc3b5cd437df8b1ada", size = 58643, upload-time = "2026-02-03T02:12:43.022Z" }, + { url = "https://files.pythonhosted.org/packages/df/cb/4d5255d19bbd12be7f8ee2c1fb4269dddec9cef777ef17174d357468efaa/wrapt-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab8e3793b239db021a18782a5823fcdea63b9fe75d0e340957f5828ef55fcc02", size = 61143, upload-time = "2026-02-03T02:11:46.313Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/7ed02daa35542023464e3c8b7cb937fa61f6c61c0361ecf8f5fecf8ad8da/wrapt-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c0300007836373d1c2df105b40777986accb738053a92fe09b615a7a4547e9f", size = 61740, upload-time = "2026-02-03T02:12:51.966Z" }, + { url = "https://files.pythonhosted.org/packages/c4/60/a237a4e4a36f6d966061ccc9b017627d448161b19e0a3ab80a7c7c97f859/wrapt-2.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2b27c070fd1132ab23957bcd4ee3ba707a91e653a9268dc1afbd39b77b2799f7", size = 121327, upload-time = "2026-02-03T02:11:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fe/9139058a3daa8818fc67e6460a2340e8bbcf3aef8b15d0301338bbe181ca/wrapt-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b0e36d845e8b6f50949b6b65fc6cd279f47a1944582ed4ec8258cd136d89a64", size = 122903, upload-time = "2026-02-03T02:12:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/b8479202b4164649675846a531763531f0a6608339558b5a0a718fc49a8d/wrapt-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aeea04a9889370fcfb1ef828c4cc583f36a875061505cd6cd9ba24d8b43cc36", size = 121333, upload-time = "2026-02-03T02:11:32.148Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/75fc793b791d79444aca2c03ccde64e8b99eda321b003f267d570b7b0985/wrapt-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d88b46bb0dce9f74b6817bc1758ff2125e1ca9e1377d62ea35b6896142ab6825", size = 120458, upload-time = "2026-02-03T02:11:16.039Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3f30d511082ca6d947c405f9d8f6c8eaf83cfde527c439ec2c9a30eb5ea/wrapt-2.1.1-cp312-cp312-win32.whl", hash = "sha256:63decff76ca685b5c557082dfbea865f3f5f6d45766a89bff8dc61d336348833", size = 58086, upload-time = "2026-02-03T02:12:35.041Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/37625b643eea2849f10c3b90f69c7462faa4134448d4443234adaf122ae5/wrapt-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b828235d26c1e35aca4107039802ae4b1411be0fe0367dd5b7e4d90e562fcbcd", size = 60328, upload-time = "2026-02-03T02:12:45.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/79/56242f07572d5682ba8065a9d4d9c2218313f576e3c3471873c2a5355ffd/wrapt-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:75128507413a9f1bcbe2db88fd18fbdbf80f264b82fa33a6996cdeaf01c52352", size = 58722, upload-time = "2026-02-03T02:12:27.949Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3cf290212855b19af9fcc41b725b5620b32f470d6aad970c2593500817eb/wrapt-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9646e17fa7c3e2e7a87e696c7de66512c2b4f789a8db95c613588985a2e139", size = 61150, upload-time = "2026-02-03T02:12:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/9d/33/5b8f89a82a9859ce82da4870c799ad11ce15648b6e1c820fec3e23f4a19f/wrapt-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:428cfc801925454395aa468ba7ddb3ed63dc0d881df7b81626cdd433b4e2b11b", size = 61743, upload-time = "2026-02-03T02:11:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2f/60c51304fbdf47ce992d9eefa61fbd2c0e64feee60aaa439baf42ea6f40b/wrapt-2.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5797f65e4d58065a49088c3b32af5410751cd485e83ba89e5a45e2aa8905af98", size = 121341, upload-time = "2026-02-03T02:11:20.461Z" }, + { url = "https://files.pythonhosted.org/packages/ad/03/ce5256e66dd94e521ad5e753c78185c01b6eddbed3147be541f4d38c0cb7/wrapt-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a2db44a71202c5ae4bb5f27c6d3afbc5b23053f2e7e78aa29704541b5dad789", size = 122947, upload-time = "2026-02-03T02:11:33.596Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/50ca8854b81b946a11a36fcd6ead32336e6db2c14b6e4a8b092b80741178/wrapt-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d5350c3590af09c1703dd60ec78a7370c0186e11eaafb9dda025a30eee6492d", size = 121370, upload-time = "2026-02-03T02:11:09.886Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/d6a7c654e0043319b4cc137a4caaf7aa16b46b51ee8df98d1060254705b7/wrapt-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d9b076411bed964e752c01b49fd224cc385f3a96f520c797d38412d70d08359", size = 120465, upload-time = "2026-02-03T02:11:37.592Z" }, + { url = "https://files.pythonhosted.org/packages/55/90/65be41e40845d951f714b5a77e84f377a3787b1e8eee6555a680da6d0db5/wrapt-2.1.1-cp313-cp313-win32.whl", hash = "sha256:0bb7207130ce6486727baa85373503bf3334cc28016f6928a0fa7e19d7ecdc06", size = 58090, upload-time = "2026-02-03T02:12:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/66/6a09e0294c4fc8c26028a03a15191721c9271672467cc33e6617ee0d91d2/wrapt-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:cbfee35c711046b15147b0ae7db9b976f01c9520e6636d992cd9e69e5e2b03b1", size = 60341, upload-time = "2026-02-03T02:12:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/20ceb8b701e9a71555c87a5ddecbed76ec16742cf1e4b87bbaf26735f998/wrapt-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7d2756061022aebbf57ba14af9c16e8044e055c22d38de7bf40d92b565ecd2b0", size = 58731, upload-time = "2026-02-03T02:12:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/80/b4/fe95beb8946700b3db371f6ce25115217e7075ca063663b8cca2888ba55c/wrapt-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4814a3e58bc6971e46baa910ecee69699110a2bf06c201e24277c65115a20c20", size = 62969, upload-time = "2026-02-03T02:11:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/477b0bdc784e3299edf69c279697372b8bd4c31d9c6966eae405442899df/wrapt-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:106c5123232ab9b9f4903692e1fa0bdc231510098f04c13c3081f8ad71c3d612", size = 63606, upload-time = "2026-02-03T02:12:02.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/55/9d0c1269ab76de87715b3b905df54dd25d55bbffd0b98696893eb613469f/wrapt-2.1.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1a40b83ff2535e6e56f190aff123821eea89a24c589f7af33413b9c19eb2c738", size = 152536, upload-time = "2026-02-03T02:11:24.492Z" }, + { url = "https://files.pythonhosted.org/packages/44/18/2004766030462f79ad86efaa62000b5e39b1ff001dcce86650e1625f40ae/wrapt-2.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:789cea26e740d71cf1882e3a42bb29052bc4ada15770c90072cb47bf73fb3dbf", size = 158697, upload-time = "2026-02-03T02:12:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/e1/bb/0a880fa0f35e94ee843df4ee4dd52a699c9263f36881311cfb412c09c3e5/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ba49c14222d5e5c0ee394495a8655e991dc06cbca5398153aefa5ac08cd6ccd7", size = 155563, upload-time = "2026-02-03T02:11:49.737Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/cd1b7c4846c8678fac359a6eb975dc7ab5bd606030adb22acc8b4a9f53f1/wrapt-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ac8cda531fe55be838a17c62c806824472bb962b3afa47ecbd59b27b78496f4e", size = 150161, upload-time = "2026-02-03T02:12:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/67c90a7082f452964b4621e4890e9a490f1add23cdeb7483cc1706743291/wrapt-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:b8af75fe20d381dd5bcc9db2e86a86d7fcfbf615383a7147b85da97c1182225b", size = 59783, upload-time = "2026-02-03T02:11:39.863Z" }, + { url = "https://files.pythonhosted.org/packages/ec/08/466afe4855847d8febdfa2c57c87e991fc5820afbdef01a273683dfd15a0/wrapt-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:45c5631c9b6c792b78be2d7352129f776dd72c605be2c3a4e9be346be8376d83", size = 63082, upload-time = "2026-02-03T02:12:09.075Z" }, + { url = "https://files.pythonhosted.org/packages/9a/62/60b629463c28b15b1eeadb3a0691e17568622b12aa5bfa7ebe9b514bfbeb/wrapt-2.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:da815b9263947ac98d088b6414ac83507809a1d385e4632d9489867228d6d81c", size = 60251, upload-time = "2026-02-03T02:11:21.794Z" }, + { url = "https://files.pythonhosted.org/packages/95/a0/1c2396e272f91efe6b16a6a8bce7ad53856c8f9ae4f34ceaa711d63ec9e1/wrapt-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aa1765054245bb01a37f615503290d4e207e3fd59226e78341afb587e9c1236", size = 61311, upload-time = "2026-02-03T02:12:44.41Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9a/d2faba7e61072a7507b5722db63562fdb22f5a24e237d460d18755627f15/wrapt-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:feff14b63a6d86c1eee33a57f77573649f2550935981625be7ff3cb7342efe05", size = 61805, upload-time = "2026-02-03T02:11:59.905Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/073989deb4b5d7d6e7ea424476a4ae4bda02140f2dbeaafb14ba4864dd60/wrapt-2.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81fc5f22d5fcfdbabde96bb3f5379b9f4476d05c6d524d7259dc5dfb501d3281", size = 120308, upload-time = "2026-02-03T02:12:04.46Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/84f37261295e38167a29eb82affaf1dc15948dc416925fe2091beee8e4ac/wrapt-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:951b228ecf66def855d22e006ab9a1fc12535111ae7db2ec576c728f8ddb39e8", size = 122688, upload-time = "2026-02-03T02:11:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ea/80/32db2eec6671f80c65b7ff175be61bc73d7f5223f6910b0c921bbc4bd11c/wrapt-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ddf582a95641b9a8c8bd643e83f34ecbbfe1b68bc3850093605e469ab680ae3", size = 121115, upload-time = "2026-02-03T02:12:39.068Z" }, + { url = "https://files.pythonhosted.org/packages/49/ef/dcd00383df0cd696614127902153bf067971a5aabcd3c9dcb2d8ef354b2a/wrapt-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc5c500966bf48913f795f1984704e6d452ba2414207b15e1f8c339a059d5b16", size = 119484, upload-time = "2026-02-03T02:11:48.419Z" }, + { url = "https://files.pythonhosted.org/packages/76/29/0630280cdd2bd8f86f35cb6854abee1c9d6d1a28a0c6b6417cd15d378325/wrapt-2.1.1-cp314-cp314-win32.whl", hash = "sha256:4aa4baadb1f94b71151b8e44a0c044f6af37396c3b8bcd474b78b49e2130a23b", size = 58514, upload-time = "2026-02-03T02:11:58.616Z" }, + { url = "https://files.pythonhosted.org/packages/db/19/5bed84f9089ed2065f6aeda5dfc4f043743f642bc871454b261c3d7d322b/wrapt-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:860e9d3fd81816a9f4e40812f28be4439ab01f260603c749d14be3c0a1170d19", size = 60763, upload-time = "2026-02-03T02:12:24.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/b967f2f9669e4249b4fe82e630d2a01bc6b9e362b9b12ed91bbe23ae8df4/wrapt-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3c59e103017a2c1ea0ddf589cbefd63f91081d7ce9d491d69ff2512bb1157e23", size = 59051, upload-time = "2026-02-03T02:11:29.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/19/6fed62be29f97eb8a56aff236c3f960a4b4a86e8379dc7046a8005901a97/wrapt-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9fa7c7e1bee9278fc4f5dd8275bc8d25493281a8ec6c61959e37cc46acf02007", size = 63059, upload-time = "2026-02-03T02:12:06.368Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1c/b757fd0adb53d91547ed8fad76ba14a5932d83dde4c994846a2804596378/wrapt-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39c35e12e8215628984248bd9c8897ce0a474be2a773db207eb93414219d8469", size = 63618, upload-time = "2026-02-03T02:12:23.197Z" }, + { url = "https://files.pythonhosted.org/packages/10/fe/e5ae17b1480957c7988d991b93df9f2425fc51f128cf88144d6a18d0eb12/wrapt-2.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:94ded4540cac9125eaa8ddf5f651a7ec0da6f5b9f248fe0347b597098f8ec14c", size = 152544, upload-time = "2026-02-03T02:11:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cc/99aed210c6b547b8a6e4cb9d1425e4466727158a6aeb833aa7997e9e08dd/wrapt-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0af328373f97ed9bdfea24549ac1b944096a5a71b30e41c9b8b53ab3eec04a", size = 158700, upload-time = "2026-02-03T02:12:30.684Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/d442f745f4957944d5f8ad38bc3a96620bfff3562533b87e486e979f3d99/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4ad839b55f0bf235f8e337ce060572d7a06592592f600f3a3029168e838469d3", size = 155561, upload-time = "2026-02-03T02:11:28.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/9891816280e0018c48f8dfd61b136af7b0dcb4a088895db2531acde5631b/wrapt-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d89c49356e5e2a50fa86b40e0510082abcd0530f926cbd71cf25bee6b9d82d7", size = 150188, upload-time = "2026-02-03T02:11:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/e2f273b6d70d41f98d0739aa9a269d0b633684a5fb17b9229709375748d4/wrapt-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:f4c7dd22cf7f36aafe772f3d88656559205c3af1b7900adfccb70edeb0d2abc4", size = 60425, upload-time = "2026-02-03T02:11:35.007Z" }, + { url = "https://files.pythonhosted.org/packages/1e/06/b500bfc38a4f82d89f34a13069e748c82c5430d365d9e6b75afb3ab74457/wrapt-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f76bc12c583ab01e73ba0ea585465a41e48d968f6d1311b4daec4f8654e356e3", size = 63855, upload-time = "2026-02-03T02:12:15.47Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/5f6193c32166faee1d2a613f278608e6f3b95b96589d020f0088459c46c9/wrapt-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7ea74fc0bec172f1ae5f3505b6655c541786a5cabe4bbc0d9723a56ac32eb9b9", size = 60443, upload-time = "2026-02-03T02:11:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/5a086bf4c22a41995312db104ec2ffeee2cf6accca9faaee5315c790377d/wrapt-2.1.1-py3-none-any.whl", hash = "sha256:3b0f4629eb954394a3d7c7a1c8cca25f0b07cefe6aa8545e862e9778152de5b7", size = 43886, upload-time = "2026-02-03T02:11:45.048Z" }, ] [[package]] @@ -2794,12 +2720,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] From ca28a6e6d00c7e07218e0caac0439b5336edc050 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Mar 2026 14:51:59 -0800 Subject: [PATCH 47/74] Unlink broken symlinks in the assets folder (#6140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certain build environments, such as docker bind mounts, can create symlinks that exist, but cannot be overwritten for whatever reason. So we unlink those for Simon 🎁 --- reflex/assets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/reflex/assets.py b/reflex/assets.py index 693041b7a18..500cf9408ea 100644 --- a/reflex/assets.py +++ b/reflex/assets.py @@ -92,6 +92,11 @@ def asset( if not dst_file.exists() and ( not dst_file.is_symlink() or dst_file.resolve() != src_file_shared.resolve() ): - dst_file.symlink_to(src_file_shared) + try: + dst_file.symlink_to(src_file_shared) + except FileExistsError: + # This happens when Simon builds the app on a bind mount in a docker container. + dst_file.unlink() + dst_file.symlink_to(src_file_shared) return f"/{external}/{subfolder}/{path}" From 5cd839724d37b39c678ad2b5b814ad04cfa09bb0 Mon Sep 17 00:00:00 2001 From: Milo Chen Date: Sat, 7 Mar 2026 03:44:39 +0800 Subject: [PATCH 48/74] feat: add vite_allowed_hosts config option for Vite dev server (#6147) * feat: add vite_allowed_hosts config option for Vite dev server Add a configurable `vite_allowed_hosts` field to `rx.Config` that controls whether `allowedHosts: true` is set in the Vite dev server configuration. This allows users running Reflex in Docker, Codespaces, or behind a reverse proxy to opt-in to allowing all hosts, preventing 403 Forbidden errors from Vite's host header validation. Default is `False` (original behavior, Vite only allows localhost). Users can enable it in rxconfig.py: config = rx.Config(app_name="myapp", vite_allowed_hosts=True) Or via environment variable: REFLEX_VITE_ALLOWED_HOSTS=true reflex run * refactor: support bool | list[str] for vite_allowed_hosts * feat: support union types in interpret_env_var_value Instead of raising an error for union types, try each type in the union in order and return the first successful interpretation. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Masen Furer Co-authored-by: Claude Opus 4.6 --- reflex/compiler/templates.py | 10 +++++++++- reflex/config.py | 5 +++++ reflex/environment.py | 10 ++++++++-- reflex/utils/frontend_skeleton.py | 1 + tests/units/test_environment.py | 17 +++++++++++++---- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index 2abcb6dd533..a8a7dbe4ec2 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -502,6 +502,7 @@ def vite_config_template( force_full_reload: bool, experimental_hmr: bool, sourcemap: bool | Literal["inline", "hidden"], + allowed_hosts: bool | list[str] = False, ): """Template for vite.config.js. @@ -511,10 +512,17 @@ def vite_config_template( force_full_reload: Whether to force a full reload on changes. experimental_hmr: Whether to enable experimental HMR features. sourcemap: The sourcemap configuration. + allowed_hosts: Allow all hosts (True), specific hosts (list of strings), or only localhost (False). Returns: Rendered vite.config.js content as string. """ + if allowed_hosts is True: + allowed_hosts_line = "\n allowedHosts: true," + elif isinstance(allowed_hosts, list) and allowed_hosts: + allowed_hosts_line = f"\n allowedHosts: {json.dumps(allowed_hosts)}," + else: + allowed_hosts_line = "" return rf"""import {{ fileURLToPath, URL }} from "url"; import {{ reactRouter }} from "@react-router/dev/vite"; import {{ defineConfig }} from "vite"; @@ -586,7 +594,7 @@ def vite_config_template( hmr: {"true" if experimental_hmr else "false"}, }}, server: {{ - port: process.env.PORT, + port: process.env.PORT,{allowed_hosts_line} hmr: {"true" if hmr else "false"}, watch: {{ ignored: [ diff --git a/reflex/config.py b/reflex/config.py index 6977d745631..225fe5bf2be 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -213,6 +213,11 @@ class BaseConfig: dataclasses.field(default=("*",)) ) + # Allowed hosts for the Vite dev server. Set to True to allow all hosts, + # or provide a list of hostnames (e.g. ["myservice.local"]) to allow specific ones. + # Prevents 403 errors in Docker, Codespaces, reverse proxies, etc. + vite_allowed_hosts: bool | list[str] = False + # Whether to use React strict mode. react_strict_mode: bool = True diff --git a/reflex/environment.py b/reflex/environment.py index 481caa4ce4d..826e2c06acb 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -243,8 +243,14 @@ def interpret_env_var_value( field_type = value_inside_optional(field_type) if is_union(field_type): - msg = f"Union types are not supported for environment variables: {field_name}." - raise ValueError(msg) + errors = [] + for arg in (union_types := get_args(field_type)): + try: + return interpret_env_var_value(value, arg, field_name) + except (ValueError, EnvironmentVarValueError) as e: # noqa: PERF203 + errors.append(e) + msg = f"Could not interpret {value!r} for {field_name} as any of {union_types}: {errors}" + raise EnvironmentVarValueError(msg) value = value.strip() diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 96ced280fe2..6084219df03 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -197,6 +197,7 @@ def _compile_vite_config(config: Config): force_full_reload=environment.VITE_FORCE_FULL_RELOAD.get(), experimental_hmr=environment.VITE_EXPERIMENTAL_HMR.get(), sourcemap=environment.VITE_SOURCEMAP.get(), + allowed_hosts=config.vite_allowed_hosts, ) diff --git a/tests/units/test_environment.py b/tests/units/test_environment.py index 839fea4545a..0665c6f5f0d 100644 --- a/tests/units/test_environment.py +++ b/tests/units/test_environment.py @@ -190,10 +190,19 @@ def test_interpret_enum(self): result = interpret_env_var_value("value1", _TestEnum, "TEST_FIELD") assert result == _TestEnum.VALUE1 - def test_interpret_union_error(self): - """Test that union types raise an error.""" - with pytest.raises(ValueError, match="Union types are not supported"): - interpret_env_var_value("test", int | str, "TEST_FIELD") + def test_interpret_union_tries_each_type(self): + """Test that union types try each type in order.""" + # str matches first + assert interpret_env_var_value("hello", int | str, "TEST_FIELD") == "hello" + # int matches first + assert interpret_env_var_value("42", int | str, "TEST_FIELD") == 42 + # bool matches before str + assert interpret_env_var_value("true", bool | str, "TEST_FIELD") is True + + def test_interpret_union_no_match(self): + """Test that union types raise an error if no type matches.""" + with pytest.raises(EnvironmentVarValueError, match="Could not interpret"): + interpret_env_var_value("not_a_number", int | bool, "TEST_FIELD") def test_interpret_unsupported_type(self): """Test unsupported type raises an error.""" From 129b90470a400200a8ed4fa4561a2866e45bd1d0 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 6 Mar 2026 11:44:56 -0800 Subject: [PATCH 49/74] fix: include frontend_path in sirv prod command for correct 404.html fallback (#6154) * fix: include frontend_path in sirv prod command for correct 404.html fallback When frontend_path is set, the build moves all static files (including 404.html) into a subdirectory, but the sirv --single flag was hardcoded to look for 404.html at the root. This broke dynamic routes in prod. Closes #5812 Co-Authored-By: Claude Opus 4.6 * Update reflex/constants/installer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- reflex/constants/installer.py | 15 ++++++++++- reflex/utils/frontend_skeleton.py | 5 +++- tests/units/test_prerequisites.py | 43 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 12edb911397..84e23e51b58 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -106,7 +106,20 @@ class Commands(SimpleNamespace): DEV = "react-router dev --host" EXPORT = "react-router build" - PROD = "sirv ./build/client --single 404.html --host" + + @staticmethod + def get_prod_command(frontend_path: str = "") -> str: + """Get the prod command with the correct 404.html path for the given frontend_path. + + Args: + frontend_path: The frontend path prefix (e.g. "/app"). + + Returns: + The sirv command with the correct --single fallback path. + """ + stripped = frontend_path.strip("/") + fallback = f"{stripped}/404.html" if stripped else "404.html" + return f"sirv ./build/client --single {fallback} --host" PATH = "package.json" diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 6084219df03..37d66fac7db 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -168,11 +168,14 @@ def _update_react_router_config(config: Config, prerender_routes: bool = False): def _compile_package_json(): + config = get_config() return templates.package_json_template( scripts={ "dev": constants.PackageJson.Commands.DEV, "export": constants.PackageJson.Commands.EXPORT, - "prod": constants.PackageJson.Commands.PROD, + "prod": constants.PackageJson.Commands.get_prod_command( + config.frontend_path + ), }, dependencies=constants.PackageJson.DEPENDENCIES, dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES, diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 131904e964c..cf22404bd69 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -6,10 +6,12 @@ from click.testing import CliRunner from reflex.config import Config +from reflex.constants.installer import PackageJson from reflex.reflex import cli from reflex.testing import chdir from reflex.utils.decorator import cached_procedure from reflex.utils.frontend_skeleton import ( + _compile_package_json, _compile_vite_config, _update_react_router_config, ) @@ -90,6 +92,47 @@ def test_initialise_vite_config(config, expected_output): assert expected_output in output +@pytest.mark.parametrize( + ("frontend_path", "expected_command"), + [ + ("", "sirv ./build/client --single 404.html --host"), + ("/", "sirv ./build/client --single 404.html --host"), + ("/app", "sirv ./build/client --single app/404.html --host"), + ("/app/", "sirv ./build/client --single app/404.html --host"), + ("app", "sirv ./build/client --single app/404.html --host"), + ( + "/deep/nested/path", + "sirv ./build/client --single deep/nested/path/404.html --host", + ), + ], +) +def test_get_prod_command(frontend_path, expected_command): + assert PackageJson.Commands.get_prod_command(frontend_path) == expected_command + + +@pytest.mark.parametrize( + ("config", "expected_prod_script"), + [ + ( + Config(app_name="test"), + "sirv ./build/client --single 404.html --host", + ), + ( + Config(app_name="test", frontend_path="/app"), + "sirv ./build/client --single app/404.html --host", + ), + ( + Config(app_name="test", frontend_path="/deep/nested"), + "sirv ./build/client --single deep/nested/404.html --host", + ), + ], +) +def test_compile_package_json_prod_command(config, expected_prod_script, monkeypatch): + monkeypatch.setattr("reflex.utils.frontend_skeleton.get_config", lambda: config) + output = _compile_package_json() + assert f'"prod": "{expected_prod_script}"' in output + + def test_cached_procedure(): call_count = 0 From b89c80b7defb762db8d2390ddb314a91db6c8896 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 6 Mar 2026 11:45:12 -0800 Subject: [PATCH 50/74] fix: make disable_plugins accept Plugin types instead of strings (#6155) * fix: make disable_plugins accept Plugin types instead of strings Change `disable_plugins` from `list[str]` to `list[type[Plugin]]` so both `plugins` and `disable_plugins` use consistent Plugin-based types (#6150). Strings and instances are still accepted at runtime with a deprecation warning, and normalized to the Plugin class. Environment variable support via REFLEX_DISABLE_PLUGINS continues to work through a new `interpret_plugin_class_env` interpreter, which `interpret_plugin_env` now reuses for class resolution. Co-Authored-By: Claude Opus 4.6 * Update reflex/config.py --------- Co-authored-by: Claude Opus 4.6 --- reflex/config.py | 58 ++++++++++++++++++++++++------- reflex/environment.py | 40 +++++++++++++++++++--- tests/units/test_config.py | 60 +++++++++++++++++++++++++++++++++ tests/units/test_environment.py | 34 +++++++++++++++++++ 4 files changed, 174 insertions(+), 18 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 225fe5bf2be..47fd969ac49 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -259,8 +259,8 @@ class BaseConfig: # List of plugins to use in the app. plugins: list[Plugin] = dataclasses.field(default_factory=list) - # List of fully qualified import paths of plugins to disable in the app (e.g. reflex.plugins.sitemap.SitemapPlugin). - disable_plugins: list[str] = dataclasses.field(default_factory=list) + # List of plugin types to disable in the app. + disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list) # The transport method for client-server communication. transport: Literal["websocket", "polling"] = "websocket" @@ -358,6 +358,9 @@ def _post_init(self, **kwargs): for key, env_value in env_kwargs.items(): setattr(self, key, env_value) + # Normalize disable_plugins: convert strings and Plugin subclasses to instances. + self._normalize_disable_plugins() + # Add builtin plugins if not disabled. if not self._skip_plugins_checks: self._add_builtin_plugins() @@ -374,16 +377,52 @@ def _post_init(self, **kwargs): msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager." raise ConfigError(msg) + def _normalize_disable_plugins(self): + """Normalize disable_plugins list entries to Plugin subclasses. + + Handles backward compatibility by converting strings (fully qualified + import paths) and Plugin instances to their associated classes. + """ + normalized: list[type[Plugin]] = [] + for entry in self.disable_plugins: + if isinstance(entry, type) and issubclass(entry, Plugin): + normalized.append(entry) + elif isinstance(entry, Plugin): + normalized.append(type(entry)) + elif isinstance(entry, str): + console.deprecate( + feature_name="Passing strings to disable_plugins", + reason="pass Plugin classes directly instead, e.g. disable_plugins=[SitemapPlugin]", + deprecation_version="0.8.28", + removal_version="0.9.0", + ) + try: + from reflex.environment import interpret_plugin_class_env + + normalized.append( + interpret_plugin_class_env(entry, "disable_plugins") + ) + except Exception: + console.warn( + f"Failed to import plugin from string {entry!r} in disable_plugins. " + "Please pass Plugin subclasses directly.", + ) + else: + console.warn( + f"reflex.Config.disable_plugins should contain Plugin subclasses, but got {entry!r}.", + ) + self.disable_plugins = normalized + def _add_builtin_plugins(self): """Add the builtin plugins to the config.""" for plugin in _PLUGINS_ENABLED_BY_DEFAULT: plugin_name = plugin.__module__ + "." + plugin.__qualname__ - if plugin_name not in self.disable_plugins: + if plugin not in self.disable_plugins: if not any(isinstance(p, plugin) for p in self.plugins): console.warn( f"`{plugin_name}` plugin is enabled by default, but not explicitly added to the config. " "If you want to use it, please add it to the `plugins` list in your config inside of `rxconfig.py`. " - f"To disable this plugin, set `disable_plugins` to `{[plugin_name, *self.disable_plugins]!r}`.", + f"To disable this plugin, add `{plugin.__name__}` to the `disable_plugins` list.", ) self.plugins.append(plugin()) else: @@ -394,16 +433,9 @@ def _add_builtin_plugins(self): ) for disabled_plugin in self.disable_plugins: - if not isinstance(disabled_plugin, str): - console.warn( - f"reflex.Config.disable_plugins should only contain strings, but got {disabled_plugin!r}. " - ) - if not any( - plugin.__module__ + "." + plugin.__qualname__ == disabled_plugin - for plugin in _PLUGINS_ENABLED_BY_DEFAULT - ): + if disabled_plugin not in _PLUGINS_ENABLED_BY_DEFAULT: console.warn( - f"`{disabled_plugin}` is disabled in the config, but it is not a built-in plugin. " + f"`{disabled_plugin!r}` is disabled in the config, but it is not a built-in plugin. " "Please remove it from the `disable_plugins` list in your config inside of `rxconfig.py`.", ) diff --git a/reflex/environment.py b/reflex/environment.py index 826e2c06acb..1291efeaeaa 100644 --- a/reflex/environment.py +++ b/reflex/environment.py @@ -149,15 +149,17 @@ def interpret_path_env(value: str, field_name: str) -> Path: return Path(value) -def interpret_plugin_env(value: str, field_name: str) -> Plugin: - """Interpret a plugin environment variable value. +def interpret_plugin_class_env(value: str, field_name: str) -> type[Plugin]: + """Interpret an environment variable value as a Plugin subclass. + + Resolves a fully qualified import path to the Plugin subclass it refers to. Args: - value: The environment variable value. + value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). field_name: The field name. Returns: - The interpreted value. + The Plugin subclass. Raises: EnvironmentVarValueError: If the value is invalid. @@ -184,10 +186,30 @@ def interpret_plugin_env(value: str, field_name: str) -> Plugin: msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin." raise EnvironmentVarValueError(msg) + return plugin_class + + +def interpret_plugin_env(value: str, field_name: str) -> Plugin: + """Interpret a plugin environment variable value. + + Resolves a fully qualified import path and returns an instance of the Plugin. + + Args: + value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin"). + field_name: The field name. + + Returns: + An instance of the Plugin subclass. + + Raises: + EnvironmentVarValueError: If the value is invalid. + """ + plugin_class = interpret_plugin_class_env(value, field_name) + try: return plugin_class() except Exception as e: - msg = f"Failed to instantiate plugin {plugin_name!r} for {field_name}: {e}" + msg = f"Failed to instantiate plugin {plugin_class.__name__!r} for {field_name}: {e}" raise EnvironmentVarValueError(msg) from e @@ -274,6 +296,14 @@ def interpret_env_var_value( return interpret_existing_path_env(value, field_name) if field_type is Plugin: return interpret_plugin_env(value, field_name) + if get_origin(field_type) is type: + type_args = get_args(field_type) + if ( + type_args + and isinstance(type_args[0], type) + and issubclass(type_args[0], Plugin) + ): + return interpret_plugin_class_env(value, field_name) if get_origin(field_type) is Literal: literal_values = get_args(field_type) for literal_value in literal_values: diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 5b5ad71d71e..a29b526f52a 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -17,6 +17,8 @@ interpret_enum_env, interpret_int_env, ) +from reflex.plugins import Plugin +from reflex.plugins.sitemap import SitemapPlugin def test_requires_app_name(): @@ -402,3 +404,61 @@ def test_env_file( ) for key, value in exp_env_vars.items(): assert os.environ.get(key) == value + + +class TestDisablePlugins: + """Tests for the disable_plugins config option.""" + + def test_disable_with_plugin_class(self): + """Test disabling a plugin by passing the class (type).""" + config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin]) + assert not any(isinstance(p, SitemapPlugin) for p in config.plugins) + + def test_disable_with_plugin_instance_backward_compat(self): + """Test disabling a plugin by passing an instance (deprecated).""" + config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin()]) # pyright: ignore[reportArgumentType] + assert not any(isinstance(p, SitemapPlugin) for p in config.plugins) + + def test_disable_with_string_backward_compat(self): + """Test disabling a plugin by passing a string (deprecated).""" + config = rx.Config( + app_name="test", + disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], # pyright: ignore[reportArgumentType] + ) + assert not any(isinstance(p, SitemapPlugin) for p in config.plugins) + + def test_disable_plugins_normalized_to_classes(self): + """Test that disable_plugins entries are normalized to Plugin subclasses.""" + config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin]) + assert all( + isinstance(dp, type) and issubclass(dp, Plugin) + for dp in config.disable_plugins + ) + + def test_disable_instance_normalized_to_class(self): + """Test that a Plugin instance in disable_plugins is normalized to its class.""" + config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin()]) # pyright: ignore[reportArgumentType] + assert config.disable_plugins == [SitemapPlugin] + + def test_disable_string_normalized_to_class(self): + """Test that a string in disable_plugins is normalized to the class.""" + config = rx.Config( + app_name="test", + disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], # pyright: ignore[reportArgumentType] + ) + assert config.disable_plugins == [SitemapPlugin] + + def test_disable_and_plugins_conflict_warns(self): + """Test that a warning is issued when a plugin is both enabled and disabled.""" + config = rx.Config( + app_name="test", + plugins=[SitemapPlugin()], + disable_plugins=[SitemapPlugin], + ) + # Plugin should still be in plugins list (just warned) + assert any(isinstance(p, SitemapPlugin) for p in config.plugins) + + def test_no_disable_adds_builtin(self): + """Test that builtin plugins are added when not disabled.""" + config = rx.Config(app_name="test") + assert any(isinstance(p, SitemapPlugin) for p in config.plugins) diff --git a/tests/units/test_environment.py b/tests/units/test_environment.py index 0665c6f5f0d..ee22bcc1357 100644 --- a/tests/units/test_environment.py +++ b/tests/units/test_environment.py @@ -30,6 +30,7 @@ interpret_existing_path_env, interpret_int_env, interpret_path_env, + interpret_plugin_class_env, interpret_plugin_env, ) from reflex.plugins import Plugin @@ -125,6 +126,30 @@ def test_interpret_plugin_env_invalid_class(self): with pytest.raises(EnvironmentVarValueError, match="Invalid plugin class"): interpret_plugin_env("tests.units.test_environment.TestEnum", "TEST_FIELD") + def test_interpret_plugin_class_env_valid(self): + """Test plugin class interpretation returns the class, not an instance.""" + result = interpret_plugin_class_env( + "tests.units.test_environment.TestPlugin", "TEST_FIELD" + ) + assert result is TestPlugin + + def test_interpret_plugin_class_env_invalid_format(self): + """Test plugin class interpretation with invalid format.""" + with pytest.raises(EnvironmentVarValueError, match="Invalid plugin value"): + interpret_plugin_class_env("invalid_format", "TEST_FIELD") + + def test_interpret_plugin_class_env_import_error(self): + """Test plugin class interpretation with import error.""" + with pytest.raises(EnvironmentVarValueError, match="Failed to import module"): + interpret_plugin_class_env("non.existent.module.Plugin", "TEST_FIELD") + + def test_interpret_plugin_class_env_invalid_class(self): + """Test plugin class interpretation with invalid class.""" + with pytest.raises(EnvironmentVarValueError, match="Invalid plugin class"): + interpret_plugin_class_env( + "tests.units.test_environment.TestEnum", "TEST_FIELD" + ) + def test_interpret_enum_env_valid(self): """Test enum interpretation with valid values.""" result = interpret_enum_env("value1", _TestEnum, "TEST_FIELD") @@ -172,6 +197,15 @@ def test_interpret_plugin(self): ) assert isinstance(result, TestPlugin) + def test_interpret_plugin_class(self): + """Test type[Plugin] interpretation returns the class.""" + result = interpret_env_var_value( + "tests.units.test_environment.TestPlugin", + type[Plugin], + "TEST_FIELD", + ) + assert result is TestPlugin + def test_interpret_list(self): """Test list interpretation.""" result = interpret_env_var_value("1:2:3", list[int], "TEST_FIELD") From 5716a9e652d2d8367e67116c243820b530a2fb87 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 6 Mar 2026 11:45:24 -0800 Subject: [PATCH 51/74] bump devcontainer to python:3.14-trixie (#6158) --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 24eb14cf9f1..8202e8aee02 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm", + "image": "mcr.microsoft.com/devcontainers/python:3.14-trixie", "postCreateCommand": "/bin/bash -c 'python -m pip install uv && python -m uv sync & git clone https://github.com/reflex-dev/reflex-examples; wait'", "forwardPorts": [3000, 8000], "portsAttributes": { From e8b5724c4a6b0b7cc23385db8d87d82b02475ab1 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Fri, 13 Mar 2026 00:46:43 +0100 Subject: [PATCH 52/74] fix: use actual parent class for sibling grouping in minify sync/validate sync_minify_config and validate_minify_config grouped siblings by string-splitting state paths, which fails when children of the same parent class are defined in different Python modules. This caused colliding minified IDs (e.g. both getting 'a'). Now uses get_parent_state() class objects for grouping, consistent with generate_minify_config which was already correct. Also resolves merge conflicts in _get_event_handler to keep the minified event name reverse lookup. --- reflex/minify.py | 68 ++++++++++++++--------------- reflex/state.py | 6 --- tests/units/istate/test_proxy.py | 9 ---- tests/units/test_minification.py | 75 ++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 50 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 7794d6198c3..9176cc35a08 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -400,25 +400,24 @@ def validate_minify_config( all_states = collect_all_states(root_state) - # Check for duplicate state IDs among siblings - # Group states by parent path and check for duplicate minified names - parent_to_state_ids: dict[str | None, dict[str, list[str]]] = {} + # Check for duplicate state IDs among siblings. + # Group by actual parent class (not string-split path) since children of + # the same parent can be defined in different modules. + path_to_cls = {get_state_full_path(s): s for s in all_states} + parent_cls_to_state_ids: dict[type[BaseState] | None, dict[str, list[str]]] = {} for state_path, minified_name in config["states"].items(): - # Get parent path - parts = state_path.rsplit(".", 1) - parent_path = parts[0] if len(parts) > 1 else None + state_cls = path_to_cls.get(state_path) + parent_cls = state_cls.get_parent_state() if state_cls else None + parent_cls_to_state_ids.setdefault(parent_cls, {}).setdefault( + minified_name, [] + ).append(state_path) - if parent_path not in parent_to_state_ids: - parent_to_state_ids[parent_path] = {} - if minified_name not in parent_to_state_ids[parent_path]: - parent_to_state_ids[parent_path][minified_name] = [] - parent_to_state_ids[parent_path][minified_name].append(state_path) - - for parent_path, id_to_states in parent_to_state_ids.items(): + for parent_cls, id_to_states in parent_cls_to_state_ids.items(): for minified_name, state_paths in id_to_states.items(): if len(state_paths) > 1: + parent_name = parent_cls.__name__ if parent_cls else "root" errors.append( - f"Duplicate state_id='{minified_name}' under '{parent_path or 'root'}': " + f"Duplicate state_id='{minified_name}' under '{parent_name}': " f"{state_paths}" ) @@ -525,31 +524,30 @@ def sync_minify_config( # Remove empty event dicts new_events = {k: v for k, v in new_events.items() if v} - # Find states that need IDs assigned - # Group by parent for sibling-unique assignment - parent_to_children: dict[str | None, list[str]] = {} + # Build a map from actual parent class to existing sibling minified IDs. + # Using the live state classes avoids the string-split bug where children + # of the same parent class defined in different modules get different + # string-based parent paths and are assigned colliding IDs. + parent_cls_to_existing_ids: dict[type[BaseState] | None, set[int]] = {} + for state_cls in all_states: + state_path = get_state_full_path(state_cls) + if state_path in new_states: + parent = state_cls.get_parent_state() + parent_cls_to_existing_ids.setdefault(parent, set()).add( + minified_name_to_int(new_states[state_path]) + ) + + # Find states that need IDs assigned, grouped by actual parent class. + parent_cls_to_new_children: dict[type[BaseState] | None, list[str]] = {} for state_cls in all_states: state_path = get_state_full_path(state_cls) if state_path not in new_states: parent = state_cls.get_parent_state() - parent_path = get_state_full_path(parent) if parent else None - if parent_path not in parent_to_children: - parent_to_children[parent_path] = [] - parent_to_children[parent_path].append(state_path) - - # Assign new state IDs - for parent_path, children in parent_to_children.items(): - # Get existing IDs for this parent's children (convert to ints for finding max) - existing_ids: set[int] = set() - for state_path, minified_name in new_states.items(): - parts = state_path.rsplit(".", 1) - sp_parent = parts[0] if len(parts) > 1 else None - # Compare parent paths correctly - if parent_path is None: - if sp_parent is None or "." not in state_path: - existing_ids.add(minified_name_to_int(minified_name)) - elif sp_parent == parent_path: - existing_ids.add(minified_name_to_int(minified_name)) + parent_cls_to_new_children.setdefault(parent, []).append(state_path) + + # Assign new state IDs (unique among siblings of the same parent class) + for parent_cls, children in parent_cls_to_new_children.items(): + existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()).copy() # Assign IDs starting from max + 1 (or 0 if reassign_deleted and gaps exist) next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1) diff --git a/reflex/state.py b/reflex/state.py index a7b1f5bf065..029fc379161 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1780,7 +1780,6 @@ async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE: ) return getattr(other_state, var_data.field_name) -<<<<<<< .merge_file_rqeWiK @classmethod def _get_original_event_name(cls, minified_name: str) -> str | None: """Look up the original event handler name from a minified name. @@ -1797,8 +1796,6 @@ def _get_original_event_name(cls, minified_name: str) -> str | None: # Direct lookup: _event_id_to_name maps minified_name -> original_name return cls._event_id_to_name.get(minified_name) -======= ->>>>>>> .merge_file_20JAsy def _get_event_handler(self, event: Event | str) -> tuple[BaseState, EventHandler]: """Get the event handler for the given event. @@ -1821,7 +1818,6 @@ def _get_event_handler(self, event: Event | str) -> tuple[BaseState, EventHandle msg = "The value of state cannot be None when processing an event." raise ValueError(msg) -<<<<<<< .merge_file_rqeWiK # Try to look up the handler directly first handler = substate.event_handlers.get(name) if handler is None: @@ -1833,8 +1829,6 @@ def _get_event_handler(self, event: Event | str) -> tuple[BaseState, EventHandle msg = f"Event handler '{name}' not found in state '{type(substate).__name__}'" raise KeyError(msg) -======= ->>>>>>> .merge_file_20JAsy return substate, handler async def _process(self, event: Event) -> AsyncIterator[StateUpdate]: diff --git a/tests/units/istate/test_proxy.py b/tests/units/istate/test_proxy.py index ab7e4f461d8..5fd29725fa9 100644 --- a/tests/units/istate/test_proxy.py +++ b/tests/units/istate/test_proxy.py @@ -2,7 +2,6 @@ import dataclasses import pickle -<<<<<<< .merge_file_7wIbhS from asyncio import CancelledError from contextlib import asynccontextmanager from unittest.mock import patch @@ -11,11 +10,6 @@ import reflex as rx from reflex.istate.proxy import MutableProxy, StateProxy -======= - -import reflex as rx -from reflex.istate.proxy import MutableProxy ->>>>>>> .merge_file_giC46q @dataclasses.dataclass @@ -46,7 +40,6 @@ def test_mutable_proxy_pickle_preserves_object_identity(): assert unpickled["direct"][0].id == 1 assert unpickled["proxied"][0].id == 1 assert unpickled["direct"][0] is unpickled["proxied"][0] -<<<<<<< .merge_file_7wIbhS @pytest.mark.usefixtures("mock_app") @@ -73,5 +66,3 @@ async def mock_modify_state_context(*args, **kwargs): # noqa: RUF029 # After the exception, we should be able to enter the context again without issues async with state_proxy: pass -======= ->>>>>>> .merge_file_giC46q diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index a941d175bc8..a42b9162db7 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -318,6 +318,81 @@ def handler_b(self): new_config["events"][state_path]["handler_b"] == "l" ) # 10 + 1 = 11 -> 'l' + def test_sync_no_sibling_collision_across_modules(self): + """Test that sync assigns unique IDs to siblings of the same parent. + + When children of the same parent state class are defined in different + Python modules, their get_state_full_path() produces different string + prefixes. The sync function must group siblings by the actual parent + class object, not by string-splitting the path, to avoid ID collisions. + """ + + class ParentState(BaseState): + pass + + class ChildA(ParentState): + pass + + class ChildB(ParentState): + pass + + parent_path = get_state_full_path(ParentState) + child_a_path = get_state_full_path(ChildA) + + # Config already has ParentState and ChildA assigned + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: "a", + child_a_path: "a", + }, + "events": {}, + } + + # Sync should assign ChildB a DIFFERENT ID than ChildA + new_config = sync_minify_config(existing_config, ParentState) + + child_b_path = get_state_full_path(ChildB) + assert child_b_path in new_config["states"] + assert ( + new_config["states"][child_a_path] != new_config["states"][child_b_path] + ), ( + f"Sibling collision: ChildA and ChildB both got " + f"'{new_config['states'][child_a_path]}'" + ) + + def test_validate_detects_sibling_collision(self): + """Test that validate catches duplicate IDs among siblings of same parent.""" + + class ParentState(BaseState): + pass + + class ChildA(ParentState): + pass + + class ChildB(ParentState): + pass + + parent_path = get_state_full_path(ParentState) + child_a_path = get_state_full_path(ChildA) + child_b_path = get_state_full_path(ChildB) + + # Manually create a config with colliding sibling IDs + bad_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: "a", + child_a_path: "a", + child_b_path: "a", # collision! + }, + "events": {}, + } + + errors, _warnings, _missing = validate_minify_config(bad_config, ParentState) + assert any("Duplicate" in e and "'a'" in e for e in errors), ( + f"Expected duplicate ID error, got: {errors}" + ) + class TestStateMinification: """Tests for state name minification with minify.json.""" From 9e73376eda9d8df5cc44e5e9f7f6d968dd5d9151 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 14:31:59 +0200 Subject: [PATCH 53/74] ruff format --- docs/app/agent_files/_plugin.py | 12 +- .../reflex_docs/pages/docs/cloud_cliref.py | 135 ++++++++++-------- docs/app/reflex_docs/pages/docs/component.py | 40 +++--- docs/app/reflex_docs/pages/docs/source.py | 14 +- .../pages/docs_landing/views/ai_builder.py | 18 +-- .../pages/integrations/integration_list.py | 18 +-- docs/app/tests/conftest.py | 16 ++- 7 files changed, 138 insertions(+), 115 deletions(-) diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 487e777976f..5e07e9b97cd 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -9,11 +9,13 @@ def generate_markdown_files() -> tuple[tuple[Path, str | bytes], ...]: from reflex_docs.pages.docs import doc_markdown_sources - return tuple([ - (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) - for route, source_path in doc_markdown_sources.items() - if (resolved := Path(source_path)).is_file() - ]) + return tuple( + [ + (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) + for route, source_path in doc_markdown_sources.items() + if (resolved := Path(source_path)).is_file() + ] + ) def generate_llms_txt( diff --git a/docs/app/reflex_docs/pages/docs/cloud_cliref.py b/docs/app/reflex_docs/pages/docs/cloud_cliref.py index b8051fba339..428f8eafeae 100644 --- a/docs/app/reflex_docs/pages/docs/cloud_cliref.py +++ b/docs/app/reflex_docs/pages/docs/cloud_cliref.py @@ -177,74 +177,83 @@ def process( """Convert a Click command to a Markdown element.""" actual_name = override_name or command["name"] full_name = prefix + " " + actual_name if prefix and actual_name else actual_name - cli_to_doc[full_name] = Section(( - Paragraph(InlineText(command["help"])) if command["help"] else Empty(), - Section(( - Header(3, InlineText("Usage")), - CodeBlock( - "$" - + (" " + prefix.strip() if prefix else "") - + " " - + actual_name.strip() - + ( - " [OPTIONS]" - if command["params"] - and any( - param.get("param_type_name") != "argument" - for param in command["params"] - ) - else "" - ) - + ( - " " + " ".join(arguments) - if ( - arguments := [ - param["name"].upper() - for param in command["params"] - if param.get("param_type_name") == "argument" - and param["name"] - ] - ) - else "" - ), - language="console", - ), - )) - if actual_name - else Empty(), - Section(( - Header(3, InlineText("Options")), - List( - tuple( - InlineTextCollection(( - InlineCode( - ", ".join(param["opts"]) - + ( - " / " + ", ".join(param["secondary_opts"]) - if param["secondary_opts"] - else "" + cli_to_doc[full_name] = Section( + ( + Paragraph(InlineText(command["help"])) if command["help"] else Empty(), + Section( + ( + Header(3, InlineText("Usage")), + CodeBlock( + "$" + + (" " + prefix.strip() if prefix else "") + + " " + + actual_name.strip() + + ( + " [OPTIONS]" + if command["params"] + and any( + param.get("param_type_name") != "argument" + for param in command["params"] ) - + ( + else "" + ) + + ( + " " + " ".join(arguments) + if ( + arguments := [ + param["name"].upper() + for param in command["params"] + if param.get("param_type_name") == "argument" + and param["name"] + ] + ) + else "" + ), + language="console", + ), + ) + ) + if actual_name + else Empty(), + Section( + ( + Header(3, InlineText("Options")), + List( + tuple( + InlineTextCollection( ( - " " + param["type"]["name"].upper() - if param["type"]["name"] != "boolean" - else "" + InlineCode( + ", ".join(param["opts"]) + + ( + " / " + ", ".join(param["secondary_opts"]) + if param["secondary_opts"] + else "" + ) + + ( + ( + " " + param["type"]["name"].upper() + if param["type"]["name"] != "boolean" + else "" + ) + if (choices := param["type"].get("choices")) + is None + else " [" + "|".join(choices) + "]" + ) + ), + InlineText(": " + option_help), ) - if (choices := param["type"].get("choices")) is None - else " [" + "|".join(choices) + "]" ) + for param in command["params"] + if (option_help := param.get("help")) is not None ), - InlineText(": " + option_help), - )) - for param in command["params"] - if (option_help := param.get("help")) is not None - ), - ordered=False, - ), - )) - if command["params"] - else Empty(), - )).into_text() + ordered=False, + ), + ) + ) + if command["params"] + else Empty(), + ) + ).into_text() for name, sub_command in sort_subcommands(command.get("commands", {})).items(): process( sub_command, diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 77be602c2a8..9aa31d8d8d8 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -140,10 +140,12 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="w-32 font-small text-slate-11"), rx.select.content( - rx.select.group(*[ - rx.select.item(item, value=item, class_name="font-small") - for item in literal_values - ]) + rx.select.group( + *[ + rx.select.item(item, value=item, class_name="font-small") + for item in literal_values + ] + ) ), value=var, on_change=setter, @@ -202,20 +204,22 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="font-small w-32 text-slate-11"), rx.select.content( - rx.select.group(*[ - rx.select.item( - item, - value=item, - class_name="font-small", - _hover=( - {"background": f"var(--{item}-9)"} - if prop.name == "color_scheme" - else None - ), - ) - for item in list(map(str, type_.__args__)) - if item != "" - ]), + rx.select.group( + *[ + rx.select.item( + item, + value=item, + class_name="font-small", + _hover=( + {"background": f"var(--{item}-9)"} + if prop.name == "color_scheme" + else None + ), + ) + for item in list(map(str, type_.__args__)) + if item != "" + ] + ), ), value=var, on_change=setter, diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 3ceadda4036..74c18cc63b0 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -28,12 +28,14 @@ def format_fields( rx.scroll_area( rx.table.root( rx.table.header( - rx.table.row(*[ - rx.table.column_header_cell( - header, class_name=table_header_class_name - ) - for header in headers - ]) + rx.table.row( + *[ + rx.table.column_header_cell( + header, class_name=table_header_class_name + ) + for header in headers + ] + ) ), rx.table.body( *[ diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index 788913d2e3f..a2491e8e1fc 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -51,15 +51,17 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append({ - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, + result.append( + { + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, + } } - }) + ) return result diff --git a/docs/app/reflex_docs/pages/integrations/integration_list.py b/docs/app/reflex_docs/pages/integrations/integration_list.py index f95c17403c4..e6f1a90826d 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_list.py +++ b/docs/app/reflex_docs/pages/integrations/integration_list.py @@ -46,14 +46,16 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append({ - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, + result.append( + { + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, + } } - }) + ) return result diff --git a/docs/app/tests/conftest.py b/docs/app/tests/conftest.py index d1b9a7e77ef..b0b941b347b 100644 --- a/docs/app/tests/conftest.py +++ b/docs/app/tests/conftest.py @@ -13,13 +13,15 @@ def reflex_web_app(): app_root = Path(__file__).parent.parent from reflex_docs.whitelist import WHITELISTED_PAGES - WHITELISTED_PAGES.extend([ - "/events", - "/vars", - "/getting-started", - "/library/graphing", - "/api-reference/special-events", - ]) + WHITELISTED_PAGES.extend( + [ + "/events", + "/vars", + "/getting-started", + "/library/graphing", + "/api-reference/special-events", + ] + ) with AppHarness.create(root=app_root) as harness: yield harness From 79b735d265dd3b1001c93d7d73f858de2b5fcecc Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 14:32:29 +0200 Subject: [PATCH 54/74] pre-commit --- docs/app/agent_files/_plugin.py | 12 +- .../reflex_docs/pages/docs/cloud_cliref.py | 135 ++++++++---------- docs/app/reflex_docs/pages/docs/component.py | 40 +++--- docs/app/reflex_docs/pages/docs/source.py | 14 +- .../pages/docs_landing/views/ai_builder.py | 18 ++- .../pages/integrations/integration_list.py | 18 ++- docs/app/tests/conftest.py | 16 +-- pyi_hashes.json | 119 +++++++++++++++ 8 files changed, 234 insertions(+), 138 deletions(-) diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 5e07e9b97cd..487e777976f 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -9,13 +9,11 @@ def generate_markdown_files() -> tuple[tuple[Path, str | bytes], ...]: from reflex_docs.pages.docs import doc_markdown_sources - return tuple( - [ - (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) - for route, source_path in doc_markdown_sources.items() - if (resolved := Path(source_path)).is_file() - ] - ) + return tuple([ + (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) + for route, source_path in doc_markdown_sources.items() + if (resolved := Path(source_path)).is_file() + ]) def generate_llms_txt( diff --git a/docs/app/reflex_docs/pages/docs/cloud_cliref.py b/docs/app/reflex_docs/pages/docs/cloud_cliref.py index 428f8eafeae..b8051fba339 100644 --- a/docs/app/reflex_docs/pages/docs/cloud_cliref.py +++ b/docs/app/reflex_docs/pages/docs/cloud_cliref.py @@ -177,83 +177,74 @@ def process( """Convert a Click command to a Markdown element.""" actual_name = override_name or command["name"] full_name = prefix + " " + actual_name if prefix and actual_name else actual_name - cli_to_doc[full_name] = Section( - ( - Paragraph(InlineText(command["help"])) if command["help"] else Empty(), - Section( - ( - Header(3, InlineText("Usage")), - CodeBlock( - "$" - + (" " + prefix.strip() if prefix else "") - + " " - + actual_name.strip() - + ( - " [OPTIONS]" - if command["params"] - and any( - param.get("param_type_name") != "argument" - for param in command["params"] - ) - else "" - ) - + ( - " " + " ".join(arguments) - if ( - arguments := [ - param["name"].upper() - for param in command["params"] - if param.get("param_type_name") == "argument" - and param["name"] - ] - ) - else "" - ), - language="console", - ), + cli_to_doc[full_name] = Section(( + Paragraph(InlineText(command["help"])) if command["help"] else Empty(), + Section(( + Header(3, InlineText("Usage")), + CodeBlock( + "$" + + (" " + prefix.strip() if prefix else "") + + " " + + actual_name.strip() + + ( + " [OPTIONS]" + if command["params"] + and any( + param.get("param_type_name") != "argument" + for param in command["params"] + ) + else "" ) - ) - if actual_name - else Empty(), - Section( - ( - Header(3, InlineText("Options")), - List( - tuple( - InlineTextCollection( + + ( + " " + " ".join(arguments) + if ( + arguments := [ + param["name"].upper() + for param in command["params"] + if param.get("param_type_name") == "argument" + and param["name"] + ] + ) + else "" + ), + language="console", + ), + )) + if actual_name + else Empty(), + Section(( + Header(3, InlineText("Options")), + List( + tuple( + InlineTextCollection(( + InlineCode( + ", ".join(param["opts"]) + + ( + " / " + ", ".join(param["secondary_opts"]) + if param["secondary_opts"] + else "" + ) + + ( ( - InlineCode( - ", ".join(param["opts"]) - + ( - " / " + ", ".join(param["secondary_opts"]) - if param["secondary_opts"] - else "" - ) - + ( - ( - " " + param["type"]["name"].upper() - if param["type"]["name"] != "boolean" - else "" - ) - if (choices := param["type"].get("choices")) - is None - else " [" + "|".join(choices) + "]" - ) - ), - InlineText(": " + option_help), + " " + param["type"]["name"].upper() + if param["type"]["name"] != "boolean" + else "" ) + if (choices := param["type"].get("choices")) is None + else " [" + "|".join(choices) + "]" ) - for param in command["params"] - if (option_help := param.get("help")) is not None ), - ordered=False, - ), - ) - ) - if command["params"] - else Empty(), - ) - ).into_text() + InlineText(": " + option_help), + )) + for param in command["params"] + if (option_help := param.get("help")) is not None + ), + ordered=False, + ), + )) + if command["params"] + else Empty(), + )).into_text() for name, sub_command in sort_subcommands(command.get("commands", {})).items(): process( sub_command, diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 9aa31d8d8d8..77be602c2a8 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -140,12 +140,10 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="w-32 font-small text-slate-11"), rx.select.content( - rx.select.group( - *[ - rx.select.item(item, value=item, class_name="font-small") - for item in literal_values - ] - ) + rx.select.group(*[ + rx.select.item(item, value=item, class_name="font-small") + for item in literal_values + ]) ), value=var, on_change=setter, @@ -204,22 +202,20 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="font-small w-32 text-slate-11"), rx.select.content( - rx.select.group( - *[ - rx.select.item( - item, - value=item, - class_name="font-small", - _hover=( - {"background": f"var(--{item}-9)"} - if prop.name == "color_scheme" - else None - ), - ) - for item in list(map(str, type_.__args__)) - if item != "" - ] - ), + rx.select.group(*[ + rx.select.item( + item, + value=item, + class_name="font-small", + _hover=( + {"background": f"var(--{item}-9)"} + if prop.name == "color_scheme" + else None + ), + ) + for item in list(map(str, type_.__args__)) + if item != "" + ]), ), value=var, on_change=setter, diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 74c18cc63b0..3ceadda4036 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -28,14 +28,12 @@ def format_fields( rx.scroll_area( rx.table.root( rx.table.header( - rx.table.row( - *[ - rx.table.column_header_cell( - header, class_name=table_header_class_name - ) - for header in headers - ] - ) + rx.table.row(*[ + rx.table.column_header_cell( + header, class_name=table_header_class_name + ) + for header in headers + ]) ), rx.table.body( *[ diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index a2491e8e1fc..788913d2e3f 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -51,17 +51,15 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append( - { - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, - } + result.append({ + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, } - ) + }) return result diff --git a/docs/app/reflex_docs/pages/integrations/integration_list.py b/docs/app/reflex_docs/pages/integrations/integration_list.py index e6f1a90826d..f95c17403c4 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_list.py +++ b/docs/app/reflex_docs/pages/integrations/integration_list.py @@ -46,16 +46,14 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append( - { - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, - } + result.append({ + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, } - ) + }) return result diff --git a/docs/app/tests/conftest.py b/docs/app/tests/conftest.py index b0b941b347b..d1b9a7e77ef 100644 --- a/docs/app/tests/conftest.py +++ b/docs/app/tests/conftest.py @@ -13,15 +13,13 @@ def reflex_web_app(): app_root = Path(__file__).parent.parent from reflex_docs.whitelist import WHITELISTED_PAGES - WHITELISTED_PAGES.extend( - [ - "/events", - "/vars", - "/getting-started", - "/library/graphing", - "/api-reference/special-events", - ] - ) + WHITELISTED_PAGES.extend([ + "/events", + "/vars", + "/getting-started", + "/library/graphing", + "/api-reference/special-events", + ]) with AppHarness.create(root=app_root) as harness: yield harness diff --git a/pyi_hashes.json b/pyi_hashes.json index e4c8693d9c8..ba57aa5a477 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -1,4 +1,123 @@ { + "packages/reflex-components-code/src/reflex_components_code/code.pyi": "a879ccd253e901964a7ab7ea7154f904", + "packages/reflex-components-code/src/reflex_components_code/shiki_code_block.pyi": "d3e0c33fdc34f5c154ac387d550c0d29", + "packages/reflex-components-core/src/reflex_components_core/__init__.pyi": "82b29d23f2490161d42fd21021bd39c3", + "packages/reflex-components-core/src/reflex_components_core/base/__init__.pyi": "7009187aaaf191814d031e5462c48318", + "packages/reflex-components-core/src/reflex_components_core/base/app_wrap.pyi": "ecccfd8a9b0e8b2f4128ff13ff27a9da", + "packages/reflex-components-core/src/reflex_components_core/base/body.pyi": "2535814d409e5feaf57da63dcf0abeaf", + "packages/reflex-components-core/src/reflex_components_core/base/document.pyi": "a2e67a9814dc61853ca2299d9d9c698d", + "packages/reflex-components-core/src/reflex_components_core/base/error_boundary.pyi": "59170074a1a228ce58685f3f207954f2", + "packages/reflex-components-core/src/reflex_components_core/base/fragment.pyi": "e4cbfc46eabb904596be4372392add35", + "packages/reflex-components-core/src/reflex_components_core/base/link.pyi": "629a483c570b04ca3d83ecdc53914770", + "packages/reflex-components-core/src/reflex_components_core/base/meta.pyi": "0cfa2d8c52321ce7440e887d03007d5b", + "packages/reflex-components-core/src/reflex_components_core/base/script.pyi": "bfc7fb609b822f597d1141595f8090fe", + "packages/reflex-components-core/src/reflex_components_core/base/strict_mode.pyi": "8ee129808abb4389cbd77a1736190eae", + "packages/reflex-components-core/src/reflex_components_core/core/__init__.pyi": "dd5142b3c9087bf2bf22651adf6f2724", + "packages/reflex-components-core/src/reflex_components_core/core/auto_scroll.pyi": "918dfad4d5925addd0f741e754b3b076", + "packages/reflex-components-core/src/reflex_components_core/core/banner.pyi": "6040fbada9b96c55637a9c8cc21a5e10", + "packages/reflex-components-core/src/reflex_components_core/core/clipboard.pyi": "e3950e0963a6d04299ff58294687e407", + "packages/reflex-components-core/src/reflex_components_core/core/debounce.pyi": "dd221754c5e076a3a833c8584da72dc5", + "packages/reflex-components-core/src/reflex_components_core/core/helmet.pyi": "7fd81a99bde5b0ff94bb52523597fd5c", + "packages/reflex-components-core/src/reflex_components_core/core/html.pyi": "753d6ae315369530dad450ed643f5be6", + "packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "ba60a7d9cba75b27a1133bd63a9fbd59", + "packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "17775edb94cc804686ae4cd873584810", + "packages/reflex-components-core/src/reflex_components_core/core/window_events.pyi": "cab827931770be082cd1598a9908abbc", + "packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.pyi": "c96fed4da42a13576d64f84e3c7cb25c", + "packages/reflex-components-core/src/reflex_components_core/el/__init__.pyi": "f09129ddefb57ab4c7769c86dc9a3153", + "packages/reflex-components-core/src/reflex_components_core/el/element.pyi": "ff68d843c5987d3f0d773a6367eb9c63", + "packages/reflex-components-core/src/reflex_components_core/el/elements/__init__.pyi": "e6c845f2f29eb079697a2e31b0c2f23a", + "packages/reflex-components-core/src/reflex_components_core/el/elements/base.pyi": "d2500a39e6e532bb90c83438343905bf", + "packages/reflex-components-core/src/reflex_components_core/el/elements/forms.pyi": "ca840a20c8e1c1f5335fb815a25b6c32", + "packages/reflex-components-core/src/reflex_components_core/el/elements/inline.pyi": "c38a432d1fd0c3208c4fc3a546c67e4d", + "packages/reflex-components-core/src/reflex_components_core/el/elements/media.pyi": "b794f4f4f7ad17c6939d5526b9c63397", + "packages/reflex-components-core/src/reflex_components_core/el/elements/metadata.pyi": "f9e51feebda79fb063bc264a235df0c3", + "packages/reflex-components-core/src/reflex_components_core/el/elements/other.pyi": "c86abf00384b5f15725a0daf2533848d", + "packages/reflex-components-core/src/reflex_components_core/el/elements/scripts.pyi": "222176bffc14191018fd0e3af3741aff", + "packages/reflex-components-core/src/reflex_components_core/el/elements/sectioning.pyi": "fbbe0bf222d4196c32c88d05cb077997", + "packages/reflex-components-core/src/reflex_components_core/el/elements/tables.pyi": "cba93678248925c981935a251379aa7c", + "packages/reflex-components-core/src/reflex_components_core/el/elements/typography.pyi": "4abedc6f98f6d54194ff9e7f1f76314e", + "packages/reflex-components-core/src/reflex_components_core/react_router/dom.pyi": "1074a512195ae23d479c4a2d553954e1", + "packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "8e379fa038c7c6c0672639eb5902934d", + "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "d2dc211d707c402eb24678a4cba945f7", + "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "e3ec310276f9d091fbb0261e523ca9ed", + "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "da02f81678d920a68101c08fe64483a5", + "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "d6a02e447dfd3c91bba84bcd02722aed", + "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "91e956633778c6992f04940c69ff7140", + "packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "19216eb3618f68c8a76e5e43801cf4af", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "5404a8da97e8b5129133d7f300e3f642", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.pyi": "e8ef2b44f2afe3e9b8d678d523673882", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/base.pyi": "e779c6739baee98c8a588768a88de45a", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/dialog.pyi": "ffb06f3aa8722c2345a952869118e224", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/drawer.pyi": "cc724f697e62efba294e19b58c6f1bd8", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/form.pyi": "4d6121ccc963c64e33c49acd4295eb7a", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/progress.pyi": "b3b66ec57525c53ea741897e2bc8370e", + "packages/reflex-components-radix/src/reflex_components_radix/primitives/slider.pyi": "c86bc8d4604e3d8c8d40baad2ac6dc17", + "packages/reflex-components-radix/src/reflex_components_radix/themes/__init__.pyi": "b433b9a099dc5b0ab008d02c85d38059", + "packages/reflex-components-radix/src/reflex_components_radix/themes/base.pyi": "e75cbf2a34620721432b1556f3c875cd", + "packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.pyi": "ed020269e4728cc6abe72354193146b7", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/__init__.pyi": "f10f0169f81c78290333da831915762f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/alert_dialog.pyi": "d5e0419729df4ddf2caf214f40ae7845", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/aspect_ratio.pyi": "613abb9870259547c99eb434a3a17512", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/avatar.pyi": "1671e796449b236386d8f53d33e42b2f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/badge.pyi": "9fde9929ca5197e0e1880bce9a08e926", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/button.pyi": "e5d6387a93c74dafaa0d6f1719e08bac", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/callout.pyi": "31da62c4d8c1d459089aab32cd232feb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/card.pyi": "76afb58340c6be1f26b7b110473efa55", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox.pyi": "6cbf013e21d7280118dfd7383998b3bf", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_cards.pyi": "63b4134246f68f9f556896d6ce194462", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/checkbox_group.pyi": "ec3f89e7d187303344d4127a83522b22", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/context_menu.pyi": "99541ee46f112eb4096f903a99f5ffb8", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/data_list.pyi": "1dfd91741ff402b3ed93b6daca4939f3", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dialog.pyi": "ed9198da4a7950a8579e50ad970c34ef", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/dropdown_menu.pyi": "15f9cee0584414f2d2e0fb82c167f216", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/hover_card.pyi": "3f328bb0ba5225e4478febf8c7623833", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/icon_button.pyi": "be8eed28e19221a406e554829809ff0d", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/inset.pyi": "9b2adf18f7d239b8e7431f39042ed301", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/popover.pyi": "4d5813a47b8f8b6ac317ca01d87d9afb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/progress.pyi": "1ab01f45a4c5ef4211eacc00cc99e4a5", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio.pyi": "38a7412205a98617f98218a5b213ada1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_cards.pyi": "d84b16ac16083a534199fd23659aaa06", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/radio_group.pyi": "51fda6313f1ce86d5b1ffdfd68ae8b74", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/scroll_area.pyi": "bba40e5eae75314157378c9e8b0eea73", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/segmented_control.pyi": "bf9f751a701137bfedc254657d4c5be4", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/select.pyi": "605479e11d19dd7730c90125b198c9b6", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/separator.pyi": "519781d33b99c675a12014d400e54d08", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/skeleton.pyi": "f4848f7d89abb4c78f6db52c624cdabf", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/slider.pyi": "61a08374fa19a0bb3f52b8654effc0f2", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/spinner.pyi": "530c51742031389d4b2ae43548ff0f03", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/switch.pyi": "23b21bc11a0012e13ce9bb79b47ba146", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/table.pyi": "8364f40600870bafa585528d9cadedf8", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tabs.pyi": "3a52910c327f55656eb59309f9362361", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_area.pyi": "fcf562b2f61ecdcc2de6f70d2ebf9907", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/text_field.pyi": "250b7e77b67e7d8cd3fff2b40526c04c", + "packages/reflex-components-radix/src/reflex_components_radix/themes/components/tooltip.pyi": "799acce0af81899a3a310bdcd43c403b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/__init__.pyi": "9e452af27229b676ad0146e40f75bed5", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/base.pyi": "5b262189e235cac17182e79188b1681a", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/box.pyi": "e06c8fd64132765d61b9edb87a48558b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/center.pyi": "5aa934d7c6ba3889fa943eabee7dc05f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/container.pyi": "c67fafd1aec105cb5a9927ff0e6d2071", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/flex.pyi": "aa68061a8e5dfd4adf336d1d1cb000fb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/grid.pyi": "06b92d31331c6f08b5083fcc811b754a", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/list.pyi": "e7cd3a9cea1c34e21f731f1bd05c1ceb", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/section.pyi": "8c968fead3155b2d51c687459811b5df", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/spacer.pyi": "cfc8a927642e5b68feabc80080aeb8dc", + "packages/reflex-components-radix/src/reflex_components_radix/themes/layout/stack.pyi": "cf88cf870eefaacaf765ead10fb4593b", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/__init__.pyi": "de7ee994f66a4c1d1a6ac2ad3370c30e", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/blockquote.pyi": "92d5a2df77a69a28a4d591000ee46bd1", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/code.pyi": "8a1e4376cadf4961212d39a5128a0e4f", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/heading.pyi": "34c7ed3fe1e5f702a98d72751b0052fa", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/link.pyi": "619a9d8351748fffe76136002931e583", + "packages/reflex-components-radix/src/reflex_components_radix/themes/typography/text.pyi": "4919daa4483b7c12f6fafd02a2275e0f", + "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "0817c9232a6e4790cff8ea8aa6001950", + "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "6c1c26149d57c708fab04b82de0eb515", + "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "75207a9fe4f37ec2a2f1becbbbd5237b", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "7b8b69840a3637c1f1cac45ba815cccf", + "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "277bbf09d72e0c450241f0b7d39ebb60", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "be20d1d71c3b16f7e973a0329c3d81d6", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "c051ab3a26c23107043e203b060e1412", + "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "1979bb6c22bb7a0d3342b2d63fb19d74", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "234407dbd466bf9c87d75ce979ab0e2d", + "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "2c5fadcc014056f041cd4d916137d9e7", "reflex/__init__.pyi": "3a9bb8544cbc338ffaf0a5927d9156df", "reflex/components/__init__.pyi": "f39a2af77f438fa243c58c965f19d42e", "reflex/experimental/memo.pyi": "2c119a0dfea362dcd8193786363cbc02" From 052beca9350b11f5383c1321189b4b53ca5bee94 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 15:11:44 +0200 Subject: [PATCH 55/74] test adjustments --- tests/units/test_minification.py | 84 +++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 3621281a6fd..6362c4af753 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -627,9 +627,21 @@ class TestStateWithSetvar(State): def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" + from reflex_base import config as base_config + monkeypatch.setenv( environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value ) + # state_auto_setters is False by default; force it on so that + # `_init_var` actually creates the setter we want to verify. + real_get_config = base_config.get_config + + def _mock_get_config(*args, **kwargs): + cfg = real_get_config(*args, **kwargs) + cfg.state_auto_setters = True + return cfg + + monkeypatch.setattr(base_config, "get_config", _mock_get_config) expected_module = "tests.units.test_minification" expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" @@ -856,17 +868,19 @@ def test_get_class_substate_with_parent_child_name_collision( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) - # Build a hierarchy: State -> ParentState -> ChildState - # where ParentState and ChildState both get minified name "b" - - class ParentState(State): - pass - - class ChildState(ParentState): - pass - - parent_path = get_state_full_path(ParentState) - child_path = get_state_full_path(ChildState) + # Build a hierarchy: State -> ParentClassSubstateCollision -> ChildClassSubstateCollision + # where both child classes get minified name "b". + # The substate registry is keyed by parent.get_full_name() at + # registration time, so the minify config must be loaded *before* + # the test classes are defined. Class names are deliberately unique + # so `_handle_local_def` does not append a numeric suffix when run + # alongside other tests that also define `ParentState`. + expected_module = "tests.units.test_minification" + parent_path = f"{expected_module}.State.ParentClassSubstateCollision" + child_path = ( + f"{expected_module}.State.ParentClassSubstateCollision." + "ChildClassSubstateCollision" + ) config: MinifyConfig = { "version": SCHEMA_VERSION, @@ -882,10 +896,15 @@ class ChildState(ParentState): State.get_name.cache_clear() State.get_full_name.cache_clear() State.get_class_substate.cache_clear() - ParentState.get_name.cache_clear() - ParentState.get_full_name.cache_clear() - ChildState.get_name.cache_clear() - ChildState.get_full_name.cache_clear() + + class ParentClassSubstateCollision(State): + pass + + class ChildClassSubstateCollision(ParentClassSubstateCollision): + pass + + ParentState = ParentClassSubstateCollision + ChildState = ChildClassSubstateCollision # Verify both get the same minified name assert ParentState.get_name() == "b" @@ -910,16 +929,17 @@ def test_get_substate_with_parent_child_name_collision( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) - class ParentState2(State): - pass - - class ChildState2(ParentState2): - @rx.event - def my_handler(self): - pass - - parent_path = get_state_full_path(ParentState2) - child_path = get_state_full_path(ChildState2) + # Substates are registered under parent.get_full_name() at class + # creation time, so the minify config must be in place before the + # test classes are defined. Class names are deliberately unique so + # `_handle_local_def` does not append a numeric suffix when run + # alongside other tests that also define `ParentState2`. + expected_module = "tests.units.test_minification" + parent_path = f"{expected_module}.State.ParentInstanceSubstateCollision" + child_path = ( + f"{expected_module}.State.ParentInstanceSubstateCollision." + "ChildInstanceSubstateCollision" + ) config: MinifyConfig = { "version": SCHEMA_VERSION, @@ -935,10 +955,16 @@ def my_handler(self): State.get_name.cache_clear() State.get_full_name.cache_clear() State.get_class_substate.cache_clear() - ParentState2.get_name.cache_clear() - ParentState2.get_full_name.cache_clear() - ChildState2.get_name.cache_clear() - ChildState2.get_full_name.cache_clear() + + class ParentInstanceSubstateCollision(State): + pass + + class ChildInstanceSubstateCollision(ParentInstanceSubstateCollision): + @rx.event + def my_handler(self): + pass + + ChildState2 = ChildInstanceSubstateCollision # Create a state instance tree root = State(_reflex_internal_init=True) # type: ignore[call-arg] From 1cc98a8cd51c4ddf1e0c0c107593141b6c351dfb Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 22:39:14 +0200 Subject: [PATCH 56/74] migrate minify to registry name resolver --- docs/app/agent_files/_plugin.py | 12 +- .../reflex_docs/pages/docs/cloud_cliref.py | 135 ++++--- docs/app/reflex_docs/pages/docs/component.py | 40 +- docs/app/reflex_docs/pages/docs/source.py | 14 +- .../pages/docs_landing/views/ai_builder.py | 18 +- .../pages/integrations/integration_list.py | 18 +- docs/app/tests/conftest.py | 16 +- .../reflex-base/src/reflex_base/registry.py | 175 +++++++- .../src/reflex_base/utils/format.py | 35 +- reflex/minify.py | 246 ++++++++--- reflex/reflex.py | 21 +- reflex/state.py | 91 ++--- tests/units/test_minification.py | 382 ++++++++++++++---- 13 files changed, 850 insertions(+), 353 deletions(-) diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 487e777976f..5e07e9b97cd 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -9,11 +9,13 @@ def generate_markdown_files() -> tuple[tuple[Path, str | bytes], ...]: from reflex_docs.pages.docs import doc_markdown_sources - return tuple([ - (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) - for route, source_path in doc_markdown_sources.items() - if (resolved := Path(source_path)).is_file() - ]) + return tuple( + [ + (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) + for route, source_path in doc_markdown_sources.items() + if (resolved := Path(source_path)).is_file() + ] + ) def generate_llms_txt( diff --git a/docs/app/reflex_docs/pages/docs/cloud_cliref.py b/docs/app/reflex_docs/pages/docs/cloud_cliref.py index b8051fba339..428f8eafeae 100644 --- a/docs/app/reflex_docs/pages/docs/cloud_cliref.py +++ b/docs/app/reflex_docs/pages/docs/cloud_cliref.py @@ -177,74 +177,83 @@ def process( """Convert a Click command to a Markdown element.""" actual_name = override_name or command["name"] full_name = prefix + " " + actual_name if prefix and actual_name else actual_name - cli_to_doc[full_name] = Section(( - Paragraph(InlineText(command["help"])) if command["help"] else Empty(), - Section(( - Header(3, InlineText("Usage")), - CodeBlock( - "$" - + (" " + prefix.strip() if prefix else "") - + " " - + actual_name.strip() - + ( - " [OPTIONS]" - if command["params"] - and any( - param.get("param_type_name") != "argument" - for param in command["params"] - ) - else "" - ) - + ( - " " + " ".join(arguments) - if ( - arguments := [ - param["name"].upper() - for param in command["params"] - if param.get("param_type_name") == "argument" - and param["name"] - ] - ) - else "" - ), - language="console", - ), - )) - if actual_name - else Empty(), - Section(( - Header(3, InlineText("Options")), - List( - tuple( - InlineTextCollection(( - InlineCode( - ", ".join(param["opts"]) - + ( - " / " + ", ".join(param["secondary_opts"]) - if param["secondary_opts"] - else "" + cli_to_doc[full_name] = Section( + ( + Paragraph(InlineText(command["help"])) if command["help"] else Empty(), + Section( + ( + Header(3, InlineText("Usage")), + CodeBlock( + "$" + + (" " + prefix.strip() if prefix else "") + + " " + + actual_name.strip() + + ( + " [OPTIONS]" + if command["params"] + and any( + param.get("param_type_name") != "argument" + for param in command["params"] ) - + ( + else "" + ) + + ( + " " + " ".join(arguments) + if ( + arguments := [ + param["name"].upper() + for param in command["params"] + if param.get("param_type_name") == "argument" + and param["name"] + ] + ) + else "" + ), + language="console", + ), + ) + ) + if actual_name + else Empty(), + Section( + ( + Header(3, InlineText("Options")), + List( + tuple( + InlineTextCollection( ( - " " + param["type"]["name"].upper() - if param["type"]["name"] != "boolean" - else "" + InlineCode( + ", ".join(param["opts"]) + + ( + " / " + ", ".join(param["secondary_opts"]) + if param["secondary_opts"] + else "" + ) + + ( + ( + " " + param["type"]["name"].upper() + if param["type"]["name"] != "boolean" + else "" + ) + if (choices := param["type"].get("choices")) + is None + else " [" + "|".join(choices) + "]" + ) + ), + InlineText(": " + option_help), ) - if (choices := param["type"].get("choices")) is None - else " [" + "|".join(choices) + "]" ) + for param in command["params"] + if (option_help := param.get("help")) is not None ), - InlineText(": " + option_help), - )) - for param in command["params"] - if (option_help := param.get("help")) is not None - ), - ordered=False, - ), - )) - if command["params"] - else Empty(), - )).into_text() + ordered=False, + ), + ) + ) + if command["params"] + else Empty(), + ) + ).into_text() for name, sub_command in sort_subcommands(command.get("commands", {})).items(): process( sub_command, diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 77be602c2a8..9aa31d8d8d8 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -140,10 +140,12 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="w-32 font-small text-slate-11"), rx.select.content( - rx.select.group(*[ - rx.select.item(item, value=item, class_name="font-small") - for item in literal_values - ]) + rx.select.group( + *[ + rx.select.item(item, value=item, class_name="font-small") + for item in literal_values + ] + ) ), value=var, on_change=setter, @@ -202,20 +204,22 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="font-small w-32 text-slate-11"), rx.select.content( - rx.select.group(*[ - rx.select.item( - item, - value=item, - class_name="font-small", - _hover=( - {"background": f"var(--{item}-9)"} - if prop.name == "color_scheme" - else None - ), - ) - for item in list(map(str, type_.__args__)) - if item != "" - ]), + rx.select.group( + *[ + rx.select.item( + item, + value=item, + class_name="font-small", + _hover=( + {"background": f"var(--{item}-9)"} + if prop.name == "color_scheme" + else None + ), + ) + for item in list(map(str, type_.__args__)) + if item != "" + ] + ), ), value=var, on_change=setter, diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 3ceadda4036..74c18cc63b0 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -28,12 +28,14 @@ def format_fields( rx.scroll_area( rx.table.root( rx.table.header( - rx.table.row(*[ - rx.table.column_header_cell( - header, class_name=table_header_class_name - ) - for header in headers - ]) + rx.table.row( + *[ + rx.table.column_header_cell( + header, class_name=table_header_class_name + ) + for header in headers + ] + ) ), rx.table.body( *[ diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index 788913d2e3f..a2491e8e1fc 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -51,15 +51,17 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append({ - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, + result.append( + { + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, + } } - }) + ) return result diff --git a/docs/app/reflex_docs/pages/integrations/integration_list.py b/docs/app/reflex_docs/pages/integrations/integration_list.py index f95c17403c4..e6f1a90826d 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_list.py +++ b/docs/app/reflex_docs/pages/integrations/integration_list.py @@ -46,14 +46,16 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append({ - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, + result.append( + { + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, + } } - }) + ) return result diff --git a/docs/app/tests/conftest.py b/docs/app/tests/conftest.py index d1b9a7e77ef..b0b941b347b 100644 --- a/docs/app/tests/conftest.py +++ b/docs/app/tests/conftest.py @@ -13,13 +13,15 @@ def reflex_web_app(): app_root = Path(__file__).parent.parent from reflex_docs.whitelist import WHITELISTED_PAGES - WHITELISTED_PAGES.extend([ - "/events", - "/vars", - "/getting-started", - "/library/graphing", - "/api-reference/special-events", - ]) + WHITELISTED_PAGES.extend( + [ + "/events", + "/vars", + "/getting-started", + "/library/graphing", + "/api-reference/special-events", + ] + ) with AppHarness.create(root=app_root) as harness: yield harness diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 8caa1d2b2c3..36906cf5d68 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -1,9 +1,24 @@ -"""A contextual registry for state and event handlers.""" +"""A contextual registry for state and event handlers. + +The registry owns three kinds of bookkeeping: + +* the set of registered ``BaseState`` subclasses and their parent/child topology +* the set of registered ``EventHandler`` instances keyed by their full dotted name +* the **name resolution** strategy that turns a state class or handler name into the + string used by the rest of the framework + +The third concern is pluggable through the :class:`NameResolver` protocol. The +default resolver is a no-op (every state and handler keeps its built-in name). +``reflex.minify`` ships a :class:`MinifyNameResolver` that consults +``minify.json``; users can plug in their own (custom prefixes, multi-tenant +aliasing, deterministic test fixtures, etc.) by calling +:meth:`RegistrationContext.set_name_resolver`. +""" from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Protocol, runtime_checkable from typing_extensions import Self @@ -16,6 +31,46 @@ from reflex_base.event import EventHandler +@runtime_checkable +class NameResolver(Protocol): + """Resolves the user-visible names for state classes and event handlers. + + Implementations may apply minification, prefixing, locale-based aliasing, + or any other transformation. Returning ``None`` from either method means + "no override — use the default name". + + The protocol is intentionally tiny so resolvers compose well; see + :class:`DefaultNameResolver` for the no-op base case and + ``reflex.minify.MinifyNameResolver`` for the canonical example of a + resolver that consults external configuration. + """ + + def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: + """Return the resolved name for ``state_cls``, or ``None`` for default.""" + ... + + def resolve_handler_name( + self, state_cls: type[BaseState], handler_name: str + ) -> str | None: + """Return the resolved name for the handler, or ``None`` for default.""" + ... + + +@dataclasses.dataclass(frozen=True, slots=True) +class DefaultNameResolver: + """No-op resolver — every state and handler keeps its default name.""" + + def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: D102 + return None + + def resolve_handler_name( # noqa: D102 + self, + state_cls: type[BaseState], + handler_name: str, + ) -> str | None: + return None + + @dataclasses.dataclass(frozen=True, kw_only=True, slots=True) class RegisteredEventHandler: """A registered event handler, which includes the handler and its full name.""" @@ -44,6 +99,10 @@ class RegistrationContext(BaseContext): default_factory=dict, repr=False, ) + name_resolver: NameResolver = dataclasses.field( + default_factory=DefaultNameResolver, + repr=False, + ) @classmethod def ensure_context(cls) -> Self: @@ -158,3 +217,115 @@ def get_substates( return self.base_state_substates.setdefault( base_state_cls.get_full_name(), set() ) + + @staticmethod + def default_state_name(state_cls: type[BaseState]) -> str: + """Compute the built-in name for a state class (no resolver applied). + + This is the snake-cased ``module___ClassName`` form used when no + resolver overrides it. + + Args: + state_cls: The state class. + + Returns: + The default name. + """ + from reflex.utils import format + + module = state_cls.__module__.replace(".", "___") + return format.to_snake_case(f"{module}___{state_cls.__name__}") + + def get_state_name(self, state_cls: type[BaseState]) -> str: + """Resolve the user-visible name for a state class. + + Asks the installed :class:`NameResolver` first; falls back to + :meth:`default_state_name` when the resolver returns ``None``. + + Args: + state_cls: The state class. + + Returns: + The resolved name. + """ + resolved = self.name_resolver.resolve_state_name(state_cls) + if resolved is not None: + return resolved + return self.default_state_name(state_cls) + + def get_handler_name(self, state_cls: type[BaseState], handler_name: str) -> str: + """Resolve the user-visible name for an event handler. + + Asks the installed :class:`NameResolver` first; falls back to the + original ``handler_name`` when the resolver returns ``None``. + + Args: + state_cls: The state class the handler is attached to. + handler_name: The original (Python) name of the handler. + + Returns: + The resolved name. + """ + resolved = self.name_resolver.resolve_handler_name(state_cls, handler_name) + if resolved is not None: + return resolved + return handler_name + + def set_name_resolver(self, resolver: NameResolver) -> None: + """Install ``resolver`` and propagate the new names through the registry. + + Concretely: clears the per-class ``get_name``/``get_full_name``/ + ``get_class_substate`` lru_caches on every registered state and calls + :meth:`refresh_keys` so the registry's name-keyed dicts reflect what + the new resolver returns. + + ``resolver`` is stored even on a ``frozen`` dataclass via + ``object.__setattr__`` — the field is conceptually mutable while the + rest of the context shape stays immutable. + + Args: + resolver: The resolver to install. Pass :class:`DefaultNameResolver` + to revert to built-in names. + """ + object.__setattr__(self, "name_resolver", resolver) + for cls in self.base_states.values(): + cls.get_name.cache_clear() + cls.get_full_name.cache_clear() + cls.get_class_substate.cache_clear() + self.refresh_keys() + + def refresh_keys(self) -> None: + """Re-key all registered classes/handlers using current full names. + + State minification rewrites ``BaseState.get_name()``, but the registry + uses ``parent.get_full_name()`` as the dict key at registration time. + If the minify config (or env var) changes after a class was registered, + lookups will miss. Call this after ``minify.clear_config_cache()`` to + rebuild the dicts with the current names. + + Builds the replacement dicts before mutating ``self`` so a failure + partway through (e.g. an unreadable minify.json that makes + ``format_event_handler`` raise) leaves the existing registry intact. + """ + from reflex.utils.format import format_event_handler + + all_classes = list(self.base_states.values()) + new_base_states: dict[str, type[BaseState]] = {} + new_substates: dict[str, set[type[BaseState]]] = {} + for cls in all_classes: + new_base_states[cls.get_full_name()] = cls + parent = cls.get_parent_state() + if parent is not None: + new_substates.setdefault(parent.get_full_name(), set()).add(cls) + + all_handlers = list(self.event_handlers.values()) + new_handlers: dict[str, RegisteredEventHandler] = { + format_event_handler(reg.handler): reg for reg in all_handlers + } + + self.base_states.clear() + self.base_states.update(new_base_states) + self.base_state_substates.clear() + self.base_state_substates.update(new_substates) + self.event_handlers.clear() + self.event_handlers.update(new_handlers) diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index ee3da4f6c95..3e1b633ddcd 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -450,46 +450,43 @@ def get_event_handler_parts( ) -> tuple[str, str]: """Get the state and function name of an event handler. + Both the state name (via ``state.get_full_name()``) and the handler name + (via :meth:`~reflex_base.registry.RegistrationContext.get_handler_name`) + pass through the active :class:`~reflex_base.registry.NameResolver`, so + minification — or any other pluggable rewrite — is applied transparently. + Args: handler: The event handler to get the parts of. Returns: - The state and function name (possibly minified based on minify.json). + The (resolved) state full name and (resolved) handler function name. Raises: TypeError: If the handler is not an EventHandler. """ - from reflex.minify import is_event_minify_enabled from reflex_base.event import EventHandler + from reflex_base.registry import RegistrationContext if not isinstance(handler, EventHandler): msg = f"Expected EventHandler, got {type(handler)}" raise TypeError(msg) - # Get the name of the event function. + # The Python name of the handler function (e.g. "ClassName.handler_name"). name = handler.fn.__qualname__ - # Get the state full name - state_full_name = handler.state.get_full_name() if handler.state else "" - - # If there's no enclosing state, just return the full name. + # If there's no enclosing state, just return the full qualname. if handler.state is None: return ("", name) - # Get the event name inside the state. + state_full_name = handler.state.get_full_name() func_name = name.rpartition(".")[2] - # Check for event_id minification from minify.json. - # The state class stores its mapping in _event_id_to_name where key is - # minified_name and value is original_handler_name. Use the handler's own - # state class directly to avoid touching reflex.state during init. - if is_event_minify_enabled(): - for minified_name, handler_name in handler.state._event_id_to_name.items(): - if handler_name == func_name: - func_name = minified_name - break - - return (state_full_name, func_name) + # Let the registry's resolver have a say (e.g. minified handler names). + try: + ctx = RegistrationContext.get() + except LookupError: + return (state_full_name, func_name) + return (state_full_name, ctx.get_handler_name(handler.state, func_name)) def format_event_handler(handler: EventHandler | Callable[..., Any]) -> str: diff --git a/reflex/minify.py b/reflex/minify.py index 3b8593de3ea..c0508339343 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -1,11 +1,25 @@ """Minification configuration for state and event names. -This module provides centralized ID management for minifying state and event handler -names. The configuration is stored in a `minify.json` file at the project root. +This module provides centralized ID management for minifying state and event +handler names. The configuration is stored in a ``minify.json`` file at the +project root. + +The minification entry point is :class:`MinifyNameResolver`, an implementation +of :class:`reflex_base.registry.NameResolver`. To enable minification at +runtime, install the resolver into the active registration context:: + + from reflex.minify import MinifyNameResolver + from reflex_base.registry import RegistrationContext + + RegistrationContext.get().set_name_resolver(MinifyNameResolver.from_disk()) + +:func:`clear_config_cache` does this automatically — call it whenever +``minify.json`` or the ``REFLEX_MINIFY_*`` environment variables change. """ from __future__ import annotations +import dataclasses import functools import json from pathlib import Path @@ -203,14 +217,123 @@ def save_minify_config(config: MinifyConfig) -> None: f.write("\n") -def clear_config_cache() -> None: - """Clear the cached configuration. +@dataclasses.dataclass(slots=True) +class MinifyNameResolver: + """:class:`~reflex_base.registry.NameResolver` driven by ``minify.json``. + + Returns the minified name from the configuration when the corresponding + ``REFLEX_MINIFY_STATES`` / ``REFLEX_MINIFY_EVENTS`` env-var is set to + ``enabled`` *and* the entry exists in the config. All other lookups return + ``None`` so the registry falls back to the default name. + + The resolver is constructed once (typically via :meth:`from_disk`) and + held by the active :class:`~reflex_base.registry.RegistrationContext`. + Per-class lookups are memoized in ``_state_cache`` and ``_event_cache`` + for O(1) amortized cost on the hot path. + + Attributes: + config: The parsed ``minify.json`` content, or ``None`` when the file + is absent or malformed. + states_enabled: Whether ``REFLEX_MINIFY_STATES`` is on. + events_enabled: Whether ``REFLEX_MINIFY_EVENTS`` is on. + """ - This should be called after modifying minify.json programmatically. + config: MinifyConfig | None + states_enabled: bool + events_enabled: bool + _state_cache: dict[type[BaseState], str] = dataclasses.field( + default_factory=dict, repr=False + ) + _event_cache: dict[type[BaseState], dict[str, str]] = dataclasses.field( + default_factory=dict, repr=False + ) + + @classmethod + def from_disk(cls) -> MinifyNameResolver: + """Build a resolver from the current ``minify.json`` and env vars. + + Reads each input exactly once. Subsequent state/event lookups never + touch the filesystem. + + Returns: + A configured resolver. May still return ``None`` from every lookup + if minification is disabled or the config file is absent. + """ + from reflex.environment import MinifyMode, environment + + config: MinifyConfig | None + try: + config = _load_minify_config_uncached() + except ValueError: + # Treat malformed config as "no config" for the resolver — callers + # that need to surface the error can read it via get_minify_config. + config = None + return cls( + config=config, + states_enabled=environment.REFLEX_MINIFY_STATES.get() == MinifyMode.ENABLED, + events_enabled=environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED, + ) + + def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: D102 + if not (self.states_enabled and self.config): + return None + cached = self._state_cache.get(state_cls) + if cached is not None: + return cached + resolved = self.config["states"].get(get_state_full_path(state_cls)) + if resolved is not None: + self._state_cache[state_cls] = resolved + return resolved + + def resolve_handler_name( # noqa: D102 + self, state_cls: type[BaseState], handler_name: str + ) -> str | None: + if not (self.events_enabled and self.config): + return None + per_state = self._event_cache.get(state_cls) + if per_state is None: + per_state = self.config["events"].get(get_state_full_path(state_cls), {}) + self._event_cache[state_cls] = per_state + return per_state.get(handler_name) + + +def install_minify_resolver() -> None: + """Install a fresh :class:`MinifyNameResolver` into the active context. + + Use this after the user's app has been imported (so the registration + context contains the right state classes) to switch the framework over + to minified names. + + Returns silently if no registration context is currently attached. + """ + try: + from reflex_base.registry import RegistrationContext + + ctx = RegistrationContext.get() + except LookupError: + return + ctx.set_name_resolver(MinifyNameResolver.from_disk()) + + +def clear_config_cache() -> None: + """Reload the minify configuration and propagate it through the registry. + + Clears the module-level lru_caches for :func:`get_minify_config`, + :func:`is_state_minify_enabled`, :func:`is_event_minify_enabled`, then + rebuilds the :class:`MinifyNameResolver` from the current + ``minify.json`` / env vars and installs it via + :meth:`reflex_base.registry.RegistrationContext.set_name_resolver`. The + set-resolver call clears per-class name caches and re-keys the registry + in one atomic step. + + Call this whenever ``minify.json`` is rewritten programmatically, or + after monkey-patching ``REFLEX_MINIFY_STATES`` / ``REFLEX_MINIFY_EVENTS`` + at runtime. """ get_minify_config.cache_clear() is_state_minify_enabled.cache_clear() is_event_minify_enabled.cache_clear() + install_minify_resolver() # Base-54 encoding for minified names @@ -303,75 +426,81 @@ def get_state_full_path(state_cls: type[BaseState]) -> str: def collect_all_states( - root_state: type[BaseState], + root_state: type[BaseState] | None = None, ) -> list[type[BaseState]]: - """Recursively collect all state classes starting from root. + """Collect state classes in deterministic depth-first order. + + Without ``root_state``, walks every state registered in the active + :class:`~reflex_base.registry.RegistrationContext` (each connected tree + starting from a parentless root), sorting siblings alphabetically by + class name. With ``root_state``, the walk is restricted to that subtree. + + The CLI commands use the parameterless form so the result reflects the + user's currently-loaded app. Tests typically pass an explicit root to + scope the walk to the classes defined inside the test. Args: - root_state: The root state class to start from. + root_state: Optional subtree root. ``None`` means "every registered + state". Returns: - List of all state classes in depth-first order. + List of state classes in depth-first, sibling-sorted order. """ - result = [root_state] - for substate in sorted(root_state.get_substates(), key=lambda s: s.__name__): - result.extend(collect_all_states(substate)) - return result + if root_state is not None: + result = [root_state] + for substate in sorted(root_state.get_substates(), key=lambda s: s.__name__): + result.extend(collect_all_states(substate)) + return result + + from reflex_base.registry import RegistrationContext + + ctx = RegistrationContext.get() + roots = sorted( + (cls for cls in ctx.base_states.values() if cls.get_parent_state() is None), + key=lambda s: s.__name__, + ) + out: list[type[BaseState]] = [] + for root in roots: + out.extend(collect_all_states(root)) + return out -def generate_minify_config(root_state: type[BaseState]) -> MinifyConfig: - """Generate a complete minify configuration for all states and events. +def generate_minify_config( + root_state: type[BaseState] | None = None, +) -> MinifyConfig: + """Generate a complete minify configuration. - Assigns minified names starting from 'a' for each scope (siblings get unique names), - sorted alphabetically by name for determinism. + Walks the state tree (see :func:`collect_all_states`) and assigns minified + names starting from ``"a"`` per sibling group. Siblings are sorted by + class name and handlers by handler name so the output is byte-stable across + runs (and therefore VCS-friendly). Args: - root_state: The root state class. + root_state: Optional subtree root. ``None`` (the default) generates a + config for every state registered in the active context. Returns: - A complete MinifyConfig. + A complete :class:`MinifyConfig`. """ states: dict[str, str] = {} events: dict[str, dict[str, str]] = {} + sibling_counter: dict[type[BaseState] | None, int] = {} - def process_state( - state_cls: type[BaseState], - sibling_counter: dict[type[BaseState] | None, int], - ) -> None: - """Process a state and its children recursively. - - Args: - state_cls: The state class to process. - sibling_counter: Counter for assigning sibling-unique IDs. - """ + for state_cls in collect_all_states(root_state): parent = state_cls.get_parent_state() - - # Assign state ID (unique among siblings) - if parent not in sibling_counter: - sibling_counter[parent] = 0 + sibling_counter.setdefault(parent, 0) state_id = sibling_counter[parent] sibling_counter[parent] += 1 - # Store state minified name state_path = get_state_full_path(state_cls) states[state_path] = int_to_minified_name(state_id) - # Assign event IDs for this state's handlers (sorted alphabetically) handler_names = sorted(state_cls.event_handlers.keys()) - state_events: dict[str, str] = {} - for event_id, handler_name in enumerate(handler_names): - state_events[handler_name] = int_to_minified_name(event_id) - if state_events: - events[state_path] = state_events - - # Process children (sorted alphabetically) - children = sorted(state_cls.get_substates(), key=lambda s: s.__name__) - for child in children: - process_state(child, sibling_counter) - - # Start processing from root - sibling_counter: dict[type[BaseState] | None, int] = {} - process_state(root_state, sibling_counter) + if handler_names: + events[state_path] = { + handler_name: int_to_minified_name(event_id) + for event_id, handler_name in enumerate(handler_names) + } return MinifyConfig( version=SCHEMA_VERSION, @@ -382,19 +511,21 @@ def process_state( def validate_minify_config( config: MinifyConfig, - root_state: type[BaseState], + root_state: type[BaseState] | None = None, ) -> tuple[list[str], list[str], list[str]]: """Validate a minify configuration against the current state tree. Args: config: The configuration to validate. - root_state: The root state class. + root_state: Optional subtree root. ``None`` validates against every + state in the active context. Returns: - A tuple of (errors, warnings, missing_entries): - - errors: Critical issues (duplicate IDs, etc.) - - warnings: Non-critical issues (orphaned entries) - - missing_entries: States/events in code but not in config + A tuple ``(errors, warnings, missing_entries)``: + + * ``errors`` — critical issues (duplicate IDs, etc.) + * ``warnings`` — non-critical issues (orphaned entries) + * ``missing_entries`` — states/events in code but not in the config """ errors: list[str] = [] @@ -480,7 +611,7 @@ def validate_minify_config( def sync_minify_config( existing_config: MinifyConfig, - root_state: type[BaseState], + root_state: type[BaseState] | None = None, reassign_deleted: bool = False, prune: bool = False, ) -> MinifyConfig: @@ -488,7 +619,8 @@ def sync_minify_config( Args: existing_config: The existing configuration to update. - root_state: The root state class. + root_state: Optional subtree root. ``None`` syncs against every state + in the active context. reassign_deleted: If True, reassign IDs that are no longer in use. prune: If True, remove entries for states/events that no longer exist. diff --git a/reflex/reflex.py b/reflex/reflex.py index 54b06d75f58..343f623e94c 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -919,7 +919,6 @@ def minify_init(): generate_minify_config, save_minify_config, ) - from reflex.state import State from reflex.utils import prerequisites path = _get_minify_json_path() @@ -930,11 +929,11 @@ def minify_init(): ) raise SystemExit(1) - # Load the user's app to register all state classes + # Load the user's app so every State subclass registers itself. prerequisites.get_app() - # Generate the configuration - config = generate_minify_config(State) + # Generate the configuration over every registered state. + config = generate_minify_config() save_minify_config(config) num_states = len(config["states"]) @@ -968,7 +967,6 @@ def minify_sync(reassign_deleted: bool, prune: bool): save_minify_config, sync_minify_config, ) - from reflex.state import State from reflex.utils import prerequisites path = _get_minify_json_path() @@ -978,7 +976,7 @@ def minify_sync(reassign_deleted: bool, prune: bool): ) raise SystemExit(1) - # Load the user's app to register all state classes + # Load the user's app so every State subclass registers itself. prerequisites.get_app() # Load existing config @@ -990,9 +988,9 @@ def minify_sync(reassign_deleted: bool, prune: bool): old_states = len(existing_config["states"]) old_events = sum(len(handlers) for handlers in existing_config["events"].values()) - # Sync the configuration + # Sync against every registered state. new_config = sync_minify_config( - existing_config, State, reassign_deleted=reassign_deleted, prune=prune + existing_config, reassign_deleted=reassign_deleted, prune=prune ) save_minify_config(new_config) @@ -1017,7 +1015,6 @@ def minify_validate(): _load_minify_config_uncached, validate_minify_config, ) - from reflex.state import State from reflex.utils import prerequisites path = _get_minify_json_path() @@ -1037,7 +1034,7 @@ def minify_validate(): raise SystemExit(1) # Validate - errors, warnings, missing = validate_minify_config(config, State) + errors, warnings, missing = validate_minify_config(config) if errors: console.error("Errors found:") @@ -1050,11 +1047,11 @@ def minify_validate(): console.warn(f" - {warning}") if missing: - console.info("Missing entries (in code but not in config):") + console.warn("Missing entries (in code but not in config):") for entry in missing: console.warn(f" - {entry}") - if errors: + if errors or missing: raise SystemExit(1) console.log(f"{MINIFY_JSON} is valid and up-to-date.") diff --git a/reflex/state.py b/reflex/state.py index 0d13fa92bdc..057ae06dd6b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -357,11 +357,6 @@ class BaseState(EvenMoreBasicBaseState): # Set of states which might need to be recomputed if vars in this state change. _potentially_dirty_states: ClassVar[set[str]] = set() - # Per-class registry mapping event_id -> event handler name for minification. - # Populated from minify.json at class creation time. - # Maps minified event ID (e.g., "a") to original handler name (e.g., "increment"). - _event_id_to_name: ClassVar[builtins.dict[str, str]] = {} - # The parent state. parent_state: BaseState | None = field(default=None, is_var=False) @@ -575,7 +570,6 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): **cls.computed_vars, } cls.event_handlers = {} - cls._event_id_to_name = {} # Setup the base vars at the class level. for name, prop in cls.base_vars.items(): @@ -618,12 +612,6 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): cls.event_handlers[name] = handler setattr(cls, name, handler) - # Register user-defined event handlers for minification - # (must happen before register_base_state so format_event_handler returns - # the minified name when registering with the context). - for handler_name in events: - cls._register_event_handler_for_minify(handler_name) - RegistrationContext.register_base_state(cls) # Initialize per-class var dependency tracking. @@ -650,34 +638,8 @@ def _add_event_handler( handler = cls._create_event_handler(fn) cls.event_handlers[name] = handler setattr(cls, name, handler) - cls._register_event_handler_for_minify(name) return handler - @classmethod - def _register_event_handler_for_minify(cls, handler_name: str) -> None: - """Register an event handler for minification if applicable. - - Called when an event handler is added to event_handlers dict. - Updates _event_id_to_name if minification is enabled and the handler - has a minified ID in the config. - - Args: - handler_name: The original name of the event handler. - """ - from reflex.minify import ( - get_event_id, - get_state_full_path, - is_event_minify_enabled, - ) - - if not is_event_minify_enabled(): - return - - state_path = get_state_full_path(cls) - event_id = get_event_id(state_path, handler_name) - if event_id is not None: - cls._event_id_to_name[event_id] = handler_name - @staticmethod def _copy_fn(fn: Callable) -> Callable: """Copy a function. Used to copy ComputedVars and EventHandlers from mixins. @@ -1003,28 +965,23 @@ def get_substates(cls) -> set[type[BaseState]]: @classmethod @functools.lru_cache def get_name(cls) -> str: - """Get the name of the state. + """Get the user-visible name of the state. + + Delegates to the active :class:`~reflex_base.registry.NameResolver` via + :meth:`~reflex_base.registry.RegistrationContext.get_state_name` so that + e.g. ``minify.json`` (or any other resolver a user installs) can rewrite + the name. Falls back to the built-in snake-cased ``module___ClassName`` + form when no registration context is active. Returns: - The name of the state (minified if configured in minify.json). + The resolved name of the state. """ - from reflex.minify import ( - get_state_full_path, - get_state_id, - is_state_minify_enabled, - ) - - module = cls.__module__.replace(".", "___") - full_name = format.to_snake_case(f"{module}___{cls.__name__}") - - # If state minification is enabled, look up the state ID from minify.json - if is_state_minify_enabled(): - state_path = get_state_full_path(cls) - state_id = get_state_id(state_path) - if state_id is not None: - return state_id + from reflex_base.registry import RegistrationContext - return full_name + try: + return RegistrationContext.get().get_state_name(cls) + except LookupError: + return RegistrationContext.default_state_name(cls) @classmethod @functools.lru_cache @@ -1049,10 +1006,12 @@ def get_class_substate( Args: path: The path to the substate. - _skip_self: If True, strip the leading segment when it matches this - state's name. Only the initial (root) call should use True; - recursive calls pass False so that a child whose minified name - collides with its parent is resolved correctly. + _skip_self: Internal recursion flag. External callers must leave + this at the default ``True`` — only the root call should strip + a leading segment that matches ``cls.get_name()``. Recursive + calls pass ``False`` so that a child whose minified name + collides with its parent (e.g. ``"a.b.b"``) resolves to the + child rather than terminating early at the parent. Returns: The class substate. @@ -1220,7 +1179,6 @@ def _create_setvar(cls): cls.setvar = cls.event_handlers[constants.event.SETVAR] = EventHandlerSetVar( state_cls=cls ) - cls._register_event_handler_for_minify(constants.event.SETVAR) @classmethod def _create_setter(cls, name: str, prop: Var): @@ -1239,7 +1197,6 @@ def _create_setter(cls, name: str, prop: Var): ) cls.event_handlers[setter_name] = event_handler setattr(cls, setter_name, event_handler) - cls._register_event_handler_for_minify(setter_name) @classmethod def _set_default_value(cls, name: str, prop: Var): @@ -1584,10 +1541,12 @@ def get_substate(self, path: Sequence[str], _skip_self: bool = True) -> BaseStat Args: path: The path to the substate. - _skip_self: If True, strip the leading segment when it matches this - state's name. Only the initial (root) call should use True; - recursive calls pass False so that a child whose minified name - collides with its parent is resolved correctly. + _skip_self: Internal recursion flag. External callers must leave + this at the default ``True`` — only the root call should strip + a leading segment that matches ``self.get_name()``. Recursive + calls pass ``False`` so that a child whose minified name + collides with its parent (e.g. ``"a.b.b"``) resolves to the + child rather than terminating early at the parent. Returns: The substate. diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 6362c4af753..3f54c0e389b 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -5,12 +5,14 @@ import json import pytest +from reflex_base.registry import DefaultNameResolver, NameResolver, RegistrationContext from reflex.environment import MinifyMode, environment from reflex.minify import ( MINIFY_JSON, SCHEMA_VERSION, MinifyConfig, + MinifyNameResolver, clear_config_cache, generate_minify_config, get_event_id, @@ -28,6 +30,21 @@ from reflex.state import BaseState, State +def _resolved_event_id(state_cls: type[BaseState], handler_name: str) -> str | None: + """Ask the active resolver for the minified name of a handler. + + Args: + state_cls: The state class the handler is attached to. + handler_name: The handler's original (Python) name. + + Returns: + The minified id, or ``None`` if the resolver doesn't override it. + """ + return RegistrationContext.get().name_resolver.resolve_handler_name( + state_cls, handler_name + ) + + class TestIntToMinifiedName: """Tests for int_to_minified_name function.""" @@ -112,21 +129,24 @@ class TestState(BaseState): def temp_minify_json(tmp_path, monkeypatch): """Create a temporary directory and mock cwd to use it for minify.json. + ``clear_config_cache()`` walks the registration context and clears + ``get_name``/``get_full_name``/``get_class_substate`` LRU caches on every + registered State class, then re-keys the registry, so we don't have to + remember every State subclass that earlier tests may have registered. + + The teardown undoes monkeypatch *before* clearing the cache so that the + refresh is based on the unmodified env/cwd; otherwise the registry would + be left pinned to whatever minified names the test set up, breaking + subsequent tests that don't use this fixture. + Yields: The temporary directory path. """ monkeypatch.chdir(tmp_path) clear_config_cache() - # Clear State caches to ensure clean slate - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() yield tmp_path - # Clean up: clear config and all cached state names + monkeypatch.undo() clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() class TestMinifyConfig: @@ -535,8 +555,8 @@ def my_handler(self): f"Expected path {expected_state_path}, got {actual_path}" ) - # The state's _event_id_to_name should be populated (key is minified name) - assert TestStateWithMinifiedEvent._event_id_to_name == {"d": "my_handler"} + # The active resolver should map this handler to its minified name. + assert _resolved_event_id(TestStateWithMinifiedEvent, "my_handler") == "d" handler = TestStateWithMinifiedEvent.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) @@ -581,8 +601,10 @@ class TestStateWithMinifiedEventDisabled(State): def my_handler(self): pass - # The state's _event_id_to_name should be empty when env var is disabled - assert TestStateWithMinifiedEventDisabled._event_id_to_name == {} + # When env var is disabled, the resolver yields no minified name. + assert ( + _resolved_event_id(TestStateWithMinifiedEventDisabled, "my_handler") is None + ) handler = TestStateWithMinifiedEventDisabled.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) @@ -595,7 +617,7 @@ class TestDynamicHandlerMinification: """Tests for dynamic event handler minification (setvar, auto-setters).""" def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): - """Test that setvar is registered in _event_id_to_name when config exists.""" + """Test that ``setvar`` is resolvable to its minified name.""" monkeypatch.setenv( environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value ) @@ -621,12 +643,11 @@ def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): class TestStateWithSetvar(State): pass - # Verify setvar is registered for minification - assert "s" in TestStateWithSetvar._event_id_to_name - assert TestStateWithSetvar._event_id_to_name["s"] == "setvar" + # The active resolver should report setvar's minified id. + assert _resolved_event_id(TestStateWithSetvar, "setvar") == "s" def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): - """Test that auto-setters (set_*) are registered in _event_id_to_name when config exists.""" + """Test that auto-setters (set_*) are resolvable to their minified name.""" from reflex_base import config as base_config monkeypatch.setenv( @@ -664,19 +685,19 @@ def _mock_get_config(*args, **kwargs): class TestStateWithAutoSetter(State): count: int = 0 - # Verify auto-setter is registered for minification - assert "c" in TestStateWithAutoSetter._event_id_to_name - assert TestStateWithAutoSetter._event_id_to_name["c"] == "set_count" + # The auto-setter should resolve to the minified id from the config. + assert _resolved_event_id(TestStateWithAutoSetter, "set_count") == "c" def test_dynamic_handlers_not_registered_without_config(self, temp_minify_json): - """Test that dynamic handlers are NOT registered when no config exists.""" + """Test that dynamic handlers have no resolved minified name without config.""" # No config saved - temp_minify_json fixture ensures clean state class TestStateNoConfig(State): count: int = 0 - # Without config, _event_id_to_name should be empty - assert TestStateNoConfig._event_id_to_name == {} + # Without config, the resolver returns None for every handler. + for handler_name in TestStateNoConfig.event_handlers: + assert _resolved_event_id(TestStateNoConfig, handler_name) is None def test_add_event_handler_registered_with_config( self, temp_minify_json, monkeypatch @@ -718,9 +739,9 @@ def dynamic_handler(self): "dynamic_handler", dynamic_handler ) - # Verify dynamic handler is registered for minification - assert "d" in TestStateWithDynamicHandler._event_id_to_name - assert TestStateWithDynamicHandler._event_id_to_name["d"] == "dynamic_handler" + # Dynamically-added handlers are resolved through the same resolver, + # so the new name picks up the minified id without re-registering. + assert _resolved_event_id(TestStateWithDynamicHandler, "dynamic_handler") == "d" class TestMinifyModeEnvVars: @@ -868,19 +889,19 @@ def test_get_class_substate_with_parent_child_name_collision( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) - # Build a hierarchy: State -> ParentClassSubstateCollision -> ChildClassSubstateCollision - # where both child classes get minified name "b". - # The substate registry is keyed by parent.get_full_name() at - # registration time, so the minify config must be loaded *before* - # the test classes are defined. Class names are deliberately unique - # so `_handle_local_def` does not append a numeric suffix when run - # alongside other tests that also define `ParentState`. - expected_module = "tests.units.test_minification" - parent_path = f"{expected_module}.State.ParentClassSubstateCollision" - child_path = ( - f"{expected_module}.State.ParentClassSubstateCollision." - "ChildClassSubstateCollision" - ) + # Build a hierarchy: State -> ParentClassSubstateCollision -> + # ChildClassSubstateCollision where both children minify to "b". + # Class names are deliberately unique so ``_handle_local_def`` does + # not append a numeric suffix when run alongside other tests that + # might also define a ``ParentState``. + class ParentClassSubstateCollision(State): + pass + + class ChildClassSubstateCollision(ParentClassSubstateCollision): + pass + + parent_path = get_state_full_path(ParentClassSubstateCollision) + child_path = get_state_full_path(ChildClassSubstateCollision) config: MinifyConfig = { "version": SCHEMA_VERSION, @@ -892,30 +913,20 @@ def test_get_class_substate_with_parent_child_name_collision( "events": {}, } save_minify_config(config) + # ``clear_config_cache`` resets the per-class name caches and re-keys + # the registry under the new minified names. clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() - - class ParentClassSubstateCollision(State): - pass - - class ChildClassSubstateCollision(ParentClassSubstateCollision): - pass - - ParentState = ParentClassSubstateCollision - ChildState = ChildClassSubstateCollision # Verify both get the same minified name - assert ParentState.get_name() == "b" - assert ChildState.get_name() == "b" + assert ParentClassSubstateCollision.get_name() == "b" + assert ChildClassSubstateCollision.get_name() == "b" # Full path should be a.b.b - assert ChildState.get_full_name() == "a.b.b" + assert ChildClassSubstateCollision.get_full_name() == "a.b.b" - # get_class_substate should resolve a.b.b to ChildState, not ParentState + # get_class_substate should resolve a.b.b to the child, not the parent. resolved = State.get_class_substate("a.b.b") - assert resolved is ChildState + assert resolved is ChildClassSubstateCollision def test_get_substate_with_parent_child_name_collision( self, temp_minify_json, monkeypatch @@ -929,17 +940,19 @@ def test_get_substate_with_parent_child_name_collision( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) - # Substates are registered under parent.get_full_name() at class - # creation time, so the minify config must be in place before the - # test classes are defined. Class names are deliberately unique so - # `_handle_local_def` does not append a numeric suffix when run - # alongside other tests that also define `ParentState2`. - expected_module = "tests.units.test_minification" - parent_path = f"{expected_module}.State.ParentInstanceSubstateCollision" - child_path = ( - f"{expected_module}.State.ParentInstanceSubstateCollision." - "ChildInstanceSubstateCollision" - ) + # Class names are deliberately unique so ``_handle_local_def`` does + # not append a numeric suffix when run alongside other tests that + # might also define a ``ParentState2``. + class ParentInstanceSubstateCollision(State): + pass + + class ChildInstanceSubstateCollision(ParentInstanceSubstateCollision): + @rx.event + def my_handler(self): + pass + + parent_path = get_state_full_path(ParentInstanceSubstateCollision) + child_path = get_state_full_path(ChildInstanceSubstateCollision) config: MinifyConfig = { "version": SCHEMA_VERSION, @@ -951,27 +964,16 @@ def test_get_substate_with_parent_child_name_collision( "events": {}, } save_minify_config(config) + # ``clear_config_cache`` resets the per-class name caches and re-keys + # the registry under the new minified names. clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() - - class ParentInstanceSubstateCollision(State): - pass - - class ChildInstanceSubstateCollision(ParentInstanceSubstateCollision): - @rx.event - def my_handler(self): - pass - - ChildState2 = ChildInstanceSubstateCollision # Create a state instance tree root = State(_reflex_internal_init=True) # type: ignore[call-arg] - # Instance get_substate should resolve a.b.b to ChildState2 + # Instance get_substate should resolve a.b.b to ChildInstanceSubstateCollision resolved = root.get_substate(["a", "b", "b"]) - assert type(resolved) is ChildState2 + assert type(resolved) is ChildInstanceSubstateCollision class TestMinifyLookupCLI: @@ -1117,3 +1119,219 @@ class JsonTestState(State): assert output_data[0]["class"] == "State" assert output_data[1]["class"] == "JsonTestState" assert output_data[1]["state_id"] == "b" + + +class TestNameResolverProtocol: + """Tests for the pluggable :class:`NameResolver` protocol. + + These exercise the contract independent of minification: any resolver that + implements ``resolve_state_name`` / ``resolve_handler_name`` can be slotted + into the registration context to rewrite names. + """ + + def test_default_resolver_returns_none(self): + """The default resolver yields no overrides.""" + resolver = DefaultNameResolver() + assert resolver.resolve_state_name(State) is None + assert resolver.resolve_handler_name(State, "any_handler") is None + + def test_default_resolver_satisfies_protocol(self): + """``DefaultNameResolver`` is a structural :class:`NameResolver`.""" + assert isinstance(DefaultNameResolver(), NameResolver) + + def test_minify_resolver_satisfies_protocol(self): + """``MinifyNameResolver`` is a structural :class:`NameResolver`.""" + resolver = MinifyNameResolver( + config=None, states_enabled=False, events_enabled=False + ) + assert isinstance(resolver, NameResolver) + + def test_get_state_name_falls_back_to_default(self): + """``RegistrationContext.get_state_name`` returns the built-in name when + the resolver returns None (the default). + """ + ctx = RegistrationContext.get() + assert ctx.get_state_name(State) == RegistrationContext.default_state_name( + State + ) + + def test_get_handler_name_falls_back_to_default(self): + """``RegistrationContext.get_handler_name`` returns the input name when + the resolver returns None (the default). + """ + ctx = RegistrationContext.get() + assert ctx.get_handler_name(State, "some_handler") == "some_handler" + + def test_set_name_resolver_propagates_through_get_name(self, monkeypatch): + """Installing a custom resolver swaps ``BaseState.get_name`` output. + + The resolver overrides only ``State`` so other registered classes + retain their default names — without this scoping, every class would + collapse to one entry in the registry's name-keyed dicts. + """ + + class FixedStateNameResolver: + def resolve_state_name(self, state_cls): + return "fixed_name" if state_cls is State else None + + def resolve_handler_name(self, state_cls, handler_name): + return None + + ctx = RegistrationContext.get() + original = ctx.name_resolver + try: + ctx.set_name_resolver(FixedStateNameResolver()) + assert State.get_name() == "fixed_name" + finally: + ctx.set_name_resolver(original) + + def test_set_name_resolver_propagates_through_format_event_handler(self): + """Installing a custom resolver swaps the formatted handler name.""" + from reflex.state import OnLoadInternalState + from reflex.utils.format import format_event_handler + + class HandlerPrefixResolver: + def resolve_state_name(self, state_cls): + return None + + def resolve_handler_name(self, state_cls, handler_name): + return f"px_{handler_name}" + + ctx = RegistrationContext.get() + original = ctx.name_resolver + try: + ctx.set_name_resolver(HandlerPrefixResolver()) + formatted = format_event_handler(OnLoadInternalState.on_load_internal) # pyright: ignore[reportArgumentType] + assert formatted.endswith(".px_on_load_internal") + finally: + ctx.set_name_resolver(original) + + def test_resolver_swap_clears_lru_caches(self): + """``set_name_resolver`` invalidates per-class name caches so that + ``get_full_name`` reflects the new resolver immediately. + """ + + class A: + def resolve_state_name(self, state_cls): + return "first" if state_cls is State else None + + def resolve_handler_name(self, state_cls, handler_name): + return None + + class B: + def resolve_state_name(self, state_cls): + return "second" if state_cls is State else None + + def resolve_handler_name(self, state_cls, handler_name): + return None + + ctx = RegistrationContext.get() + original = ctx.name_resolver + try: + ctx.set_name_resolver(A()) + assert State.get_full_name() == "first" + ctx.set_name_resolver(B()) + assert State.get_full_name() == "second" + finally: + ctx.set_name_resolver(original) + + def test_chain_of_resolvers(self): + """Resolvers compose with a tiny user-written chain wrapper.""" + + class Chain: + """Returns the first non-None override from the wrapped resolvers.""" + + def __init__(self, *resolvers): + self.resolvers = resolvers + + def resolve_state_name(self, state_cls): + for r in self.resolvers: + v = r.resolve_state_name(state_cls) + if v is not None: + return v + return None + + def resolve_handler_name(self, state_cls, handler_name): + for r in self.resolvers: + v = r.resolve_handler_name(state_cls, handler_name) + if v is not None: + return v + return None + + class FirstOnly: + def resolve_state_name(self, state_cls): + return "from_first" if state_cls is State else None + + def resolve_handler_name(self, state_cls, handler_name): + return None + + ctx = RegistrationContext.get() + original = ctx.name_resolver + try: + ctx.set_name_resolver(Chain(FirstOnly(), DefaultNameResolver())) + assert State.get_name() == "from_first" + finally: + ctx.set_name_resolver(original) + + +class TestMinifyNameResolver: + """Tests for :class:`MinifyNameResolver` itself (config + caching).""" + + def test_disabled_returns_none(self, temp_minify_json): + """When neither flag is enabled, the resolver returns None for all.""" + resolver = MinifyNameResolver( + config={"version": SCHEMA_VERSION, "states": {}, "events": {}}, + states_enabled=False, + events_enabled=False, + ) + assert resolver.resolve_state_name(State) is None + assert resolver.resolve_handler_name(State, "any") is None + + def test_no_config_returns_none(self): + """No config means no overrides even when flags are enabled.""" + resolver = MinifyNameResolver( + config=None, states_enabled=True, events_enabled=True + ) + assert resolver.resolve_state_name(State) is None + assert resolver.resolve_handler_name(State, "any") is None + + def test_state_lookup_caches(self): + """Resolved state names are memoized after the first lookup.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {get_state_full_path(State): "rs"}, + "events": {}, + } + resolver = MinifyNameResolver( + config=config, states_enabled=True, events_enabled=False + ) + assert resolver.resolve_state_name(State) == "rs" + # second call hits the cache + assert State in resolver._state_cache + assert resolver.resolve_state_name(State) == "rs" + + def test_event_lookup_caches(self): + """Resolved handler names are memoized per state class.""" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {}, + "events": {get_state_full_path(State): {"foo": "f", "bar": "b"}}, + } + resolver = MinifyNameResolver( + config=config, states_enabled=False, events_enabled=True + ) + assert resolver.resolve_handler_name(State, "foo") == "f" + assert resolver.resolve_handler_name(State, "bar") == "b" + assert resolver.resolve_handler_name(State, "missing") is None + assert State in resolver._event_cache + + def test_from_disk_handles_malformed_config(self, temp_minify_json): + """``from_disk`` returns a usable resolver even when minify.json is bad.""" + path = temp_minify_json / MINIFY_JSON + with path.open("w") as f: + f.write("{not valid json") + resolver = MinifyNameResolver.from_disk() + # config falls back to None — every lookup returns None. + assert resolver.config is None + assert resolver.resolve_state_name(State) is None + assert resolver.resolve_handler_name(State, "any") is None From bd6321037773ac471ca6abd35fcc20b028c6ac33 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 22:40:25 +0200 Subject: [PATCH 57/74] pre-commit --- docs/app/agent_files/_plugin.py | 12 +- .../reflex_docs/pages/docs/cloud_cliref.py | 135 ++++++++---------- docs/app/reflex_docs/pages/docs/component.py | 40 +++--- docs/app/reflex_docs/pages/docs/source.py | 14 +- .../pages/docs_landing/views/ai_builder.py | 18 ++- .../pages/integrations/integration_list.py | 18 ++- docs/app/tests/conftest.py | 16 +-- 7 files changed, 115 insertions(+), 138 deletions(-) diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index 5e07e9b97cd..487e777976f 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -9,13 +9,11 @@ def generate_markdown_files() -> tuple[tuple[Path, str | bytes], ...]: from reflex_docs.pages.docs import doc_markdown_sources - return tuple( - [ - (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) - for route, source_path in doc_markdown_sources.items() - if (resolved := Path(source_path)).is_file() - ] - ) + return tuple([ + (PosixPath(route.strip("/") + ".md"), resolved.read_bytes()) + for route, source_path in doc_markdown_sources.items() + if (resolved := Path(source_path)).is_file() + ]) def generate_llms_txt( diff --git a/docs/app/reflex_docs/pages/docs/cloud_cliref.py b/docs/app/reflex_docs/pages/docs/cloud_cliref.py index 428f8eafeae..b8051fba339 100644 --- a/docs/app/reflex_docs/pages/docs/cloud_cliref.py +++ b/docs/app/reflex_docs/pages/docs/cloud_cliref.py @@ -177,83 +177,74 @@ def process( """Convert a Click command to a Markdown element.""" actual_name = override_name or command["name"] full_name = prefix + " " + actual_name if prefix and actual_name else actual_name - cli_to_doc[full_name] = Section( - ( - Paragraph(InlineText(command["help"])) if command["help"] else Empty(), - Section( - ( - Header(3, InlineText("Usage")), - CodeBlock( - "$" - + (" " + prefix.strip() if prefix else "") - + " " - + actual_name.strip() - + ( - " [OPTIONS]" - if command["params"] - and any( - param.get("param_type_name") != "argument" - for param in command["params"] - ) - else "" - ) - + ( - " " + " ".join(arguments) - if ( - arguments := [ - param["name"].upper() - for param in command["params"] - if param.get("param_type_name") == "argument" - and param["name"] - ] - ) - else "" - ), - language="console", - ), + cli_to_doc[full_name] = Section(( + Paragraph(InlineText(command["help"])) if command["help"] else Empty(), + Section(( + Header(3, InlineText("Usage")), + CodeBlock( + "$" + + (" " + prefix.strip() if prefix else "") + + " " + + actual_name.strip() + + ( + " [OPTIONS]" + if command["params"] + and any( + param.get("param_type_name") != "argument" + for param in command["params"] + ) + else "" ) - ) - if actual_name - else Empty(), - Section( - ( - Header(3, InlineText("Options")), - List( - tuple( - InlineTextCollection( + + ( + " " + " ".join(arguments) + if ( + arguments := [ + param["name"].upper() + for param in command["params"] + if param.get("param_type_name") == "argument" + and param["name"] + ] + ) + else "" + ), + language="console", + ), + )) + if actual_name + else Empty(), + Section(( + Header(3, InlineText("Options")), + List( + tuple( + InlineTextCollection(( + InlineCode( + ", ".join(param["opts"]) + + ( + " / " + ", ".join(param["secondary_opts"]) + if param["secondary_opts"] + else "" + ) + + ( ( - InlineCode( - ", ".join(param["opts"]) - + ( - " / " + ", ".join(param["secondary_opts"]) - if param["secondary_opts"] - else "" - ) - + ( - ( - " " + param["type"]["name"].upper() - if param["type"]["name"] != "boolean" - else "" - ) - if (choices := param["type"].get("choices")) - is None - else " [" + "|".join(choices) + "]" - ) - ), - InlineText(": " + option_help), + " " + param["type"]["name"].upper() + if param["type"]["name"] != "boolean" + else "" ) + if (choices := param["type"].get("choices")) is None + else " [" + "|".join(choices) + "]" ) - for param in command["params"] - if (option_help := param.get("help")) is not None ), - ordered=False, - ), - ) - ) - if command["params"] - else Empty(), - ) - ).into_text() + InlineText(": " + option_help), + )) + for param in command["params"] + if (option_help := param.get("help")) is not None + ), + ordered=False, + ), + )) + if command["params"] + else Empty(), + )).into_text() for name, sub_command in sort_subcommands(command.get("commands", {})).items(): process( sub_command, diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 9aa31d8d8d8..77be602c2a8 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -140,12 +140,10 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="w-32 font-small text-slate-11"), rx.select.content( - rx.select.group( - *[ - rx.select.item(item, value=item, class_name="font-small") - for item in literal_values - ] - ) + rx.select.group(*[ + rx.select.item(item, value=item, class_name="font-small") + for item in literal_values + ]) ), value=var, on_change=setter, @@ -204,22 +202,20 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict return rx.select.root( rx.select.trigger(class_name="font-small w-32 text-slate-11"), rx.select.content( - rx.select.group( - *[ - rx.select.item( - item, - value=item, - class_name="font-small", - _hover=( - {"background": f"var(--{item}-9)"} - if prop.name == "color_scheme" - else None - ), - ) - for item in list(map(str, type_.__args__)) - if item != "" - ] - ), + rx.select.group(*[ + rx.select.item( + item, + value=item, + class_name="font-small", + _hover=( + {"background": f"var(--{item}-9)"} + if prop.name == "color_scheme" + else None + ), + ) + for item in list(map(str, type_.__args__)) + if item != "" + ]), ), value=var, on_change=setter, diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 74c18cc63b0..3ceadda4036 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -28,14 +28,12 @@ def format_fields( rx.scroll_area( rx.table.root( rx.table.header( - rx.table.row( - *[ - rx.table.column_header_cell( - header, class_name=table_header_class_name - ) - for header in headers - ] - ) + rx.table.row(*[ + rx.table.column_header_cell( + header, class_name=table_header_class_name + ) + for header in headers + ]) ), rx.table.body( *[ diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index a2491e8e1fc..788913d2e3f 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -51,17 +51,15 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append( - { - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, - } + result.append({ + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, } - ) + }) return result diff --git a/docs/app/reflex_docs/pages/integrations/integration_list.py b/docs/app/reflex_docs/pages/integrations/integration_list.py index e6f1a90826d..f95c17403c4 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_list.py +++ b/docs/app/reflex_docs/pages/integrations/integration_list.py @@ -46,16 +46,14 @@ def get_integration_path() -> list: if title == "Open Ai": title = "Open AI" - result.append( - { - key: { - "path": f"{web_path_prefix}/{slug}", - "tags": tag, - "description": description, - "name": key, - "title": title, - } + result.append({ + key: { + "path": f"{web_path_prefix}/{slug}", + "tags": tag, + "description": description, + "name": key, + "title": title, } - ) + }) return result diff --git a/docs/app/tests/conftest.py b/docs/app/tests/conftest.py index b0b941b347b..d1b9a7e77ef 100644 --- a/docs/app/tests/conftest.py +++ b/docs/app/tests/conftest.py @@ -13,15 +13,13 @@ def reflex_web_app(): app_root = Path(__file__).parent.parent from reflex_docs.whitelist import WHITELISTED_PAGES - WHITELISTED_PAGES.extend( - [ - "/events", - "/vars", - "/getting-started", - "/library/graphing", - "/api-reference/special-events", - ] - ) + WHITELISTED_PAGES.extend([ + "/events", + "/vars", + "/getting-started", + "/library/graphing", + "/api-reference/special-events", + ]) with AppHarness.create(root=app_root) as harness: yield harness From 5ad6e3e511a38837492d8e2db0735edd4f1df619 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 25 Apr 2026 22:56:55 +0200 Subject: [PATCH 58/74] add support for dynamic states in minification --- reflex/reflex.py | 88 +++++++++++------------ reflex/utils/prerequisites.py | 6 ++ tests/units/test_minification.py | 116 ++++++++++++++++++++++++++++--- 3 files changed, 158 insertions(+), 52 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 343f623e94c..f80f6f2135e 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -19,6 +19,8 @@ from reflex_base.constants.base import LITERAL_ENV from reflex_cli.constants.base import LogLevel as HostingLogLevel + from reflex.minify import MinifyConfig + def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None): """Set the log level. @@ -905,41 +907,55 @@ def minify(): """Manage state and event name minification.""" +def _load_app_for_minify() -> None: + """Compile the app so dynamic states are registered. + + ``ComponentState.create()`` and locally-defined states inside page + functions only register during page evaluation, so a plain ``get_app()`` + would miss them. + """ + from reflex.utils import prerequisites + + prerequisites.get_compiled_app(dry_run=True) + + +def _count_events(config: MinifyConfig) -> int: + """Sum the event handlers across every state in a minify config. + + Args: + config: The minify configuration to count events in. + + Returns: + The total number of event handlers across every state. + """ + return sum(len(handlers) for handlers in config["events"].values()) + + @minify.command(name="init") @loglevel_option def minify_init(): - """Initialize minify.json with IDs for all states and events. - - This command scans the codebase and generates a minify.json file - with unique IDs for all states and event handlers. - """ + """Initialize minify.json with IDs for all states and events.""" from reflex.minify import ( MINIFY_JSON, _get_minify_json_path, generate_minify_config, save_minify_config, ) - from reflex.utils import prerequisites - path = _get_minify_json_path() - if path.exists(): + if _get_minify_json_path().exists(): console.error( f"{MINIFY_JSON} already exists. Use 'reflex minify sync' to update " "or delete the file to reinitialize." ) raise SystemExit(1) - # Load the user's app so every State subclass registers itself. - prerequisites.get_app() - - # Generate the configuration over every registered state. + _load_app_for_minify() config = generate_minify_config() save_minify_config(config) - num_states = len(config["states"]) - num_events = sum(len(handlers) for handlers in config["events"].values()) console.log( - f"Created {MINIFY_JSON} with {num_states} states and {num_events} events." + f"Created {MINIFY_JSON} with {len(config['states'])} states " + f"and {_count_events(config)} events." ) @@ -967,39 +983,32 @@ def minify_sync(reassign_deleted: bool, prune: bool): save_minify_config, sync_minify_config, ) - from reflex.utils import prerequisites - path = _get_minify_json_path() - if not path.exists(): + if not _get_minify_json_path().exists(): console.error( f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." ) raise SystemExit(1) - # Load the user's app so every State subclass registers itself. - prerequisites.get_app() + _load_app_for_minify() - # Load existing config existing_config = _load_minify_config_uncached() if existing_config is None: console.error(f"Failed to load {MINIFY_JSON}.") raise SystemExit(1) - old_states = len(existing_config["states"]) - old_events = sum(len(handlers) for handlers in existing_config["events"].values()) - - # Sync against every registered state. new_config = sync_minify_config( existing_config, reassign_deleted=reassign_deleted, prune=prune ) save_minify_config(new_config) - new_states = len(new_config["states"]) - new_events = sum(len(handlers) for handlers in new_config["events"].values()) - console.log(f"Updated {MINIFY_JSON}:") - console.log(f" States: {old_states} -> {new_states}") - console.log(f" Events: {old_events} -> {new_events}") + console.log( + f" States: {len(existing_config['states'])} -> {len(new_config['states'])}" + ) + console.log( + f" Events: {_count_events(existing_config)} -> {_count_events(new_config)}" + ) @minify.command(name="validate") @@ -1015,25 +1024,20 @@ def minify_validate(): _load_minify_config_uncached, validate_minify_config, ) - from reflex.utils import prerequisites - path = _get_minify_json_path() - if not path.exists(): + if not _get_minify_json_path().exists(): console.error( f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." ) raise SystemExit(1) - # Load the user's app to register all state classes - prerequisites.get_app() + _load_app_for_minify() - # Load existing config config = _load_minify_config_uncached() if config is None: console.error(f"Failed to load {MINIFY_JSON}.") raise SystemExit(1) - # Validate errors, warnings, missing = validate_minify_config(config) if errors: @@ -1075,7 +1079,6 @@ def minify_list(output_json: bool): get_state_id, ) from reflex.state import BaseState, State - from reflex.utils import prerequisites class EventHandlerData(TypedDict): """Type for event handler data in state tree.""" @@ -1092,10 +1095,9 @@ class StateTreeData(TypedDict): event_handlers: list[EventHandlerData] substates: list[StateTreeData] - # Load the user's app to register all state classes - prerequisites.get_app() + _load_app_for_minify() - # CLI inspection always shows config contents regardless of env var settings + # CLI inspection shows config contents regardless of env var settings. minify_enabled = get_minify_config() is not None def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: @@ -1215,10 +1217,8 @@ def minify_lookup(output_json: bool, minified_path: str): """ from reflex.minify import MINIFY_JSON, get_minify_config, get_state_full_path from reflex.state import BaseState, State - from reflex.utils import prerequisites - # Load the user's app to register all state classes - prerequisites.get_app() + _load_app_for_minify() config = get_minify_config() if config is None: diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 06662e53aa0..a2ab240d34a 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -264,9 +264,15 @@ def get_compiled_app( Returns: The compiled app based on the default config. """ + from reflex.minify import install_minify_resolver + app, app_module = get_and_validate_app( reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date ) + # Install the minify resolver between app import (static states already + # registered, will be re-keyed in one shot) and page evaluation (dynamic + # states pick up their minified names at registration time). + install_minify_resolver() app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich) return app_module diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 3f54c0e389b..f03e648de18 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -743,6 +743,98 @@ def dynamic_handler(self): # so the new name picks up the minified id without re-registering. assert _resolved_event_id(TestStateWithDynamicHandler, "dynamic_handler") == "d" + def test_component_state_picks_up_minified_name( + self, temp_minify_json, monkeypatch + ): + """``ComponentState.create()`` instances are real state classes too. + + They register via ``__init_subclass__`` like any other state, so as + long as the minify resolver is installed before ``create()`` runs, + the resulting class gets the minified name from ``minify.json``. + """ + import reflex as rx + + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) + + # ComponentState.create() builds a new class via ``type(...)`` with + # ``__module__ = "reflex.istate.dynamic"`` and a ``_n`` suffix, + # so the path under which the resolver will look it up is fully + # determined ahead of time. + instance_count = rx.ComponentState._per_component_state_instance_count + 1 + instance_path = ( + f"reflex.istate.dynamic.State.ComponentStateMinifyExample_n{instance_count}" + ) + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "reflex.state.State": "a", + instance_path: "z", + }, + "events": { + instance_path: {"increment": "i", "setvar": "s"}, + }, + } + save_minify_config(config) + clear_config_cache() + + class ComponentStateMinifyExample(rx.ComponentState): + count: int = 0 + + @rx.event + def increment(self): + self.count += 1 + + @classmethod + def get_component(cls, **props): + return rx.fragment() + + ComponentStateMinifyExample.create() + instance_cls = next( + cls + for cls in RegistrationContext.get().base_states.values() + if cls.__name__ == f"ComponentStateMinifyExample_n{instance_count}" + ) + + assert instance_cls.get_name() == "z" + assert _resolved_event_id(instance_cls, "increment") == "i" + + def test_state_created_after_resolver_install_uses_minified_name( + self, temp_minify_json, monkeypatch + ): + """A state class created after the resolver is installed gets its + minified name at registration time — no later refresh needed. + + This is the path exercised by ``ComponentState.create()`` and any + locally-defined state inside a page function: the class doesn't + exist when ``minify.json`` is loaded, so the resolver must be + consulted lazily on first lookup. + """ + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) + late_path = "tests.units.test_minification.State.LateBornState" + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {"reflex.state.State": "a", late_path: "lb"}, + "events": {}, + } + save_minify_config(config) + clear_config_cache() # installs the MinifyNameResolver + + class LateBornState(State): + pass + + # Newly-registered class is keyed by its minified name in the registry. + ctx = RegistrationContext.get() + assert LateBornState.get_name() == "lb" + assert ctx.base_states.get("a.lb") is LateBornState + class TestMinifyModeEnvVars: """Tests for REFLEX_MINIFY_STATES and REFLEX_MINIFY_EVENTS env vars.""" @@ -1011,9 +1103,11 @@ class ChildState(AppState): save_minify_config(config) clear_config_cache() - # Mock prerequisites.get_app to avoid needing a real app + # Stub the compiled-app loader; the test already populates the registry app_module_mock = mock.Mock() - monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + monkeypatch.setattr( + prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + ) runner = CliRunner() result = runner.invoke(cli, ["minify", "lookup", "a.b.c"]) @@ -1033,9 +1127,11 @@ def test_lookup_fails_without_minify_json(self, temp_minify_json, monkeypatch): from reflex.reflex import cli from reflex.utils import prerequisites - # Mock prerequisites.get_app + # Stub the compiled-app loader app_module_mock = mock.Mock() - monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + monkeypatch.setattr( + prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + ) # Don't create minify.json clear_config_cache() @@ -1066,9 +1162,11 @@ def test_lookup_fails_for_invalid_path(self, temp_minify_json, monkeypatch): save_minify_config(config) clear_config_cache() - # Mock prerequisites.get_app + # Stub the compiled-app loader app_module_mock = mock.Mock() - monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + monkeypatch.setattr( + prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + ) runner = CliRunner() # Try to lookup a path that doesn't exist @@ -1104,9 +1202,11 @@ class JsonTestState(State): save_minify_config(config) clear_config_cache() - # Mock prerequisites.get_app + # Stub the compiled-app loader app_module_mock = mock.Mock() - monkeypatch.setattr(prerequisites, "get_app", lambda *a, **kw: app_module_mock) + monkeypatch.setattr( + prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + ) runner = CliRunner() result = runner.invoke(cli, ["minify", "lookup", "--json", "a.b"]) From 66dcbe5aee614f858a05807682e66533e79f9784 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 00:16:41 +0200 Subject: [PATCH 59/74] cleanup --- .../reflex-base/src/reflex_base/registry.py | 114 ++++++----- .../src/reflex_base/utils/format.py | 32 +-- reflex/minify.py | 190 ++++++------------ reflex/reflex.py | 48 ++--- reflex/state.py | 36 ++-- reflex/utils/prerequisites.py | 5 +- tests/units/test_minification.py | 14 +- 7 files changed, 171 insertions(+), 268 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 36906cf5d68..5d535ecee34 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -1,18 +1,9 @@ -"""A contextual registry for state and event handlers. +"""Contextual registry for state classes, event handlers, and the +:class:`NameResolver` strategy that turns them into user-visible names. -The registry owns three kinds of bookkeeping: - -* the set of registered ``BaseState`` subclasses and their parent/child topology -* the set of registered ``EventHandler`` instances keyed by their full dotted name -* the **name resolution** strategy that turns a state class or handler name into the - string used by the rest of the framework - -The third concern is pluggable through the :class:`NameResolver` protocol. The -default resolver is a no-op (every state and handler keeps its built-in name). -``reflex.minify`` ships a :class:`MinifyNameResolver` that consults -``minify.json``; users can plug in their own (custom prefixes, multi-tenant -aliasing, deterministic test fixtures, etc.) by calling -:meth:`RegistrationContext.set_name_resolver`. +The default resolver is a no-op; ``reflex.minify.MinifyNameResolver`` +plugs in a :class:`minify.json`-driven implementation. Install a custom +resolver via :meth:`RegistrationContext.set_name_resolver`. """ from __future__ import annotations @@ -33,16 +24,10 @@ @runtime_checkable class NameResolver(Protocol): - """Resolves the user-visible names for state classes and event handlers. - - Implementations may apply minification, prefixing, locale-based aliasing, - or any other transformation. Returning ``None`` from either method means - "no override — use the default name". + """Resolves user-visible names for state classes and event handlers. - The protocol is intentionally tiny so resolvers compose well; see - :class:`DefaultNameResolver` for the no-op base case and - ``reflex.minify.MinifyNameResolver`` for the canonical example of a - resolver that consults external configuration. + Return ``None`` to defer to the framework default. See + :class:`DefaultNameResolver` (no-op) and ``reflex.minify.MinifyNameResolver``. """ def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: @@ -119,6 +104,18 @@ def ensure_context(cls) -> Self: cls._context_var.set(ctx) return ctx + @classmethod + def try_get(cls) -> Self | None: + """Return the active context, or ``None`` when none is attached. + + Returns: + The registration context instance, or ``None``. + """ + try: + return cls.get() + except LookupError: + return None + @classmethod def register_base_state(cls, state_cls: type[BaseState]) -> type[BaseState]: """Register a base state class with its full name. @@ -220,10 +217,7 @@ def get_substates( @staticmethod def default_state_name(state_cls: type[BaseState]) -> str: - """Compute the built-in name for a state class (no resolver applied). - - This is the snake-cased ``module___ClassName`` form used when no - resolver overrides it. + """Compute the built-in snake-cased ``module___ClassName`` for a state. Args: state_cls: The state class. @@ -239,14 +233,11 @@ def default_state_name(state_cls: type[BaseState]) -> str: def get_state_name(self, state_cls: type[BaseState]) -> str: """Resolve the user-visible name for a state class. - Asks the installed :class:`NameResolver` first; falls back to - :meth:`default_state_name` when the resolver returns ``None``. - Args: state_cls: The state class. Returns: - The resolved name. + The resolved name (or :meth:`default_state_name` fallback). """ resolved = self.name_resolver.resolve_state_name(state_cls) if resolved is not None: @@ -256,15 +247,12 @@ def get_state_name(self, state_cls: type[BaseState]) -> str: def get_handler_name(self, state_cls: type[BaseState], handler_name: str) -> str: """Resolve the user-visible name for an event handler. - Asks the installed :class:`NameResolver` first; falls back to the - original ``handler_name`` when the resolver returns ``None``. - Args: state_cls: The state class the handler is attached to. handler_name: The original (Python) name of the handler. Returns: - The resolved name. + The resolved name (or ``handler_name`` unchanged). """ resolved = self.name_resolver.resolve_handler_name(state_cls, handler_name) if resolved is not None: @@ -272,16 +260,12 @@ def get_handler_name(self, state_cls: type[BaseState], handler_name: str) -> str return handler_name def set_name_resolver(self, resolver: NameResolver) -> None: - """Install ``resolver`` and propagate the new names through the registry. + """Install ``resolver`` and rebuild the registry under the new names. - Concretely: clears the per-class ``get_name``/``get_full_name``/ - ``get_class_substate`` lru_caches on every registered state and calls - :meth:`refresh_keys` so the registry's name-keyed dicts reflect what - the new resolver returns. - - ``resolver`` is stored even on a ``frozen`` dataclass via - ``object.__setattr__`` — the field is conceptually mutable while the - rest of the context shape stays immutable. + Clears the per-class ``get_name`` / ``get_full_name`` / + ``get_class_substate`` lru_caches and calls :meth:`refresh_keys`. + Uses ``object.__setattr__`` to mutate the frozen ``name_resolver`` + slot. Args: resolver: The resolver to install. Pass :class:`DefaultNameResolver` @@ -295,33 +279,47 @@ def set_name_resolver(self, resolver: NameResolver) -> None: self.refresh_keys() def refresh_keys(self) -> None: - """Re-key all registered classes/handlers using current full names. - - State minification rewrites ``BaseState.get_name()``, but the registry - uses ``parent.get_full_name()`` as the dict key at registration time. - If the minify config (or env var) changes after a class was registered, - lookups will miss. Call this after ``minify.clear_config_cache()`` to - rebuild the dicts with the current names. + """Re-key the name-keyed dicts using current ``get_full_name`` values. - Builds the replacement dicts before mutating ``self`` so a failure - partway through (e.g. an unreadable minify.json that makes - ``format_event_handler`` raise) leaves the existing registry intact. + Built atomically: the replacement dicts are populated before any of + ``self`` is mutated, so a partway failure (e.g. malformed minify.json + making ``format_event_handler`` raise) leaves the existing registry + intact. A console warning is emitted on full-name collisions — + usually a sign of duplicate ids in ``minify.json``. """ + from reflex.utils import console from reflex.utils.format import format_event_handler all_classes = list(self.base_states.values()) new_base_states: dict[str, type[BaseState]] = {} new_substates: dict[str, set[type[BaseState]]] = {} for cls in all_classes: - new_base_states[cls.get_full_name()] = cls + full_name = cls.get_full_name() + existing = new_base_states.get(full_name) + if existing is not None and existing is not cls: + console.warn( + f"Two state classes resolve to the same full name " + f"{full_name!r}: {existing!r} and {cls!r}. The first one " + "will be unreachable in the registry. Check minify.json " + "for duplicate ids." + ) + new_base_states[full_name] = cls parent = cls.get_parent_state() if parent is not None: new_substates.setdefault(parent.get_full_name(), set()).add(cls) all_handlers = list(self.event_handlers.values()) - new_handlers: dict[str, RegisteredEventHandler] = { - format_event_handler(reg.handler): reg for reg in all_handlers - } + new_handlers: dict[str, RegisteredEventHandler] = {} + for reg in all_handlers: + full_name = format_event_handler(reg.handler) + existing_handler = new_handlers.get(full_name) + if existing_handler is not None and existing_handler is not reg: + console.warn( + f"Two event handlers resolve to the same full name " + f"{full_name!r}. The first one will be unreachable in " + "the registry. Check minify.json for duplicate ids." + ) + new_handlers[full_name] = reg self.base_states.clear() self.base_states.update(new_base_states) diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index 3e1b633ddcd..78bd99ecc60 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -14,13 +14,8 @@ if TYPE_CHECKING: from reflex_base.components.component import ComponentStyle - from reflex_base.event import ( - ArgsSpec, - EventChain, - EventHandler, - EventSpec, - EventType, - ) + from reflex_base.event import EventChain, EventHandler, EventSpec, EventType + from reflex_base.utils.types import ArgsSpec WRAP_MAP = { "{": "}", @@ -448,18 +443,17 @@ def format_props(*single_props, **key_value_props) -> list[str]: def get_event_handler_parts( handler: EventHandler | Callable[..., Any], ) -> tuple[str, str]: - """Get the state and function name of an event handler. + """Get the (state, function) name pair for an event handler. - Both the state name (via ``state.get_full_name()``) and the handler name - (via :meth:`~reflex_base.registry.RegistrationContext.get_handler_name`) - pass through the active :class:`~reflex_base.registry.NameResolver`, so - minification — or any other pluggable rewrite — is applied transparently. + Both names pass through the active + :class:`~reflex_base.registry.NameResolver`, so any installed rewrite + (minification, prefixing, etc.) is applied transparently. Args: - handler: The event handler to get the parts of. + handler: The event handler. Returns: - The (resolved) state full name and (resolved) handler function name. + ``(state_full_name, handler_name)`` — both resolved. Raises: TypeError: If the handler is not an EventHandler. @@ -471,20 +465,14 @@ def get_event_handler_parts( msg = f"Expected EventHandler, got {type(handler)}" raise TypeError(msg) - # The Python name of the handler function (e.g. "ClassName.handler_name"). name = handler.fn.__qualname__ - - # If there's no enclosing state, just return the full qualname. if handler.state is None: return ("", name) state_full_name = handler.state.get_full_name() func_name = name.rpartition(".")[2] - - # Let the registry's resolver have a say (e.g. minified handler names). - try: - ctx = RegistrationContext.get() - except LookupError: + ctx = RegistrationContext.try_get() + if ctx is None: return (state_full_name, func_name) return (state_full_name, ctx.get_handler_name(handler.state, func_name)) diff --git a/reflex/minify.py b/reflex/minify.py index c0508339343..8938c30c686 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -1,20 +1,10 @@ -"""Minification configuration for state and event names. +"""State and event name minification driven by ``minify.json``. -This module provides centralized ID management for minifying state and event -handler names. The configuration is stored in a ``minify.json`` file at the -project root. - -The minification entry point is :class:`MinifyNameResolver`, an implementation -of :class:`reflex_base.registry.NameResolver`. To enable minification at -runtime, install the resolver into the active registration context:: - - from reflex.minify import MinifyNameResolver - from reflex_base.registry import RegistrationContext - - RegistrationContext.get().set_name_resolver(MinifyNameResolver.from_disk()) - -:func:`clear_config_cache` does this automatically — call it whenever -``minify.json`` or the ``REFLEX_MINIFY_*`` environment variables change. +The minification entry point is :class:`MinifyNameResolver`, a +:class:`reflex_base.registry.NameResolver` implementation. Install it via +:func:`install_minify_resolver` (called automatically by +:func:`reflex.utils.prerequisites.get_compiled_app` and +:func:`clear_config_cache`). """ from __future__ import annotations @@ -36,12 +26,7 @@ class MinifyConfig(TypedDict): - """Schema for minify.json file. - - Version 2 format: - - states: dict mapping state_path -> minified_name (string) - - events: dict mapping state_path -> {handler_name -> minified_name} - """ + """Schema for ``minify.json`` (version :data:`SCHEMA_VERSION`).""" version: int states: dict[str, str] # state_path -> minified_name @@ -118,33 +103,30 @@ def _load_minify_config_uncached() -> MinifyConfig | None: @functools.cache def get_minify_config() -> MinifyConfig | None: - """Get the minify configuration, cached. - - This function is cached so the file is only read once per process. + """Read ``minify.json`` once per process. Returns: - The parsed configuration, or None if file doesn't exist. + The parsed config, or ``None`` if the file is absent. """ return _load_minify_config_uncached() def is_minify_enabled() -> bool: - """Check if any minification is enabled (state or event). + """Whether either state or event minification is enabled. Returns: - True if either state or event minification is enabled. + ``True`` if either ``REFLEX_MINIFY_STATES`` or ``REFLEX_MINIFY_EVENTS`` + is on and a config exists. """ return is_state_minify_enabled() or is_event_minify_enabled() @functools.cache def is_state_minify_enabled() -> bool: - """Check if state ID minification is enabled. - - Requires both REFLEX_MINIFY_STATE=enabled and minify.json to exist. + """Whether state-id minification is enabled. Returns: - True if state minification is enabled. + ``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists. """ from reflex.environment import MinifyMode, environment @@ -156,12 +138,10 @@ def is_state_minify_enabled() -> bool: @functools.cache def is_event_minify_enabled() -> bool: - """Check if event ID minification is enabled. - - Requires both REFLEX_MINIFY_EVENTS=enabled and minify.json to exist. + """Whether event-id minification is enabled. Returns: - True if event minification is enabled. + ``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists. """ from reflex.environment import MinifyMode, environment @@ -172,13 +152,13 @@ def is_event_minify_enabled() -> bool: def get_state_id(state_full_path: str) -> str | None: - """Get the minified ID for a state. + """Look up the minified id for a state path. Args: - state_full_path: The full path to the state (e.g., "myapp.state.AppState.UserState"). + state_full_path: e.g. ``"myapp.state.AppState.UserState"``. Returns: - The minified state name (e.g., "a", "ba") if configured, None otherwise. + The minified id, or ``None`` if not configured. """ config = get_minify_config() if config is None: @@ -187,14 +167,14 @@ def get_state_id(state_full_path: str) -> str | None: def get_event_id(state_full_path: str, handler_name: str) -> str | None: - """Get the minified ID for an event handler. + """Look up the minified id for an event handler. Args: state_full_path: The full path to the state. - handler_name: The name of the event handler. + handler_name: The handler's original name. Returns: - The minified event name (e.g., "a", "ba") if configured, None otherwise. + The minified id, or ``None`` if not configured. """ config = get_minify_config() if config is None: @@ -221,19 +201,13 @@ def save_minify_config(config: MinifyConfig) -> None: class MinifyNameResolver: """:class:`~reflex_base.registry.NameResolver` driven by ``minify.json``. - Returns the minified name from the configuration when the corresponding - ``REFLEX_MINIFY_STATES`` / ``REFLEX_MINIFY_EVENTS`` env-var is set to - ``enabled`` *and* the entry exists in the config. All other lookups return - ``None`` so the registry falls back to the default name. - - The resolver is constructed once (typically via :meth:`from_disk`) and - held by the active :class:`~reflex_base.registry.RegistrationContext`. - Per-class lookups are memoized in ``_state_cache`` and ``_event_cache`` - for O(1) amortized cost on the hot path. + Returns the minified name when the matching env-var + (``REFLEX_MINIFY_STATES`` / ``REFLEX_MINIFY_EVENTS``) is enabled and the + entry exists in the config; ``None`` otherwise. Per-class lookups are + memoized for O(1) amortized cost. Attributes: - config: The parsed ``minify.json`` content, or ``None`` when the file - is absent or malformed. + config: Parsed ``minify.json``, or ``None``. states_enabled: Whether ``REFLEX_MINIFY_STATES`` is on. events_enabled: Whether ``REFLEX_MINIFY_EVENTS`` is on. """ @@ -252,21 +226,16 @@ class MinifyNameResolver: def from_disk(cls) -> MinifyNameResolver: """Build a resolver from the current ``minify.json`` and env vars. - Reads each input exactly once. Subsequent state/event lookups never - touch the filesystem. - Returns: - A configured resolver. May still return ``None`` from every lookup - if minification is disabled or the config file is absent. + A configured resolver — may resolve nothing if minify is disabled + or the config is absent/malformed (the validate CLI surfaces + errors separately via :func:`_load_minify_config_uncached`). """ from reflex.environment import MinifyMode, environment - config: MinifyConfig | None try: - config = _load_minify_config_uncached() + config = get_minify_config() except ValueError: - # Treat malformed config as "no config" for the resolver — callers - # that need to surface the error can read it via get_minify_config. config = None return cls( config=config, @@ -300,35 +269,21 @@ def resolve_handler_name( # noqa: D102 def install_minify_resolver() -> None: """Install a fresh :class:`MinifyNameResolver` into the active context. - Use this after the user's app has been imported (so the registration - context contains the right state classes) to switch the framework over - to minified names. - - Returns silently if no registration context is currently attached. + No-op if no registration context is attached. """ - try: - from reflex_base.registry import RegistrationContext + from reflex_base.registry import RegistrationContext - ctx = RegistrationContext.get() - except LookupError: + ctx = RegistrationContext.try_get() + if ctx is None: return ctx.set_name_resolver(MinifyNameResolver.from_disk()) def clear_config_cache() -> None: - """Reload the minify configuration and propagate it through the registry. - - Clears the module-level lru_caches for :func:`get_minify_config`, - :func:`is_state_minify_enabled`, :func:`is_event_minify_enabled`, then - rebuilds the :class:`MinifyNameResolver` from the current - ``minify.json`` / env vars and installs it via - :meth:`reflex_base.registry.RegistrationContext.set_name_resolver`. The - set-resolver call clears per-class name caches and re-keys the registry - in one atomic step. - - Call this whenever ``minify.json`` is rewritten programmatically, or - after monkey-patching ``REFLEX_MINIFY_STATES`` / ``REFLEX_MINIFY_EVENTS`` - at runtime. + """Reload ``minify.json`` and propagate the new names through the registry. + + Call after editing ``minify.json`` programmatically or changing + ``REFLEX_MINIFY_*`` env vars at runtime. """ get_minify_config.cache_clear() is_state_minify_enabled.cache_clear() @@ -343,45 +298,41 @@ def clear_config_cache() -> None: def int_to_minified_name(id_: int) -> str: - """Convert integer ID to minified name using base-54 encoding. + """Encode a non-negative integer as a base-54 minified name. Args: - id_: The integer ID to convert. + id_: The integer to encode. Returns: - A minified string representation. + e.g. ``0 → "a"``, ``25 → "z"``, ``54 → "ba"``. Raises: - ValueError: If id_ is negative. + ValueError: If ``id_`` is negative. """ if id_ < 0: msg = f"ID must be non-negative, got {id_}" raise ValueError(msg) - - # Special case: 0 maps to 'a' if id_ == 0: return _MINIFY_CHARS[0] - result = [] num = id_ while num > 0: result.append(_MINIFY_CHARS[num % _MINIFY_BASE]) num //= _MINIFY_BASE - return "".join(reversed(result)) def minified_name_to_int(name: str) -> int: - """Convert minified name back to integer ID. + """Decode a base-54 minified name back to its integer id. Args: - name: The minified string to convert. + name: The minified string. Returns: - The integer ID. + The integer id. Raises: - ValueError: If name contains invalid characters. + ValueError: If ``name`` contains invalid characters. """ result = 0 for char in name: @@ -394,57 +345,41 @@ def minified_name_to_int(name: str) -> int: def get_state_full_path(state_cls: type[BaseState]) -> str: - """Get the full path for a state class suitable for minify.json. + """Build the unique ``module.Class.SubClass`` path for a state class. - This returns the module path plus class name hierarchy, which uniquely - identifies a state class. + Uses ``__original_module__`` when available so dynamically-relocated + states (e.g. ``ComponentState.create()``) keep their import-site path. Args: state_cls: The state class. Returns: - The full path string (e.g., "myapp.state.AppState.UserState"). + e.g. ``"myapp.state.AppState.UserState"``. """ - # Build the path from module + class hierarchy - # Use __original_module__ if available (for dynamic states that get moved) module = getattr(state_cls, "__original_module__", None) or state_cls.__module__ - parts = [module] - - # Get the class hierarchy from root to this class - class_hierarchy = [] + class_hierarchy: list[str] = [] current: type[BaseState] | None = state_cls while current is not None: class_hierarchy.append(current.__name__) current = current.get_parent_state() # type: ignore[union-attr] - - # Reverse to get root-to-leaf order class_hierarchy.reverse() - - # Combine module and class hierarchy - parts.extend(class_hierarchy) - return ".".join(parts) + return ".".join([module, *class_hierarchy]) def collect_all_states( root_state: type[BaseState] | None = None, ) -> list[type[BaseState]]: - """Collect state classes in deterministic depth-first order. + """Collect state classes in deterministic depth-first, sibling-sorted order. Without ``root_state``, walks every state registered in the active - :class:`~reflex_base.registry.RegistrationContext` (each connected tree - starting from a parentless root), sorting siblings alphabetically by - class name. With ``root_state``, the walk is restricted to that subtree. - - The CLI commands use the parameterless form so the result reflects the - user's currently-loaded app. Tests typically pass an explicit root to - scope the walk to the classes defined inside the test. + :class:`~reflex_base.registry.RegistrationContext` (one tree per + parentless root). With ``root_state``, restricts the walk to that subtree. Args: - root_state: Optional subtree root. ``None`` means "every registered - state". + root_state: Optional subtree root. Returns: - List of state classes in depth-first, sibling-sorted order. + State classes in depth-first, sibling-sorted order. """ if root_state is not None: result = [root_state] @@ -470,14 +405,11 @@ def generate_minify_config( ) -> MinifyConfig: """Generate a complete minify configuration. - Walks the state tree (see :func:`collect_all_states`) and assigns minified - names starting from ``"a"`` per sibling group. Siblings are sorted by - class name and handlers by handler name so the output is byte-stable across - runs (and therefore VCS-friendly). + Walks the state tree (see :func:`collect_all_states`) and assigns ids + starting from ``"a"`` per sibling group. Output is byte-stable. Args: - root_state: Optional subtree root. ``None`` (the default) generates a - config for every state registered in the active context. + root_state: Optional subtree root. Returns: A complete :class:`MinifyConfig`. diff --git a/reflex/reflex.py b/reflex/reflex.py index f80f6f2135e..a88704960be 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -908,29 +908,39 @@ def minify(): def _load_app_for_minify() -> None: - """Compile the app so dynamic states are registered. - - ``ComponentState.create()`` and locally-defined states inside page - functions only register during page evaluation, so a plain ``get_app()`` - would miss them. - """ + """Compile the user's app so dynamic states (e.g. ``ComponentState``) register.""" from reflex.utils import prerequisites prerequisites.get_compiled_app(dry_run=True) def _count_events(config: MinifyConfig) -> int: - """Sum the event handlers across every state in a minify config. + """Sum event handlers across every state in a minify config. Args: - config: The minify configuration to count events in. + config: The minify configuration. Returns: - The total number of event handlers across every state. + Total handler count. """ return sum(len(handlers) for handlers in config["events"].values()) +def _load_existing_minify_config() -> MinifyConfig: + """Load ``minify.json`` or exit the CLI with an error. + + Returns: + The parsed minify configuration. + """ + from reflex.minify import MINIFY_JSON, _load_minify_config_uncached + + config = _load_minify_config_uncached() + if config is None: + console.error(f"Failed to load {MINIFY_JSON}.") + raise SystemExit(1) + return config + + @minify.command(name="init") @loglevel_option def minify_init(): @@ -979,7 +989,6 @@ def minify_sync(reassign_deleted: bool, prune: bool): from reflex.minify import ( MINIFY_JSON, _get_minify_json_path, - _load_minify_config_uncached, save_minify_config, sync_minify_config, ) @@ -991,11 +1000,7 @@ def minify_sync(reassign_deleted: bool, prune: bool): raise SystemExit(1) _load_app_for_minify() - - existing_config = _load_minify_config_uncached() - if existing_config is None: - console.error(f"Failed to load {MINIFY_JSON}.") - raise SystemExit(1) + existing_config = _load_existing_minify_config() new_config = sync_minify_config( existing_config, reassign_deleted=reassign_deleted, prune=prune @@ -1018,12 +1023,7 @@ def minify_validate(): Checks for duplicate IDs, missing entries, and orphaned entries. """ - from reflex.minify import ( - MINIFY_JSON, - _get_minify_json_path, - _load_minify_config_uncached, - validate_minify_config, - ) + from reflex.minify import MINIFY_JSON, _get_minify_json_path, validate_minify_config if not _get_minify_json_path().exists(): console.error( @@ -1032,11 +1032,7 @@ def minify_validate(): raise SystemExit(1) _load_app_for_minify() - - config = _load_minify_config_uncached() - if config is None: - console.error(f"Failed to load {MINIFY_JSON}.") - raise SystemExit(1) + config = _load_existing_minify_config() errors, warnings, missing = validate_minify_config(config) diff --git a/reflex/state.py b/reflex/state.py index 057ae06dd6b..46f3946f54d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -967,21 +967,19 @@ def get_substates(cls) -> set[type[BaseState]]: def get_name(cls) -> str: """Get the user-visible name of the state. - Delegates to the active :class:`~reflex_base.registry.NameResolver` via - :meth:`~reflex_base.registry.RegistrationContext.get_state_name` so that - e.g. ``minify.json`` (or any other resolver a user installs) can rewrite - the name. Falls back to the built-in snake-cased ``module___ClassName`` - form when no registration context is active. + Defers to the active :class:`~reflex_base.registry.NameResolver` + (e.g. ``minify.json`` rewrites), falling back to the built-in + snake-cased ``module___ClassName`` when no context is attached. Returns: The resolved name of the state. """ from reflex_base.registry import RegistrationContext - try: - return RegistrationContext.get().get_state_name(cls) - except LookupError: + ctx = RegistrationContext.try_get() + if ctx is None: return RegistrationContext.default_state_name(cls) + return ctx.get_state_name(cls) @classmethod @functools.lru_cache @@ -1006,12 +1004,11 @@ def get_class_substate( Args: path: The path to the substate. - _skip_self: Internal recursion flag. External callers must leave - this at the default ``True`` — only the root call should strip - a leading segment that matches ``cls.get_name()``. Recursive - calls pass ``False`` so that a child whose minified name - collides with its parent (e.g. ``"a.b.b"``) resolves to the - child rather than terminating early at the parent. + _skip_self: Internal recursion flag — leave at the default. Only + the root call strips a leading segment that matches + ``cls.get_name()``; recursion passes ``False`` so a child + that shares a minified name with its parent (``"a.b.b"``) + still resolves to the child. Returns: The class substate. @@ -1541,12 +1538,11 @@ def get_substate(self, path: Sequence[str], _skip_self: bool = True) -> BaseStat Args: path: The path to the substate. - _skip_self: Internal recursion flag. External callers must leave - this at the default ``True`` — only the root call should strip - a leading segment that matches ``self.get_name()``. Recursive - calls pass ``False`` so that a child whose minified name - collides with its parent (e.g. ``"a.b.b"``) resolves to the - child rather than terminating early at the parent. + _skip_self: Internal recursion flag — leave at the default. Only + the root call strips a leading segment that matches + ``self.get_name()``; recursion passes ``False`` so a child + that shares a minified name with its parent (``"a.b.b"``) + still resolves to the child. Returns: The substate. diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index a2ab240d34a..7d8a638fb1e 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -269,9 +269,8 @@ def get_compiled_app( app, app_module = get_and_validate_app( reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date ) - # Install the minify resolver between app import (static states already - # registered, will be re-keyed in one shot) and page evaluation (dynamic - # states pick up their minified names at registration time). + # After app import (static states registered, re-keyed in one shot) and + # before page eval (dynamic states pick up minified names on registration). install_minify_resolver() app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich) return app_module diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index f03e648de18..53d914a4137 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -127,17 +127,11 @@ class TestState(BaseState): @pytest.fixture def temp_minify_json(tmp_path, monkeypatch): - """Create a temporary directory and mock cwd to use it for minify.json. + """Run the test against a fresh ``minify.json`` location. - ``clear_config_cache()`` walks the registration context and clears - ``get_name``/``get_full_name``/``get_class_substate`` LRU caches on every - registered State class, then re-keys the registry, so we don't have to - remember every State subclass that earlier tests may have registered. - - The teardown undoes monkeypatch *before* clearing the cache so that the - refresh is based on the unmodified env/cwd; otherwise the registry would - be left pinned to whatever minified names the test set up, breaking - subsequent tests that don't use this fixture. + Teardown undoes monkeypatch *before* clearing the cache so the registry + is rebuilt under unmodified env/cwd — otherwise other tests would inherit + minified names. Yields: The temporary directory path. From 673243c1f6dd779ceb8e783cde62591b83a6e1bd Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 13:59:21 +0200 Subject: [PATCH 60/74] wip --- reflex/minify.py | 70 +++++++++++++++++++++++--- reflex/utils/prerequisites.py | 16 ++++-- tests/integration/test_minification.py | 8 ++- tests/units/test_minification.py | 59 +++++++++++++++------- 4 files changed, 118 insertions(+), 35 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 8938c30c686..d4517b0f34d 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -226,6 +226,11 @@ class MinifyNameResolver: def from_disk(cls) -> MinifyNameResolver: """Build a resolver from the current ``minify.json`` and env vars. + Reads the file directly (bypassing the lru_cache on + :func:`get_minify_config`) so the snapshot reflects the cwd at install + time rather than whatever cwd the cache happened to be warmed under. + Per-class lookups are still memoized inside the returned resolver. + Returns: A configured resolver — may resolve nothing if minify is disabled or the config is absent/malformed (the validate CLI surfaces @@ -234,7 +239,7 @@ def from_disk(cls) -> MinifyNameResolver: from reflex.environment import MinifyMode, environment try: - config = get_minify_config() + config = _load_minify_config_uncached() except ValueError: config = None return cls( @@ -244,7 +249,7 @@ def from_disk(cls) -> MinifyNameResolver: ) def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: D102 - if not (self.states_enabled and self.config): + if not (self.states_enabled and self.config) or _is_framework_state(state_cls): return None cached = self._state_cache.get(state_cls) if cached is not None: @@ -257,7 +262,7 @@ def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: def resolve_handler_name( # noqa: D102 self, state_cls: type[BaseState], handler_name: str ) -> str | None: - if not (self.events_enabled and self.config): + if not (self.events_enabled and self.config) or _is_framework_state(state_cls): return None per_state = self._event_cache.get(state_cls) if per_state is None: @@ -266,15 +271,68 @@ def resolve_handler_name( # noqa: D102 return per_state.get(handler_name) +#: Modules that define framework-owned :class:`BaseState` subclasses whose +#: :class:`Var` hooks are baked at framework-import time, before any user +#: resolver can be installed. Honoring a minified mapping for them would +#: leave dangling references in the generated frontend. +#: +#: Modules under ``reflex.istate.dynamic`` are intentionally *not* listed — +#: that's where ``ComponentState.create()`` and ``_handle_local_def`` relocate +#: user-defined classes, which remain minifiable. +_FRAMEWORK_STATE_MODULES: frozenset[str] = frozenset({ + "reflex.state", + "reflex.istate.shared", + "reflex.custom_components.custom_components", +}) + + +def _is_framework_state(state_cls: type[BaseState]) -> bool: + """Whether ``state_cls`` is one of the framework's own state classes. + + Args: + state_cls: The state class to check. + + Returns: + ``True`` if ``state_cls`` belongs to a known framework module. + """ + module = getattr(state_cls, "__original_module__", None) or state_cls.__module__ + return module in _FRAMEWORK_STATE_MODULES + + def install_minify_resolver() -> None: """Install a fresh :class:`MinifyNameResolver` into the active context. - No-op if no registration context is attached. + Uses :meth:`~reflex_base.registry.RegistrationContext.ensure_context` so + the resolver can be installed *before* any state class is registered — + that's important because :func:`reflex_base.vars.base.VarData.from_state` + captures ``state.get_full_name()`` at Var-creation time. If the resolver + is installed afterwards the captured strings stay un-minified and won't + match the registry, breaking the generated frontend. + """ + from reflex_base.registry import RegistrationContext + + ctx = RegistrationContext.ensure_context() + ctx.set_name_resolver(MinifyNameResolver.from_disk()) + + +def ensure_minify_resolver_for_active_context() -> None: + """Install a :class:`MinifyNameResolver` for the active context if needed. + + Re-installs only when a config-less resolver is currently active — + typically because an earlier install ran from a cwd where ``minify.json`` + didn't exist yet (e.g. a test fixture that called + :func:`clear_config_cache` before chdir-ing into the app root). Once a + resolver with a loaded config is in place, subsequent calls are no-ops, + so wiring this into hot paths (e.g. + :func:`reflex.utils.prerequisites.get_app`, which can be re-entered from + runtime fallbacks like + :meth:`~reflex.state.OnLoadInternalState.on_load_internal`) won't re-read + ``minify.json`` from a possibly-wrong cwd. """ from reflex_base.registry import RegistrationContext - ctx = RegistrationContext.try_get() - if ctx is None: + ctx = RegistrationContext.ensure_context() + if isinstance(ctx.name_resolver, MinifyNameResolver) and ctx.name_resolver.config: return ctx.set_name_resolver(MinifyNameResolver.from_disk()) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 7d8a638fb1e..656c68585e5 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -182,6 +182,7 @@ def get_app(reload: bool = False) -> ModuleType: Raises: Exception: If an error occurs while getting the app module. """ + from reflex.minify import ensure_minify_resolver_for_active_context from reflex.utils import telemetry try: @@ -193,6 +194,15 @@ def get_app(reload: bool = False) -> ModuleType: module = config.module sys.path.insert(0, getcwd()) # noqa: PTH109 + # Install the minify resolver into the current context before the + # user's module is imported so user-defined state classes register + # with their minified names from the start (``VarData.from_state`` + # captures ``state.get_full_name()`` at class-definition time, so the + # resolver must be in place by then). One-shot per context — won't + # re-read ``minify.json`` on subsequent ``get_app`` calls (which can + # happen at runtime from a different cwd, e.g. the framework's + # ``OnLoadInternalState.on_load_internal`` fallback). + ensure_minify_resolver_for_active_context() app = ( __import__(module, fromlist=(constants.CompileVars.APP,)) if not config.app_module @@ -264,14 +274,10 @@ def get_compiled_app( Returns: The compiled app based on the default config. """ - from reflex.minify import install_minify_resolver - app, app_module = get_and_validate_app( reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date ) - # After app import (static states registered, re-keyed in one shot) and - # before page eval (dynamic states pick up minified names on registration). - install_minify_resolver() + # ``_compile`` installs the minify resolver itself before evaluating pages. app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich) return app_module diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index ad2e94e5684..151002cec22 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -156,18 +156,16 @@ def minify_enabled_app( app_module = "minify_enabled.minify_enabled" root_state_path = f"{app_module}.State.RootState" sub_state_path = f"{app_module}.State.RootState.SubState" + # Framework-internal :class:`State` is intentionally absent — the resolver + # never minifies it (its Vars are baked at framework-import time, before + # any user code can install a resolver). minify_config = { "version": 1, "states": { - # Base State needs an ID too since it's in the hierarchy - "reflex.state.State": "a", - # RootState extends State, so path is module.State.RootState root_state_path: "k", # int_to_minified_name(10) = 'k' - # SubState extends RootState, so path is module.State.RootState.SubState sub_state_path: "l", # int_to_minified_name(11) = 'l' }, "events": { - # Events are now nested under their state path root_state_path: { "increment": "f", # int_to_minified_name(5) = 'f' }, diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 53d914a4137..09e22b67e50 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -813,9 +813,13 @@ def test_state_created_after_resolver_install_uses_minified_name( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) late_path = "tests.units.test_minification.State.LateBornState" + # NOTE: framework-internal states like ``reflex.state.State`` are never + # minified (their Var hooks are baked at framework-import time, well + # before any user resolver can run). The minified id only applies to + # the user-defined state class. config: MinifyConfig = { "version": SCHEMA_VERSION, - "states": {"reflex.state.State": "a", late_path: "lb"}, + "states": {late_path: "lb"}, "events": {}, } save_minify_config(config) @@ -827,7 +831,8 @@ class LateBornState(State): # Newly-registered class is keyed by its minified name in the registry. ctx = RegistrationContext.get() assert LateBornState.get_name() == "lb" - assert ctx.base_states.get("a.lb") is LateBornState + # State stays un-minified, so the parent prefix is its default snake form. + assert ctx.base_states.get(f"{State.get_full_name()}.lb") is LateBornState class TestMinifyModeEnvVars: @@ -989,10 +994,11 @@ class ChildClassSubstateCollision(ParentClassSubstateCollision): parent_path = get_state_full_path(ParentClassSubstateCollision) child_path = get_state_full_path(ChildClassSubstateCollision) + # Framework-internal :class:`State` is never minified, so we only map + # the user-defined classes. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { - "reflex.state.State": "a", parent_path: "b", child_path: "b", # Same minified name as parent }, @@ -1007,11 +1013,12 @@ class ChildClassSubstateCollision(ParentClassSubstateCollision): assert ParentClassSubstateCollision.get_name() == "b" assert ChildClassSubstateCollision.get_name() == "b" - # Full path should be a.b.b - assert ChildClassSubstateCollision.get_full_name() == "a.b.b" + # Full path is ``State.b.b`` (State stays un-minified). + state_prefix = State.get_full_name() + assert ChildClassSubstateCollision.get_full_name() == f"{state_prefix}.b.b" - # get_class_substate should resolve a.b.b to the child, not the parent. - resolved = State.get_class_substate("a.b.b") + # get_class_substate should resolve .b.b to the child, not parent. + resolved = State.get_class_substate(f"{state_prefix}.b.b") assert resolved is ChildClassSubstateCollision def test_get_substate_with_parent_child_name_collision( @@ -1040,10 +1047,10 @@ def my_handler(self): parent_path = get_state_full_path(ParentInstanceSubstateCollision) child_path = get_state_full_path(ChildInstanceSubstateCollision) + # Framework-internal :class:`State` is never minified. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { - "reflex.state.State": "a", parent_path: "b", child_path: "b", # Same minified name as parent }, @@ -1057,8 +1064,8 @@ def my_handler(self): # Create a state instance tree root = State(_reflex_internal_init=True) # type: ignore[call-arg] - # Instance get_substate should resolve a.b.b to ChildInstanceSubstateCollision - resolved = root.get_substate(["a", "b", "b"]) + # Instance get_substate should resolve .b.b to the collision child. + resolved = root.get_substate([State.get_name(), "b", "b"]) assert type(resolved) is ChildInstanceSubstateCollision @@ -1391,33 +1398,47 @@ def test_no_config_returns_none(self): def test_state_lookup_caches(self): """Resolved state names are memoized after the first lookup.""" + + # Use a non-framework state — :func:`MinifyNameResolver` deliberately + # never minifies classes whose module starts with ``reflex.`` because + # their ``Var`` hooks are baked at framework-import time. + class UserStateResolverCacheTest(State): + pass + config: MinifyConfig = { "version": SCHEMA_VERSION, - "states": {get_state_full_path(State): "rs"}, + "states": {get_state_full_path(UserStateResolverCacheTest): "rs"}, "events": {}, } resolver = MinifyNameResolver( config=config, states_enabled=True, events_enabled=False ) - assert resolver.resolve_state_name(State) == "rs" + assert resolver.resolve_state_name(UserStateResolverCacheTest) == "rs" # second call hits the cache - assert State in resolver._state_cache - assert resolver.resolve_state_name(State) == "rs" + assert UserStateResolverCacheTest in resolver._state_cache + assert resolver.resolve_state_name(UserStateResolverCacheTest) == "rs" def test_event_lookup_caches(self): """Resolved handler names are memoized per state class.""" + + # Use a non-framework state — see above. + class UserStateEventCacheTest(State): + pass + config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {}, - "events": {get_state_full_path(State): {"foo": "f", "bar": "b"}}, + "events": { + get_state_full_path(UserStateEventCacheTest): {"foo": "f", "bar": "b"} + }, } resolver = MinifyNameResolver( config=config, states_enabled=False, events_enabled=True ) - assert resolver.resolve_handler_name(State, "foo") == "f" - assert resolver.resolve_handler_name(State, "bar") == "b" - assert resolver.resolve_handler_name(State, "missing") is None - assert State in resolver._event_cache + assert resolver.resolve_handler_name(UserStateEventCacheTest, "foo") == "f" + assert resolver.resolve_handler_name(UserStateEventCacheTest, "bar") == "b" + assert resolver.resolve_handler_name(UserStateEventCacheTest, "missing") is None + assert UserStateEventCacheTest in resolver._event_cache def test_from_disk_handles_malformed_config(self, temp_minify_json): """``from_disk`` returns a usable resolver even when minify.json is bad.""" From 6cdf38321279e9810d0d92a53caa1e7cf01598b3 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 14:24:01 +0200 Subject: [PATCH 61/74] cleanup --- reflex/minify.py | 47 +++++++++----------------- reflex/utils/prerequisites.py | 11 ++---- tests/integration/test_minification.py | 5 ++- tests/units/test_minification.py | 12 +------ 4 files changed, 21 insertions(+), 54 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index d4517b0f34d..647c33e5b4c 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -226,15 +226,12 @@ class MinifyNameResolver: def from_disk(cls) -> MinifyNameResolver: """Build a resolver from the current ``minify.json`` and env vars. - Reads the file directly (bypassing the lru_cache on - :func:`get_minify_config`) so the snapshot reflects the cwd at install - time rather than whatever cwd the cache happened to be warmed under. - Per-class lookups are still memoized inside the returned resolver. + Bypasses the lru_cache on :func:`get_minify_config` so the snapshot + reflects the cwd at install time. Malformed configs become + ``config=None`` — the validate CLI surfaces them separately. Returns: - A configured resolver — may resolve nothing if minify is disabled - or the config is absent/malformed (the validate CLI surfaces - errors separately via :func:`_load_minify_config_uncached`). + A configured resolver. """ from reflex.environment import MinifyMode, environment @@ -271,14 +268,10 @@ def resolve_handler_name( # noqa: D102 return per_state.get(handler_name) -#: Modules that define framework-owned :class:`BaseState` subclasses whose -#: :class:`Var` hooks are baked at framework-import time, before any user -#: resolver can be installed. Honoring a minified mapping for them would -#: leave dangling references in the generated frontend. -#: -#: Modules under ``reflex.istate.dynamic`` are intentionally *not* listed — -#: that's where ``ComponentState.create()`` and ``_handle_local_def`` relocate -#: user-defined classes, which remain minifiable. +#: Modules whose ``BaseState`` subclasses can never be minified — their +#: :class:`Var` hooks are baked at framework-import time before any user +#: resolver can run. ``reflex.istate.dynamic`` is *not* listed: that's where +#: ``ComponentState.create()`` puts user-owned dynamic classes. _FRAMEWORK_STATE_MODULES: frozenset[str] = frozenset({ "reflex.state", "reflex.istate.shared", @@ -302,12 +295,10 @@ def _is_framework_state(state_cls: type[BaseState]) -> bool: def install_minify_resolver() -> None: """Install a fresh :class:`MinifyNameResolver` into the active context. - Uses :meth:`~reflex_base.registry.RegistrationContext.ensure_context` so - the resolver can be installed *before* any state class is registered — - that's important because :func:`reflex_base.vars.base.VarData.from_state` - captures ``state.get_full_name()`` at Var-creation time. If the resolver - is installed afterwards the captured strings stay un-minified and won't - match the registry, breaking the generated frontend. + Must run before any state class is registered: + :func:`reflex_base.vars.base.VarData.from_state` captures the state's + full name at Var-creation time, so a later install would leave dangling + references in the generated frontend. """ from reflex_base.registry import RegistrationContext @@ -318,16 +309,10 @@ def install_minify_resolver() -> None: def ensure_minify_resolver_for_active_context() -> None: """Install a :class:`MinifyNameResolver` for the active context if needed. - Re-installs only when a config-less resolver is currently active — - typically because an earlier install ran from a cwd where ``minify.json`` - didn't exist yet (e.g. a test fixture that called - :func:`clear_config_cache` before chdir-ing into the app root). Once a - resolver with a loaded config is in place, subsequent calls are no-ops, - so wiring this into hot paths (e.g. - :func:`reflex.utils.prerequisites.get_app`, which can be re-entered from - runtime fallbacks like - :meth:`~reflex.state.OnLoadInternalState.on_load_internal`) won't re-read - ``minify.json`` from a possibly-wrong cwd. + Idempotent once a config-loaded resolver is in place — safe to wire into + hot paths (e.g. :func:`reflex.utils.prerequisites.get_app`, which can be + re-entered at runtime from a different cwd than the one that loaded the + config). Re-installs only if the current resolver has ``config=None``. """ from reflex_base.registry import RegistrationContext diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 656c68585e5..8b3068a5d98 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -194,14 +194,8 @@ def get_app(reload: bool = False) -> ModuleType: module = config.module sys.path.insert(0, getcwd()) # noqa: PTH109 - # Install the minify resolver into the current context before the - # user's module is imported so user-defined state classes register - # with their minified names from the start (``VarData.from_state`` - # captures ``state.get_full_name()`` at class-definition time, so the - # resolver must be in place by then). One-shot per context — won't - # re-read ``minify.json`` on subsequent ``get_app`` calls (which can - # happen at runtime from a different cwd, e.g. the framework's - # ``OnLoadInternalState.on_load_internal`` fallback). + # Resolver must be active before the user module imports — see + # ``ensure_minify_resolver_for_active_context`` for why. ensure_minify_resolver_for_active_context() app = ( __import__(module, fromlist=(constants.CompileVars.APP,)) @@ -277,7 +271,6 @@ def get_compiled_app( app, app_module = get_and_validate_app( reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date ) - # ``_compile`` installs the minify resolver itself before evaluating pages. app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich) return app_module diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 151002cec22..19199676839 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -156,9 +156,8 @@ def minify_enabled_app( app_module = "minify_enabled.minify_enabled" root_state_path = f"{app_module}.State.RootState" sub_state_path = f"{app_module}.State.RootState.SubState" - # Framework-internal :class:`State` is intentionally absent — the resolver - # never minifies it (its Vars are baked at framework-import time, before - # any user code can install a resolver). + # Framework state classes (e.g. ``reflex.state.State``) are deliberately + # absent — the resolver never minifies them. minify_config = { "version": 1, "states": { diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 09e22b67e50..f8e7437e612 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -813,10 +813,6 @@ def test_state_created_after_resolver_install_uses_minified_name( environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value ) late_path = "tests.units.test_minification.State.LateBornState" - # NOTE: framework-internal states like ``reflex.state.State`` are never - # minified (their Var hooks are baked at framework-import time, well - # before any user resolver can run). The minified id only applies to - # the user-defined state class. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {late_path: "lb"}, @@ -994,8 +990,6 @@ class ChildClassSubstateCollision(ParentClassSubstateCollision): parent_path = get_state_full_path(ParentClassSubstateCollision) child_path = get_state_full_path(ChildClassSubstateCollision) - # Framework-internal :class:`State` is never minified, so we only map - # the user-defined classes. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { @@ -1047,7 +1041,6 @@ def my_handler(self): parent_path = get_state_full_path(ParentInstanceSubstateCollision) child_path = get_state_full_path(ChildInstanceSubstateCollision) - # Framework-internal :class:`State` is never minified. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { @@ -1399,9 +1392,7 @@ def test_no_config_returns_none(self): def test_state_lookup_caches(self): """Resolved state names are memoized after the first lookup.""" - # Use a non-framework state — :func:`MinifyNameResolver` deliberately - # never minifies classes whose module starts with ``reflex.`` because - # their ``Var`` hooks are baked at framework-import time. + # Non-framework state — see :func:`_is_framework_state`. class UserStateResolverCacheTest(State): pass @@ -1421,7 +1412,6 @@ class UserStateResolverCacheTest(State): def test_event_lookup_caches(self): """Resolved handler names are memoized per state class.""" - # Use a non-framework state — see above. class UserStateEventCacheTest(State): pass From 050727ac1f21f11f428fe9dbb2279d16e2f469f0 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 16:19:48 +0200 Subject: [PATCH 62/74] dry it up --- .../reflex-base/src/reflex_base/registry.py | 70 +++--- reflex/minify.py | 207 +++++++++--------- reflex/reflex.py | 117 +++++----- tests/units/test_minification.py | 14 +- 4 files changed, 207 insertions(+), 201 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 5d535ecee34..c45a4d40238 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -9,7 +9,8 @@ from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING, Protocol, runtime_checkable +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Protocol, TypeVar, runtime_checkable from typing_extensions import Self @@ -21,6 +22,37 @@ from reflex_base.components.component import StatefulComponent from reflex_base.event import EventHandler +_T = TypeVar("_T") + + +def _rekey( + items: Iterable[_T], key_fn: Callable[[_T], str], kind: str +) -> dict[str, _T]: + """Build a name-keyed dict, warning on collisions. + + Args: + items: Source items to re-key. + key_fn: Computes the new key for each item. + kind: Human-readable noun for the collision warning (e.g. ``"state class"``). + + Returns: + A new dict mapping resolved key to item; later items overwrite earlier. + """ + from reflex.utils import console + + out: dict[str, _T] = {} + for item in items: + key = key_fn(item) + existing = out.get(key) + if existing is not None and existing is not item: + console.warn( + f"Two {kind}s resolve to the same full name {key!r}: " + f"{existing!r} and {item!r}. The first one will be unreachable " + "in the registry. Check minify.json for duplicate ids." + ) + out[key] = item + return out + @runtime_checkable class NameResolver(Protocol): @@ -287,39 +319,21 @@ def refresh_keys(self) -> None: intact. A console warning is emitted on full-name collisions — usually a sign of duplicate ids in ``minify.json``. """ - from reflex.utils import console from reflex.utils.format import format_event_handler - all_classes = list(self.base_states.values()) - new_base_states: dict[str, type[BaseState]] = {} + new_base_states = _rekey( + self.base_states.values(), lambda c: c.get_full_name(), "state class" + ) new_substates: dict[str, set[type[BaseState]]] = {} - for cls in all_classes: - full_name = cls.get_full_name() - existing = new_base_states.get(full_name) - if existing is not None and existing is not cls: - console.warn( - f"Two state classes resolve to the same full name " - f"{full_name!r}: {existing!r} and {cls!r}. The first one " - "will be unreachable in the registry. Check minify.json " - "for duplicate ids." - ) - new_base_states[full_name] = cls + for cls in new_base_states.values(): parent = cls.get_parent_state() if parent is not None: new_substates.setdefault(parent.get_full_name(), set()).add(cls) - - all_handlers = list(self.event_handlers.values()) - new_handlers: dict[str, RegisteredEventHandler] = {} - for reg in all_handlers: - full_name = format_event_handler(reg.handler) - existing_handler = new_handlers.get(full_name) - if existing_handler is not None and existing_handler is not reg: - console.warn( - f"Two event handlers resolve to the same full name " - f"{full_name!r}. The first one will be unreachable in " - "the registry. Check minify.json for duplicate ids." - ) - new_handlers[full_name] = reg + new_handlers = _rekey( + self.event_handlers.values(), + lambda r: format_event_handler(r.handler), + "event handler", + ) self.base_states.clear() self.base_states.update(new_base_states) diff --git a/reflex/minify.py b/reflex/minify.py index 647c33e5b4c..72eec2d56b1 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -12,6 +12,7 @@ import dataclasses import functools import json +from collections.abc import Iterable from pathlib import Path from typing import TYPE_CHECKING, TypedDict @@ -122,67 +123,38 @@ def is_minify_enabled() -> bool: @functools.cache -def is_state_minify_enabled() -> bool: - """Whether state-id minification is enabled. - - Returns: - ``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists. - """ - from reflex.environment import MinifyMode, environment - - return ( - environment.REFLEX_MINIFY_STATES.get() == MinifyMode.ENABLED - and get_minify_config() is not None - ) - +def _is_mode_enabled(env_var_name: str) -> bool: + """Whether the given ``REFLEX_MINIFY_*`` env var is on and a config exists. -@functools.cache -def is_event_minify_enabled() -> bool: - """Whether event-id minification is enabled. + Args: + env_var_name: The env-var attribute name on + :class:`~reflex.environment.EnvironmentVariables`. Returns: - ``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists. + ``True`` if the env var is ``ENABLED`` and ``minify.json`` exists. """ from reflex.environment import MinifyMode, environment - return ( - environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED - and get_minify_config() is not None - ) + env_var = getattr(environment, env_var_name) + return env_var.get() == MinifyMode.ENABLED and get_minify_config() is not None -def get_state_id(state_full_path: str) -> str | None: - """Look up the minified id for a state path. - - Args: - state_full_path: e.g. ``"myapp.state.AppState.UserState"``. +def is_state_minify_enabled() -> bool: + """Whether state-id minification is enabled. Returns: - The minified id, or ``None`` if not configured. + ``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists. """ - config = get_minify_config() - if config is None: - return None - return config["states"].get(state_full_path) + return _is_mode_enabled("REFLEX_MINIFY_STATES") -def get_event_id(state_full_path: str, handler_name: str) -> str | None: - """Look up the minified id for an event handler. - - Args: - state_full_path: The full path to the state. - handler_name: The handler's original name. +def is_event_minify_enabled() -> bool: + """Whether event-id minification is enabled. Returns: - The minified id, or ``None`` if not configured. + ``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists. """ - config = get_minify_config() - if config is None: - return None - state_events = config["events"].get(state_full_path) - if state_events is None: - return None - return state_events.get(handler_name) + return _is_mode_enabled("REFLEX_MINIFY_EVENTS") def save_minify_config(config: MinifyConfig) -> None: @@ -245,8 +217,22 @@ def from_disk(cls) -> MinifyNameResolver: events_enabled=environment.REFLEX_MINIFY_EVENTS.get() == MinifyMode.ENABLED, ) + def _is_minify_allowed(self, state_cls: type[BaseState], enabled: bool) -> bool: + """Whether minification applies to ``state_cls`` for the given mode. + + Args: + state_cls: The state class being resolved. + enabled: Whether the relevant ``REFLEX_MINIFY_*`` env var is on. + + Returns: + ``True`` when the env var is on and ``state_cls`` is user-defined. + """ + return enabled and not _is_framework_state(state_cls) + def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: D102 - if not (self.states_enabled and self.config) or _is_framework_state(state_cls): + if self.config is None or not self._is_minify_allowed( + state_cls, self.states_enabled + ): return None cached = self._state_cache.get(state_cls) if cached is not None: @@ -259,7 +245,9 @@ def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: def resolve_handler_name( # noqa: D102 self, state_cls: type[BaseState], handler_name: str ) -> str | None: - if not (self.events_enabled and self.config) or _is_framework_state(state_cls): + if self.config is None or not self._is_minify_allowed( + state_cls, self.events_enabled + ): return None per_state = self._event_cache.get(state_cls) if per_state is None: @@ -329,8 +317,7 @@ def clear_config_cache() -> None: ``REFLEX_MINIFY_*`` env vars at runtime. """ get_minify_config.cache_clear() - is_state_minify_enabled.cache_clear() - is_event_minify_enabled.cache_clear() + _is_mode_enabled.cache_clear() install_minify_resolver() @@ -484,6 +471,51 @@ def generate_minify_config( ) +def _find_duplicate_ids(items: Iterable[tuple[str, str]]) -> dict[str, list[str]]: + """Group ``(label, minified_id)`` pairs by id, keeping only collisions. + + Args: + items: Pairs of ``(label, minified_id)``. + + Returns: + Mapping from minified id to the labels sharing it (always ``len >= 2``). + """ + by_id: dict[str, list[str]] = {} + for label, mid in items: + by_id.setdefault(mid, []).append(label) + return {mid: labels for mid, labels in by_id.items() if len(labels) > 1} + + +def _assign_next_ids( + new_keys: Iterable[str], + existing_ids: set[int], + reassign_deleted: bool, +) -> dict[str, str]: + """Assign minified ids to ``new_keys`` while skipping ``existing_ids``. + + Mutates ``existing_ids`` so callers can chain assignments against the same + pool. Keys are sorted for deterministic output. + + Args: + new_keys: Keys needing new ids. + existing_ids: Already-used integer ids in the same scope. + reassign_deleted: When ``True``, scan from 0 (filling gaps); + otherwise start past the current max. + + Returns: + Mapping from key to its newly-assigned minified id. + """ + next_id = 0 if reassign_deleted else max(existing_ids, default=-1) + 1 + out: dict[str, str] = {} + for key in sorted(new_keys): + while next_id in existing_ids: + next_id += 1 + out[key] = int_to_minified_name(next_id) + existing_ids.add(next_id) + next_id += 1 + return out + + def validate_minify_config( config: MinifyConfig, root_state: type[BaseState] | None = None, @@ -506,40 +538,27 @@ def validate_minify_config( all_states = collect_all_states(root_state) - # Check for duplicate state IDs among siblings. - # Group by actual parent class (not string-split path) since children of - # the same parent can be defined in different modules. + # Group sibling states by their actual parent class (not by string-split + # path) since children of the same parent can live in different modules. path_to_cls = {get_state_full_path(s): s for s in all_states} - parent_cls_to_state_ids: dict[type[BaseState] | None, dict[str, list[str]]] = {} + parent_to_pairs: dict[type[BaseState] | None, list[tuple[str, str]]] = {} for state_path, minified_name in config["states"].items(): state_cls = path_to_cls.get(state_path) - parent_cls = state_cls.get_parent_state() if state_cls else None - parent_cls_to_state_ids.setdefault(parent_cls, {}).setdefault( - minified_name, [] - ).append(state_path) - - for parent_cls, id_to_states in parent_cls_to_state_ids.items(): - for minified_name, state_paths in id_to_states.items(): - if len(state_paths) > 1: - parent_name = parent_cls.__name__ if parent_cls else "root" - errors.append( - f"Duplicate state_id='{minified_name}' under '{parent_name}': " - f"{state_paths}" - ) - - # Check for duplicate event IDs within same state + parent = state_cls.get_parent_state() if state_cls else None + parent_to_pairs.setdefault(parent, []).append((state_path, minified_name)) + + for parent, pairs in parent_to_pairs.items(): + parent_name = parent.__name__ if parent else "root" + errors.extend( + f"Duplicate state_id='{mid}' under '{parent_name}': {paths}" + for mid, paths in _find_duplicate_ids(pairs).items() + ) + for state_path, state_events in config["events"].items(): - id_to_handlers: dict[str, list[str]] = {} - for handler_name, minified_name in state_events.items(): - if minified_name not in id_to_handlers: - id_to_handlers[minified_name] = [] - id_to_handlers[minified_name].append(handler_name) - - for minified_name, handler_names in id_to_handlers.items(): - if len(handler_names) > 1: - errors.append( - f"Duplicate event_id='{minified_name}' in '{state_path}': {handler_names}" - ) + errors.extend( + f"Duplicate event_id='{mid}' in '{state_path}': {handlers}" + for mid, handlers in _find_duplicate_ids(state_events.items()).items() + ) # Check for missing states (in code but not in config) code_state_paths = {get_state_full_path(s) for s in all_states} @@ -652,39 +671,21 @@ def sync_minify_config( parent = state_cls.get_parent_state() parent_cls_to_new_children.setdefault(parent, []).append(state_path) - # Assign new state IDs (unique among siblings of the same parent class) + # Assign new state IDs (unique among siblings of the same parent class). for parent_cls, children in parent_cls_to_new_children.items(): existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()).copy() + new_states.update(_assign_next_ids(children, existing_ids, reassign_deleted)) - # Assign IDs starting from max + 1 (or 0 if reassign_deleted and gaps exist) - next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1) - - for state_path in sorted(children): - while next_id in existing_ids: - next_id += 1 - new_states[state_path] = int_to_minified_name(next_id) - existing_ids.add(next_id) - next_id += 1 - - # Find events that need IDs assigned + # Assign new event IDs (unique within each state). for state_cls in all_states: state_path = get_state_full_path(state_cls) state_events = new_events.get(state_path, {}) new_handlers = [h for h in state_cls.event_handlers if h not in state_events] - if new_handlers: - # Get existing IDs for this state's events existing_ids = {minified_name_to_int(eid) for eid in state_events.values()} - - next_id = 0 if reassign_deleted else (max(existing_ids, default=-1) + 1) - - for handler_name in sorted(new_handlers): - while next_id in existing_ids: - next_id += 1 - state_events[handler_name] = int_to_minified_name(next_id) - existing_ids.add(next_id) - next_id += 1 - + state_events.update( + _assign_next_ids(new_handlers, existing_ids, reassign_deleted) + ) new_events[state_path] = state_events return MinifyConfig( diff --git a/reflex/reflex.py b/reflex/reflex.py index a88704960be..6f382a8cf3f 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -4,7 +4,7 @@ from importlib.util import find_spec from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, overload import click from reflex_base import constants @@ -926,13 +926,44 @@ def _count_events(config: MinifyConfig) -> int: return sum(len(handlers) for handlers in config["events"].values()) -def _load_existing_minify_config() -> MinifyConfig: - """Load ``minify.json`` or exit the CLI with an error. +@overload +def _open_minify_session(*, require_exists: Literal[True] = True) -> MinifyConfig: ... +@overload +def _open_minify_session(*, require_exists: Literal[False]) -> MinifyConfig | None: ... + + +def _open_minify_session(*, require_exists: bool = True) -> MinifyConfig | None: + """Run the standard minify-CLI prelude. + + Compiles the user's app (so dynamic states register) and loads + ``minify.json``. Exits the CLI with an error when the file is required + but missing, or when it exists but is malformed. + + Args: + require_exists: When ``True`` (default), missing ``minify.json`` is + a fatal error. When ``False``, returns ``None`` instead. Returns: - The parsed minify configuration. + The parsed config, or ``None`` if ``require_exists`` is ``False`` and + the file is absent. """ - from reflex.minify import MINIFY_JSON, _load_minify_config_uncached + from reflex.minify import ( + MINIFY_JSON, + _get_minify_json_path, + _load_minify_config_uncached, + ) + + exists = _get_minify_json_path().exists() + if require_exists and not exists: + console.error( + f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." + ) + raise SystemExit(1) + + _load_app_for_minify() + + if not exists: + return None config = _load_minify_config_uncached() if config is None: @@ -986,22 +1017,9 @@ def minify_sync(reassign_deleted: bool, prune: bool): Adds new states and events, optionally removes orphaned entries. """ - from reflex.minify import ( - MINIFY_JSON, - _get_minify_json_path, - save_minify_config, - sync_minify_config, - ) - - if not _get_minify_json_path().exists(): - console.error( - f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." - ) - raise SystemExit(1) - - _load_app_for_minify() - existing_config = _load_existing_minify_config() + from reflex.minify import MINIFY_JSON, save_minify_config, sync_minify_config + existing_config = _open_minify_session() new_config = sync_minify_config( existing_config, reassign_deleted=reassign_deleted, prune=prune ) @@ -1023,17 +1041,9 @@ def minify_validate(): Checks for duplicate IDs, missing entries, and orphaned entries. """ - from reflex.minify import MINIFY_JSON, _get_minify_json_path, validate_minify_config - - if not _get_minify_json_path().exists(): - console.error( - f"{MINIFY_JSON} does not exist. Use 'reflex minify init' to create it." - ) - raise SystemExit(1) - - _load_app_for_minify() - config = _load_existing_minify_config() + from reflex.minify import MINIFY_JSON, validate_minify_config + config = _open_minify_session() errors, warnings, missing = validate_minify_config(config) if errors: @@ -1068,12 +1078,7 @@ def minify_list(output_json: bool): """Print the state tree with IDs and minified names.""" from typing import TypedDict - from reflex.minify import ( - get_event_id, - get_minify_config, - get_state_full_path, - get_state_id, - ) + from reflex.minify import get_state_full_path from reflex.state import BaseState, State class EventHandlerData(TypedDict): @@ -1091,10 +1096,10 @@ class StateTreeData(TypedDict): event_handlers: list[EventHandlerData] substates: list[StateTreeData] - _load_app_for_minify() - # CLI inspection shows config contents regardless of env var settings. - minify_enabled = get_minify_config() is not None + config = _open_minify_session(require_exists=False) + states_map = config["states"] if config else {} + events_map = config["events"] if config else {} def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: """Recursively build state tree data. @@ -1106,20 +1111,13 @@ def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: A dictionary containing the state tree data. """ state_path = get_state_full_path(state_cls) - # state_id is now the minified name directly (a string like "a", "ba") - state_id = get_state_id(state_path) if minify_enabled else None - - # Build event handlers list - handlers = [] - for handler_name in sorted(state_cls.event_handlers.keys()): - # event_id is now the minified name directly (a string like "a", "ba") - event_id = ( - get_event_id(state_path, handler_name) if minify_enabled else None - ) - handlers.append({ - "name": handler_name, - "event_id": event_id, - }) + state_id = states_map.get(state_path) + + handler_ids = events_map.get(state_path, {}) + handlers: list[EventHandlerData] = [ + {"name": handler_name, "event_id": handler_ids.get(handler_name)} + for handler_name in sorted(state_cls.event_handlers.keys()) + ] # Build substates recursively substates = [ @@ -1190,7 +1188,7 @@ def print_state_tree( console.log(json.dumps(tree_data, indent=2)) else: - if minify_enabled: + if config is not None: console.log("State Tree (minify.json loaded)") else: console.log("State Tree (no minify.json)") @@ -1211,17 +1209,10 @@ def minify_lookup(output_json: bool, minified_path: str): Walks the state tree from the root to resolve each segment. """ - from reflex.minify import MINIFY_JSON, get_minify_config, get_state_full_path + from reflex.minify import get_state_full_path from reflex.state import BaseState, State - _load_app_for_minify() - - config = get_minify_config() - if config is None: - console.error( - f"{MINIFY_JSON} not found. Run 'reflex minify init' to create it." - ) - raise SystemExit(1) + config = _open_minify_session() def collect_states( state_cls: type[BaseState], diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index f8e7437e612..6693abdd239 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -15,9 +15,8 @@ MinifyNameResolver, clear_config_cache, generate_minify_config, - get_event_id, + get_minify_config, get_state_full_path, - get_state_id, int_to_minified_name, is_event_minify_enabled, is_minify_enabled, @@ -149,8 +148,7 @@ class TestMinifyConfig: def test_no_config_returns_none(self, temp_minify_json): """Test that missing minify.json returns None.""" assert is_minify_enabled() is False - assert get_state_id("any.path") is None - assert get_event_id("any.path", "handler") is None + assert get_minify_config() is None def test_save_and_load_config(self, temp_minify_json, monkeypatch): """Test saving and loading a config.""" @@ -171,8 +169,10 @@ def test_save_and_load_config(self, temp_minify_json, monkeypatch): clear_config_cache() assert is_minify_enabled() is True - assert get_state_id("test.module.MyState") == "a" - assert get_event_id("test.module.MyState", "handler") == "a" + loaded = get_minify_config() + assert loaded is not None + assert loaded["states"]["test.module.MyState"] == "a" + assert loaded["events"]["test.module.MyState"]["handler"] == "a" def test_invalid_version_raises(self, temp_minify_json, monkeypatch): """Test that invalid version raises ValueError.""" @@ -1134,7 +1134,7 @@ def test_lookup_fails_without_minify_json(self, temp_minify_json, monkeypatch): result = runner.invoke(cli, ["minify", "lookup", "a.b"]) assert result.exit_code == 1 - assert "minify.json not found" in result.output + assert "minify.json does not exist" in result.output def test_lookup_fails_for_invalid_path(self, temp_minify_json, monkeypatch): """Test that lookup fails for non-existent minified path.""" From de1804ad3b60cc65644c7a3bdeca661e31e93761 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 16:45:40 +0200 Subject: [PATCH 63/74] dry up the tests --- tests/units/test_minification.py | 679 ++++++++++--------------------- 1 file changed, 204 insertions(+), 475 deletions(-) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 6693abdd239..516862e0333 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -2,9 +2,13 @@ from __future__ import annotations +import contextlib import json +from collections.abc import Iterator +from unittest import mock import pytest +from click.testing import CliRunner from reflex_base.registry import DefaultNameResolver, NameResolver, RegistrationContext from reflex.environment import MinifyMode, environment @@ -44,6 +48,91 @@ def _resolved_event_id(state_cls: type[BaseState], handler_name: str) -> str | N ) +def _set_minify_modes( + monkeypatch: pytest.MonkeyPatch, + *, + states: MinifyMode | None = None, + events: MinifyMode | None = None, +) -> None: + """Set ``REFLEX_MINIFY_*`` env vars; ``None`` leaves the var unchanged. + + Args: + monkeypatch: The pytest monkeypatch fixture. + states: Mode for ``REFLEX_MINIFY_STATES``. + events: Mode for ``REFLEX_MINIFY_EVENTS``. + """ + if states is not None: + monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, states.value) + if events is not None: + monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, events.value) + + +def _install_config( + states: dict[str, str] | None = None, + events: dict[str, dict[str, str]] | None = None, + *, + include_state_root: bool = False, +) -> MinifyConfig: + """Build, save, and activate a ``minify.json`` in one call. + + Calls ``clear_config_cache()`` afterward — that re-installs the resolver + and clears every per-class lru_cache, so tests don't need to call + ``State.get_name.cache_clear()`` etc. by hand. + + Args: + states: ``state_path -> minified_id`` map. + events: ``state_path -> {handler -> minified_id}`` map. + include_state_root: Add ``"reflex.state.State": "a"`` so subclasses + of ``State`` resolve through the root entry. + + Returns: + The saved config. + """ + states_map = dict(states or {}) + if include_state_root: + states_map.setdefault("reflex.state.State", "a") + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": states_map, + "events": events or {}, + } + save_minify_config(config) + clear_config_cache() + return config + + +@contextlib.contextmanager +def _temporary_resolver(resolver: NameResolver) -> Iterator[RegistrationContext]: + """Install ``resolver`` for the duration of the ``with`` block. + + Args: + resolver: The resolver to install temporarily. + + Yields: + The active registration context. + """ + ctx = RegistrationContext.get() + original = ctx.name_resolver + try: + ctx.set_name_resolver(resolver) + yield ctx + finally: + ctx.set_name_resolver(original) + + +@pytest.fixture +def cli_runner(monkeypatch: pytest.MonkeyPatch) -> CliRunner: + """Click runner with ``prerequisites.get_compiled_app`` stubbed out. + + Returns: + A ``CliRunner`` ready to invoke ``reflex.reflex.cli`` commands. + """ + from reflex.utils import prerequisites + + monkeypatch.setattr(prerequisites, "get_compiled_app", lambda *a, **kw: mock.Mock()) + return CliRunner() + + class TestIntToMinifiedName: """Tests for int_to_minified_name function.""" @@ -152,21 +241,13 @@ def test_no_config_returns_none(self, temp_minify_json): def test_save_and_load_config(self, temp_minify_json, monkeypatch): """Test saving and loading a config.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + _set_minify_modes( + monkeypatch, states=MinifyMode.ENABLED, events=MinifyMode.ENABLED ) - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _install_config( + states={"test.module.MyState": "a"}, + events={"test.module.MyState": {"handler": "a"}}, ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {"test.module.MyState": "a"}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - - # Clear cache and reload - clear_config_cache() assert is_minify_enabled() is True loaded = get_minify_config() @@ -176,9 +257,7 @@ def test_save_and_load_config(self, temp_minify_json, monkeypatch): def test_invalid_version_raises(self, temp_minify_json, monkeypatch): """Test that invalid version raises ValueError.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) config = {"version": 999, "states": {}, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -191,9 +270,7 @@ def test_invalid_version_raises(self, temp_minify_json, monkeypatch): def test_missing_states_raises(self, temp_minify_json, monkeypatch): """Test that missing 'states' key raises ValueError.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) config = {"version": SCHEMA_VERSION, "events": {}} path = temp_minify_json / MINIFY_JSON with path.open("w") as f: @@ -417,60 +494,32 @@ def test_state_uses_full_name_without_config(self, temp_minify_json): class TestState(BaseState): pass - TestState.get_name.cache_clear() - name = TestState.get_name() - # Should be the full name (snake_case module___class) - assert "test_state" in name.lower() + assert "test_state" in TestState.get_name().lower() def test_state_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): """Test that states use minified names when minify.json exists and env var is enabled.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) class TestState(BaseState): pass - state_path = get_state_full_path(TestState) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {state_path: "f"}, # Direct minified name - "events": {}, - } - save_minify_config(config) - clear_config_cache() - TestState.get_name.cache_clear() - - name = TestState.get_name() + _install_config(states={get_state_full_path(TestState): "f"}) - # Should be the minified name directly - assert name == "f" + assert TestState.get_name() == "f" def test_state_uses_full_name_when_env_disabled( self, temp_minify_json, monkeypatch ): """Test that states use full names when env var is disabled even with minify.json.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.DISABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.DISABLED) class TestState(BaseState): pass - state_path = get_state_full_path(TestState) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {state_path: "f"}, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - TestState.get_name.cache_clear() + _install_config(states={get_state_full_path(TestState): "f"}) name = TestState.get_name() - - # Should be the full name, not minified assert name != "f" assert "test_state" in name.lower() @@ -488,11 +537,8 @@ class TestState(BaseState): def my_handler(self): pass - TestState.get_name.cache_clear() handler = TestState.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) - - # Should use full name assert event_name == "my_handler" def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): @@ -500,62 +546,26 @@ def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatc import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) + # The config must exist before the class is defined: registration + # captures the resolved name once during __init_subclass__. + state_path = "tests.units.test_minification.State.TestStateWithMinifiedEvent" + _install_config( + states={state_path: "b"}, + events={state_path: {"my_handler": "d"}}, + include_state_root=True, ) - # First, set up the config BEFORE creating the state class - # The event_id_to_name registry is built during __init_subclass__ - # so the config must exist before the class is defined - - # For this test, we extend State (not BaseState) so that - # get_event_handler_parts can look up our state in the State tree. - # We need to include State's full path in our config too. - - # The state path includes the full class hierarchy from State. - # For a direct subclass of State defined in this test module, - # get_state_full_path returns: "tests.units.test_minification.State.TestStateWithMinifiedEvent" - # (module + class hierarchy from root state to leaf) - - expected_module = "tests.units.test_minification" - expected_state_path = f"{expected_module}.State.TestStateWithMinifiedEvent" - - # Also need to include the base State in the config (v2 format with nested events) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", # Base State - expected_state_path: "b", # Our test state - }, - "events": { - expected_state_path: {"my_handler": "d"}, # Nested under state path - }, - } - save_minify_config(config) - clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() - - # Now create the state class extending State - it will pick up the config class TestStateWithMinifiedEvent(State): @rx.event def my_handler(self): pass - # Verify the path matches what we expected - actual_path = get_state_full_path(TestStateWithMinifiedEvent) - assert actual_path == expected_state_path, ( - f"Expected path {expected_state_path}, got {actual_path}" - ) - - # The active resolver should map this handler to its minified name. + assert get_state_full_path(TestStateWithMinifiedEvent) == state_path assert _resolved_event_id(TestStateWithMinifiedEvent, "my_handler") == "d" handler = TestStateWithMinifiedEvent.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) - - # Should be the minified name directly assert event_name == "d" def test_event_uses_full_name_when_env_disabled( @@ -565,45 +575,27 @@ def test_event_uses_full_name_when_env_disabled( import reflex as rx from reflex.utils.format import get_event_handler_parts - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.DISABLED.value + _set_minify_modes(monkeypatch, events=MinifyMode.DISABLED) + state_path = ( + "tests.units.test_minification.State.TestStateWithMinifiedEventDisabled" ) - - expected_module = "tests.units.test_minification" - expected_state_path = ( - f"{expected_module}.State.TestStateWithMinifiedEventDisabled" + _install_config( + states={state_path: "b"}, + events={state_path: {"my_handler": "d"}}, + include_state_root=True, ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - expected_state_path: "b", - }, - "events": { - expected_state_path: {"my_handler": "d"}, - }, - } - save_minify_config(config) - clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() - class TestStateWithMinifiedEventDisabled(State): @rx.event def my_handler(self): pass - # When env var is disabled, the resolver yields no minified name. assert ( _resolved_event_id(TestStateWithMinifiedEventDisabled, "my_handler") is None ) handler = TestStateWithMinifiedEventDisabled.event_handlers["my_handler"] _, event_name = get_event_handler_parts(handler) - - # Should use full name assert event_name == "my_handler" @@ -612,41 +604,24 @@ class TestDynamicHandlerMinification: def test_setvar_registered_with_config(self, temp_minify_json, monkeypatch): """Test that ``setvar`` is resolvable to its minified name.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) + state_path = "tests.units.test_minification.State.TestStateWithSetvar" + _install_config( + states={state_path: "b"}, + events={state_path: {"setvar": "s"}}, + include_state_root=True, ) - expected_module = "tests.units.test_minification" - expected_state_path = f"{expected_module}.State.TestStateWithSetvar" - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - expected_state_path: "b", - }, - "events": { - expected_state_path: {"setvar": "s"}, - }, - } - save_minify_config(config) - clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() class TestStateWithSetvar(State): pass - # The active resolver should report setvar's minified id. assert _resolved_event_id(TestStateWithSetvar, "setvar") == "s" def test_auto_setter_registered_with_config(self, temp_minify_json, monkeypatch): """Test that auto-setters (set_*) are resolvable to their minified name.""" from reflex_base import config as base_config - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) # state_auto_setters is False by default; force it on so that # `_init_var` actually creates the setter we want to verify. real_get_config = base_config.get_config @@ -657,39 +632,24 @@ def _mock_get_config(*args, **kwargs): return cfg monkeypatch.setattr(base_config, "get_config", _mock_get_config) - expected_module = "tests.units.test_minification" - expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter" - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - expected_state_path: "b", - }, - "events": { - expected_state_path: {"set_count": "c", "setvar": "v"}, - }, - } - save_minify_config(config) - clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() + state_path = "tests.units.test_minification.State.TestStateWithAutoSetter" + _install_config( + states={state_path: "b"}, + events={state_path: {"set_count": "c", "setvar": "v"}}, + include_state_root=True, + ) class TestStateWithAutoSetter(State): count: int = 0 - # The auto-setter should resolve to the minified id from the config. assert _resolved_event_id(TestStateWithAutoSetter, "set_count") == "c" def test_dynamic_handlers_not_registered_without_config(self, temp_minify_json): """Test that dynamic handlers have no resolved minified name without config.""" - # No config saved - temp_minify_json fixture ensures clean state class TestStateNoConfig(State): count: int = 0 - # Without config, the resolver returns None for every handler. for handler_name in TestStateNoConfig.event_handlers: assert _resolved_event_id(TestStateNoConfig, handler_name) is None @@ -699,32 +659,17 @@ def test_add_event_handler_registered_with_config( """Test that dynamically added event handlers via _add_event_handler are registered.""" import reflex as rx - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) + state_path = "tests.units.test_minification.State.TestStateWithDynamicHandler" + _install_config( + states={state_path: "b"}, + events={state_path: {"dynamic_handler": "d", "setvar": "v"}}, + include_state_root=True, ) - expected_module = "tests.units.test_minification" - expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler" - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - expected_state_path: "b", - }, - "events": { - expected_state_path: {"dynamic_handler": "d", "setvar": "v"}, - }, - } - save_minify_config(config) - clear_config_cache() - State.get_name.cache_clear() - State.get_full_name.cache_clear() - State.get_class_substate.cache_clear() class TestStateWithDynamicHandler(State): pass - # Dynamically add an event handler after class creation @rx.event def dynamic_handler(self): pass @@ -733,8 +678,6 @@ def dynamic_handler(self): "dynamic_handler", dynamic_handler ) - # Dynamically-added handlers are resolved through the same resolver, - # so the new name picks up the minified id without re-registering. assert _resolved_event_id(TestStateWithDynamicHandler, "dynamic_handler") == "d" def test_component_state_picks_up_minified_name( @@ -748,13 +691,9 @@ def test_component_state_picks_up_minified_name( """ import reflex as rx - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _set_minify_modes( + monkeypatch, states=MinifyMode.ENABLED, events=MinifyMode.ENABLED ) - # ComponentState.create() builds a new class via ``type(...)`` with # ``__module__ = "reflex.istate.dynamic"`` and a ``_n`` suffix, # so the path under which the resolver will look it up is fully @@ -763,19 +702,11 @@ def test_component_state_picks_up_minified_name( instance_path = ( f"reflex.istate.dynamic.State.ComponentStateMinifyExample_n{instance_count}" ) - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - instance_path: "z", - }, - "events": { - instance_path: {"increment": "i", "setvar": "s"}, - }, - } - save_minify_config(config) - clear_config_cache() + _install_config( + states={instance_path: "z"}, + events={instance_path: {"increment": "i", "setvar": "s"}}, + include_state_root=True, + ) class ComponentStateMinifyExample(rx.ComponentState): count: int = 0 @@ -809,22 +740,13 @@ def test_state_created_after_resolver_install_uses_minified_name( exist when ``minify.json`` is loaded, so the resolver must be consulted lazily on first lookup. """ - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) late_path = "tests.units.test_minification.State.LateBornState" - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {late_path: "lb"}, - "events": {}, - } - save_minify_config(config) - clear_config_cache() # installs the MinifyNameResolver + _install_config(states={late_path: "lb"}) class LateBornState(State): pass - # Newly-registered class is keyed by its minified name in the registry. ctx = RegistrationContext.get() assert LateBornState.get_name() == "lb" # State stays un-minified, so the parent prefix is its default snake form. @@ -836,96 +758,51 @@ class TestMinifyModeEnvVars: def test_state_minify_disabled_by_default(self, temp_minify_json): """Test that state minification is disabled by default.""" - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {"test.module.MyState": "a"}, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - + _install_config(states={"test.module.MyState": "a"}) assert is_state_minify_enabled() is False def test_event_minify_disabled_by_default(self, temp_minify_json): """Test that event minification is disabled by default.""" - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - clear_config_cache() - + _install_config(events={"test.module.MyState": {"handler": "a"}}) assert is_event_minify_enabled() is False def test_state_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that state minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {"test.module.MyState": "a"}, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) + _install_config(states={"test.module.MyState": "a"}) assert is_state_minify_enabled() is True def test_event_minify_enabled_with_env_and_config( self, temp_minify_json, monkeypatch ): """Test that event minification is enabled when env var is enabled and config exists.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value - ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - clear_config_cache() - + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) + _install_config(events={"test.module.MyState": {"handler": "a"}}) assert is_event_minify_enabled() is True def test_state_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that state minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) clear_config_cache() - assert is_state_minify_enabled() is False def test_event_minify_disabled_without_config(self, temp_minify_json, monkeypatch): """Test that event minification is disabled when env var is enabled but no config exists.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) clear_config_cache() - assert is_event_minify_enabled() is False def test_independent_state_and_event_toggles(self, temp_minify_json, monkeypatch): """Test that state and event minification can be toggled independently.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + _set_minify_modes( + monkeypatch, states=MinifyMode.ENABLED, events=MinifyMode.DISABLED ) - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.DISABLED.value + _install_config( + states={"test.module.MyState": "a"}, + events={"test.module.MyState": {"handler": "a"}}, ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {"test.module.MyState": "a"}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - clear_config_cache() - assert is_state_minify_enabled() is True assert is_event_minify_enabled() is False assert is_minify_enabled() is True @@ -934,32 +811,18 @@ def test_is_minify_enabled_true_when_either_enabled( self, temp_minify_json, monkeypatch ): """Test that is_minify_enabled returns True when either state or event is enabled.""" - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.DISABLED.value - ) - monkeypatch.setenv( - environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + _set_minify_modes( + monkeypatch, states=MinifyMode.DISABLED, events=MinifyMode.ENABLED ) - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - clear_config_cache() - + _install_config(events={"test.module.MyState": {"handler": "a"}}) assert is_minify_enabled() is True def test_is_minify_enabled_false_when_both_disabled(self, temp_minify_json): """Test that is_minify_enabled returns False when both are disabled (default).""" - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": {"test.module.MyState": "a"}, - "events": {"test.module.MyState": {"handler": "a"}}, - } - save_minify_config(config) - clear_config_cache() - + _install_config( + states={"test.module.MyState": "a"}, + events={"test.module.MyState": {"handler": "a"}}, + ) assert is_minify_enabled() is False @@ -972,46 +835,30 @@ def test_get_class_substate_with_parent_child_name_collision( """Test that get_class_substate resolves correctly when parent and child share the same minified name (IDs are only sibling-unique). """ - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) - # Build a hierarchy: State -> ParentClassSubstateCollision -> - # ChildClassSubstateCollision where both children minify to "b". - # Class names are deliberately unique so ``_handle_local_def`` does - # not append a numeric suffix when run alongside other tests that - # might also define a ``ParentState``. + # Build State -> ParentClassSubstateCollision -> ChildClassSubstateCollision + # where both children minify to "b". Class names are deliberately unique + # so ``_handle_local_def`` doesn't append a numeric suffix. class ParentClassSubstateCollision(State): pass class ChildClassSubstateCollision(ParentClassSubstateCollision): pass - parent_path = get_state_full_path(ParentClassSubstateCollision) - child_path = get_state_full_path(ChildClassSubstateCollision) - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - parent_path: "b", - child_path: "b", # Same minified name as parent - }, - "events": {}, - } - save_minify_config(config) - # ``clear_config_cache`` resets the per-class name caches and re-keys - # the registry under the new minified names. - clear_config_cache() + _install_config( + states={ + get_state_full_path(ParentClassSubstateCollision): "b", + get_state_full_path(ChildClassSubstateCollision): "b", + } + ) - # Verify both get the same minified name assert ParentClassSubstateCollision.get_name() == "b" assert ChildClassSubstateCollision.get_name() == "b" - # Full path is ``State.b.b`` (State stays un-minified). state_prefix = State.get_full_name() assert ChildClassSubstateCollision.get_full_name() == f"{state_prefix}.b.b" - # get_class_substate should resolve .b.b to the child, not parent. resolved = State.get_class_substate(f"{state_prefix}.b.b") assert resolved is ChildClassSubstateCollision @@ -1023,13 +870,8 @@ def test_get_substate_with_parent_child_name_collision( """ import reflex as rx - monkeypatch.setenv( - environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value - ) + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) - # Class names are deliberately unique so ``_handle_local_def`` does - # not append a numeric suffix when run alongside other tests that - # might also define a ``ParentState2``. class ParentInstanceSubstateCollision(State): pass @@ -1038,26 +880,15 @@ class ChildInstanceSubstateCollision(ParentInstanceSubstateCollision): def my_handler(self): pass - parent_path = get_state_full_path(ParentInstanceSubstateCollision) - child_path = get_state_full_path(ChildInstanceSubstateCollision) - - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - parent_path: "b", - child_path: "b", # Same minified name as parent - }, - "events": {}, - } - save_minify_config(config) - # ``clear_config_cache`` resets the per-class name caches and re-keys - # the registry under the new minified names. - clear_config_cache() + _install_config( + states={ + get_state_full_path(ParentInstanceSubstateCollision): "b", + get_state_full_path(ChildInstanceSubstateCollision): "b", + } + ) - # Create a state instance tree root = State(_reflex_internal_init=True) # type: ignore[call-arg] - # Instance get_substate should resolve .b.b to the collision child. resolved = root.get_substate([State.get_name(), "b", "b"]) assert type(resolved) is ChildInstanceSubstateCollision @@ -1065,148 +896,66 @@ def my_handler(self): class TestMinifyLookupCLI: """Tests for the 'reflex minify lookup' CLI command.""" - def test_lookup_resolves_minified_path(self, temp_minify_json, monkeypatch): + def test_lookup_resolves_minified_path(self, temp_minify_json, cli_runner): """Test that lookup resolves a minified path to full state info.""" - from unittest import mock - - from click.testing import CliRunner - from reflex.reflex import cli - from reflex.utils import prerequisites - # Create test states class AppState(State): pass class ChildState(AppState): pass - app_state_path = get_state_full_path(AppState) - child_state_path = get_state_full_path(ChildState) - - # Create minify.json with known mappings - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - app_state_path: "b", - child_state_path: "c", + _install_config( + states={ + get_state_full_path(AppState): "b", + get_state_full_path(ChildState): "c", }, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - - # Stub the compiled-app loader; the test already populates the registry - app_module_mock = mock.Mock() - monkeypatch.setattr( - prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + include_state_root=True, ) - runner = CliRunner() - result = runner.invoke(cli, ["minify", "lookup", "a.b.c"]) + result = cli_runner.invoke(cli, ["minify", "lookup", "a.b.c"]) assert result.exit_code == 0, result.output - # Output should include the module and class names assert "State" in result.output assert "AppState" in result.output assert "ChildState" in result.output - def test_lookup_fails_without_minify_json(self, temp_minify_json, monkeypatch): + def test_lookup_fails_without_minify_json(self, temp_minify_json, cli_runner): """Test that lookup fails gracefully when minify.json is missing.""" - from unittest import mock - - from click.testing import CliRunner - from reflex.reflex import cli - from reflex.utils import prerequisites - # Stub the compiled-app loader - app_module_mock = mock.Mock() - monkeypatch.setattr( - prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock - ) - - # Don't create minify.json clear_config_cache() - - runner = CliRunner() - result = runner.invoke(cli, ["minify", "lookup", "a.b"]) + result = cli_runner.invoke(cli, ["minify", "lookup", "a.b"]) assert result.exit_code == 1 assert "minify.json does not exist" in result.output - def test_lookup_fails_for_invalid_path(self, temp_minify_json, monkeypatch): + def test_lookup_fails_for_invalid_path(self, temp_minify_json, cli_runner): """Test that lookup fails for non-existent minified path.""" - from unittest import mock - - from click.testing import CliRunner - from reflex.reflex import cli - from reflex.utils import prerequisites - # Create minify.json with only root state - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - }, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - - # Stub the compiled-app loader - app_module_mock = mock.Mock() - monkeypatch.setattr( - prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock - ) - - runner = CliRunner() - # Try to lookup a path that doesn't exist - result = runner.invoke(cli, ["minify", "lookup", "a.xyz"]) + _install_config(include_state_root=True) + result = cli_runner.invoke(cli, ["minify", "lookup", "a.xyz"]) assert result.exit_code == 1 assert "No state found" in result.output - def test_lookup_with_json_output(self, temp_minify_json, monkeypatch): + def test_lookup_with_json_output(self, temp_minify_json, cli_runner): """Test that lookup with --json flag outputs valid JSON.""" - from unittest import mock - - from click.testing import CliRunner - from reflex.reflex import cli - from reflex.utils import prerequisites - # Create test state class JsonTestState(State): pass - state_path = get_state_full_path(JsonTestState) - - # Create minify.json - config: MinifyConfig = { - "version": SCHEMA_VERSION, - "states": { - "reflex.state.State": "a", - state_path: "b", - }, - "events": {}, - } - save_minify_config(config) - clear_config_cache() - - # Stub the compiled-app loader - app_module_mock = mock.Mock() - monkeypatch.setattr( - prerequisites, "get_compiled_app", lambda *a, **kw: app_module_mock + _install_config( + states={get_state_full_path(JsonTestState): "b"}, + include_state_root=True, ) - runner = CliRunner() - result = runner.invoke(cli, ["minify", "lookup", "--json", "a.b"]) + result = cli_runner.invoke(cli, ["minify", "lookup", "--json", "a.b"]) assert result.exit_code == 0, result.output - # Parse output as JSON to verify it's valid output_data = json.loads(result.output) assert isinstance(output_data, list) assert len(output_data) == 2 # Root state + JsonTestState @@ -1271,13 +1020,8 @@ def resolve_state_name(self, state_cls): def resolve_handler_name(self, state_cls, handler_name): return None - ctx = RegistrationContext.get() - original = ctx.name_resolver - try: - ctx.set_name_resolver(FixedStateNameResolver()) + with _temporary_resolver(FixedStateNameResolver()): assert State.get_name() == "fixed_name" - finally: - ctx.set_name_resolver(original) def test_set_name_resolver_propagates_through_format_event_handler(self): """Installing a custom resolver swaps the formatted handler name.""" @@ -1291,14 +1035,9 @@ def resolve_state_name(self, state_cls): def resolve_handler_name(self, state_cls, handler_name): return f"px_{handler_name}" - ctx = RegistrationContext.get() - original = ctx.name_resolver - try: - ctx.set_name_resolver(HandlerPrefixResolver()) + with _temporary_resolver(HandlerPrefixResolver()): formatted = format_event_handler(OnLoadInternalState.on_load_internal) # pyright: ignore[reportArgumentType] assert formatted.endswith(".px_on_load_internal") - finally: - ctx.set_name_resolver(original) def test_resolver_swap_clears_lru_caches(self): """``set_name_resolver`` invalidates per-class name caches so that @@ -1319,15 +1058,10 @@ def resolve_state_name(self, state_cls): def resolve_handler_name(self, state_cls, handler_name): return None - ctx = RegistrationContext.get() - original = ctx.name_resolver - try: - ctx.set_name_resolver(A()) + with _temporary_resolver(A()) as ctx: assert State.get_full_name() == "first" ctx.set_name_resolver(B()) assert State.get_full_name() == "second" - finally: - ctx.set_name_resolver(original) def test_chain_of_resolvers(self): """Resolvers compose with a tiny user-written chain wrapper.""" @@ -1359,13 +1093,8 @@ def resolve_state_name(self, state_cls): def resolve_handler_name(self, state_cls, handler_name): return None - ctx = RegistrationContext.get() - original = ctx.name_resolver - try: - ctx.set_name_resolver(Chain(FirstOnly(), DefaultNameResolver())) + with _temporary_resolver(Chain(FirstOnly(), DefaultNameResolver())): assert State.get_name() == "from_first" - finally: - ctx.set_name_resolver(original) class TestMinifyNameResolver: From c99b450eecd0edf0cc91eea1c1c4c8879f780bdf Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 18:48:35 +0200 Subject: [PATCH 64/74] typing improvements and cleanup --- .../src/reflex_base/compiler/templates.py | 1 - .../src/reflex_base/constants/compiler.py | 12 -- .../src/reflex_base/utils/format.py | 15 +- reflex/minify.py | 41 ++-- reflex/reflex.py | 60 ++---- reflex/state.py | 175 ++++++++++-------- 6 files changed, 142 insertions(+), 162 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/compiler/templates.py b/packages/reflex-base/src/reflex_base/compiler/templates.py index c66cabe567f..71858af1148 100644 --- a/packages/reflex-base/src/reflex_base/compiler/templates.py +++ b/packages/reflex-base/src/reflex_base/compiler/templates.py @@ -281,7 +281,6 @@ def context_template( UpdateVarsInternalState, ) - # Compute dynamic state names that respect minification settings main_state_name = State.get_name() on_load_internal = format_event_handler(OnLoadInternalState.on_load_internal) update_vars_internal = format_event_handler( diff --git a/packages/reflex-base/src/reflex_base/constants/compiler.py b/packages/reflex-base/src/reflex_base/constants/compiler.py index 8113f522484..8e26971fb7a 100644 --- a/packages/reflex-base/src/reflex_base/constants/compiler.py +++ b/packages/reflex-base/src/reflex_base/constants/compiler.py @@ -67,18 +67,6 @@ class CompileVars(SimpleNamespace): CONNECT_ERROR = "connectErrors" # The name of the function for converting a dict to an event. TO_EVENT = "ReflexEvent" - # The name of the internal on_load event. - ON_LOAD_INTERNAL = "reflex___state____on_load_internal_state.on_load_internal" - # The name of the internal event to update generic state vars. - UPDATE_VARS_INTERNAL = ( - "reflex___state____update_vars_internal_state.update_vars_internal" - ) - # The name of the frontend event exception state - FRONTEND_EXCEPTION_STATE = "reflex___state____frontend_event_exception_state" - # The full name of the frontend exception state - FRONTEND_EXCEPTION_STATE_FULL = ( - f"reflex___state____state.{FRONTEND_EXCEPTION_STATE}" - ) class PageNames(SimpleNamespace): diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index 78bd99ecc60..1e48ef247b2 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -6,7 +6,6 @@ import json import os import re -from collections.abc import Callable from typing import TYPE_CHECKING, Any from reflex_base import constants @@ -440,9 +439,7 @@ def format_props(*single_props, **key_value_props) -> list[str]: ] + [(f"...{LiteralVar.create(prop)!s}") for prop in single_props] -def get_event_handler_parts( - handler: EventHandler | Callable[..., Any], -) -> tuple[str, str]: +def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: """Get the (state, function) name pair for an event handler. Both names pass through the active @@ -454,17 +451,9 @@ def get_event_handler_parts( Returns: ``(state_full_name, handler_name)`` — both resolved. - - Raises: - TypeError: If the handler is not an EventHandler. """ - from reflex_base.event import EventHandler from reflex_base.registry import RegistrationContext - if not isinstance(handler, EventHandler): - msg = f"Expected EventHandler, got {type(handler)}" - raise TypeError(msg) - name = handler.fn.__qualname__ if handler.state is None: return ("", name) @@ -477,7 +466,7 @@ def get_event_handler_parts( return (state_full_name, ctx.get_handler_name(handler.state, func_name)) -def format_event_handler(handler: EventHandler | Callable[..., Any]) -> str: +def format_event_handler(handler: EventHandler) -> str: """Format an event handler. Args: diff --git a/reflex/minify.py b/reflex/minify.py index 72eec2d56b1..75d72008aea 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -199,17 +199,23 @@ def from_disk(cls) -> MinifyNameResolver: """Build a resolver from the current ``minify.json`` and env vars. Bypasses the lru_cache on :func:`get_minify_config` so the snapshot - reflects the cwd at install time. Malformed configs become - ``config=None`` — the validate CLI surfaces them separately. + reflects the cwd at install time. A malformed config becomes + ``config=None`` (graceful degradation — the app still runs without + minification) and a warning is logged once per install so the user + notices. Returns: A configured resolver. """ from reflex.environment import MinifyMode, environment + from reflex.utils import console try: config = _load_minify_config_uncached() - except ValueError: + except ValueError as e: + console.warn( + f"{MINIFY_JSON} could not be loaded: {e}; minification disabled." + ) config = None return cls( config=config, @@ -297,16 +303,20 @@ def install_minify_resolver() -> None: def ensure_minify_resolver_for_active_context() -> None: """Install a :class:`MinifyNameResolver` for the active context if needed. - Idempotent once a config-loaded resolver is in place — safe to wire into - hot paths (e.g. :func:`reflex.utils.prerequisites.get_app`, which can be - re-entered at runtime from a different cwd than the one that loaded the - config). Re-installs only if the current resolver has ``config=None``. + Idempotent in the steady state — safe to wire into hot paths + (e.g. :func:`reflex.utils.prerequisites.get_app`, which can be re-entered + at runtime from a different cwd than the one that loaded the config). + Re-installs only when something on disk could have changed: a config + appearing where there wasn't one, or a non-minify resolver in the slot. """ from reflex_base.registry import RegistrationContext ctx = RegistrationContext.ensure_context() - if isinstance(ctx.name_resolver, MinifyNameResolver) and ctx.name_resolver.config: - return + if isinstance(ctx.name_resolver, MinifyNameResolver): + if ctx.name_resolver.config is not None: + return # already loaded with a config — no work to do + if not _get_minify_json_path().exists(): + return # no config and none on disk; nothing changed ctx.set_name_resolver(MinifyNameResolver.from_disk()) @@ -493,8 +503,8 @@ def _assign_next_ids( ) -> dict[str, str]: """Assign minified ids to ``new_keys`` while skipping ``existing_ids``. - Mutates ``existing_ids`` so callers can chain assignments against the same - pool. Keys are sorted for deterministic output. + Keys are sorted for deterministic output. ``existing_ids`` is read-only — + a working copy is taken internally. Args: new_keys: Keys needing new ids. @@ -505,13 +515,14 @@ def _assign_next_ids( Returns: Mapping from key to its newly-assigned minified id. """ - next_id = 0 if reassign_deleted else max(existing_ids, default=-1) + 1 + pool = set(existing_ids) + next_id = 0 if reassign_deleted else max(pool, default=-1) + 1 out: dict[str, str] = {} for key in sorted(new_keys): - while next_id in existing_ids: + while next_id in pool: next_id += 1 out[key] = int_to_minified_name(next_id) - existing_ids.add(next_id) + pool.add(next_id) next_id += 1 return out @@ -673,7 +684,7 @@ def sync_minify_config( # Assign new state IDs (unique among siblings of the same parent class). for parent_cls, children in parent_cls_to_new_children.items(): - existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()).copy() + existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()) new_states.update(_assign_next_ids(children, existing_ids, reassign_deleted)) # Assign new event IDs (unique within each state). diff --git a/reflex/reflex.py b/reflex/reflex.py index 6f382a8cf3f..5a33d8e8d99 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -1209,58 +1209,29 @@ def minify_lookup(output_json: bool, minified_path: str): Walks the state tree from the root to resolve each segment. """ - from reflex.minify import get_state_full_path - from reflex.state import BaseState, State + from reflex.minify import collect_all_states, get_state_full_path + from reflex.state import State config = _open_minify_session() - def collect_states( - state_cls: type[BaseState], - ) -> list[type[BaseState]]: - """Recursively collect all states. - - Args: - state_cls: The state class to start from. + # Build lookup: full_path -> minified_id (None if no entry). + path_to_id = { + get_state_full_path(s): config["states"].get(get_state_full_path(s)) + for s in collect_all_states(State) + } - Returns: - List of all state classes in the hierarchy. - """ - result = [state_cls] - for sub in state_cls.get_substates(): - result.extend(collect_states(sub)) - return result - - # Build lookup: full_path -> (state_class, minified_id) - all_states = collect_states(State) - path_to_info: dict[str, tuple[type[BaseState], str | None]] = {} - for state_cls in all_states: - full_path = get_state_full_path(state_cls) - minified_id = config["states"].get(full_path) - path_to_info[full_path] = (state_cls, minified_id) - - # Walk the minified path parts = minified_path.split(".") result_parts = [] current = State for i, part in enumerate(parts): - # Find state whose minified ID matches 'part' - found = None - if i == 0: - # First segment should match root state - state_path = get_state_full_path(current) - _, state_id = path_to_info.get(state_path, (None, None)) - if state_id == part: - found = current - else: - # Find among children of current - for child in current.get_substates(): - child_path = get_state_full_path(child) - _, child_id = path_to_info.get(child_path, (None, None)) - if child_id == part: - found = child - break - + # Find the state whose minified id matches ``part``: the root state + # for the first segment, otherwise a child of the previous match. + candidates = [current] if i == 0 else current.get_substates() + found = next( + (c for c in candidates if path_to_id.get(get_state_full_path(c)) == part), + None, + ) if found is None: console.error( f"No state found for minified segment '{part}' in path '{minified_path}'" @@ -1268,10 +1239,9 @@ def collect_states( raise SystemExit(1) state_path = get_state_full_path(found) - _, state_id = path_to_info.get(state_path, (None, None)) result_parts.append({ "minified": part, - "state_id": state_id, + "state_id": part, # we just matched on it "module": found.__module__, "class": found.__name__, "full_path": state_path, diff --git a/reflex/state.py b/reflex/state.py index 46f3946f54d..14cffc6b29b 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2273,6 +2273,12 @@ def wrapper() -> Component: class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" + # ``__init_subclass__`` replaces the method below with an ``EventHandler`` + # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers + # (e.g. ``format_event_handler``) see the post-rewrite type. + if TYPE_CHECKING: + handle_frontend_exception: EventHandler + # If the frontend error message contains any of these strings, automatically reload the page. auto_reload_on_errors: ClassVar[list[re.Pattern]] = [ re.compile( # Chrome/Edge @@ -2290,61 +2296,70 @@ class FrontendEventExceptionState(State): ), ] - @event - def handle_frontend_exception( - self, info: str, component_stack: str - ) -> Iterator[EventSpec]: - """Handle frontend exceptions. - - If a frontend exception handler is provided, it will be called. - Otherwise, the default frontend exception handler will be called. - - Args: - info: The exception information. - component_stack: The stack trace of the component where the exception occurred. + if not TYPE_CHECKING: - Yields: - Optional auto-reload event for certain errors outside cooldown period. - """ - # Handle automatic reload for certain errors. - if type(self).auto_reload_on_errors and any( - error.search(info) for error in type(self).auto_reload_on_errors - ): - yield call_script( - f"const last_reload = parseInt(window.sessionStorage.getItem('{LAST_RELOADED_KEY}')) || 0;" - f"if (Date.now() - last_reload > {environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS.get()})" - "{" - f"window.sessionStorage.setItem('{LAST_RELOADED_KEY}', Date.now().toString());" - "window.location.reload();" - "}" + @event + def handle_frontend_exception( + self, info: str, component_stack: str + ) -> Iterator[EventSpec]: + """Handle frontend exceptions. + + If a frontend exception handler is provided, it will be called. + Otherwise, the default frontend exception handler will be called. + + Args: + info: The exception information. + component_stack: The stack trace of the component where the exception occurred. + + Yields: + Optional auto-reload event for certain errors outside cooldown period. + """ + # Handle automatic reload for certain errors. + if type(self).auto_reload_on_errors and any( + error.search(info) for error in type(self).auto_reload_on_errors + ): + yield call_script( + f"const last_reload = parseInt(window.sessionStorage.getItem('{LAST_RELOADED_KEY}')) || 0;" + f"if (Date.now() - last_reload > {environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS.get()})" + "{" + f"window.sessionStorage.setItem('{LAST_RELOADED_KEY}', Date.now().toString());" + "window.location.reload();" + "}" + ) + prerequisites.get_and_validate_app().app.frontend_exception_handler( + Exception(info) ) - prerequisites.get_and_validate_app().app.frontend_exception_handler( - Exception(info) - ) class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - async def update_vars_internal(self, vars: dict[str, Any]) -> None: - """Apply updates to fully qualified state vars. + # ``__init_subclass__`` replaces the method below with an ``EventHandler`` + # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers + # (e.g. ``format_event_handler``) see the post-rewrite type. + if TYPE_CHECKING: + update_vars_internal: EventHandler + else: - The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`, - and each value will be set on the appropriate substate instance. + async def update_vars_internal(self, vars: dict[str, Any]) -> None: + """Apply updates to fully qualified state vars. - This function is primarily used to apply cookie and local storage - updates from the frontend to the appropriate substate. + The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`, + and each value will be set on the appropriate substate instance. - Args: - vars: The fully qualified vars and values to update. - """ - for var, value in vars.items(): - state_name, _, var_name = var.rpartition(".") - var_name = var_name.removesuffix(FIELD_MARKER) - var_state_cls = State.get_class_substate(state_name) - if var_state_cls._is_client_storage(var_name): - var_state = await self.get_state(var_state_cls) - setattr(var_state, var_name, value) + This function is primarily used to apply cookie and local storage + updates from the frontend to the appropriate substate. + + Args: + vars: The fully qualified vars and values to update. + """ + for var, value in vars.items(): + state_name, _, var_name = var.rpartition(".") + var_name = var_name.removesuffix(FIELD_MARKER) + var_state_cls = State.get_class_substate(state_name) + if var_state_cls._is_client_storage(var_name): + var_state = await self.get_state(var_state_cls) + setattr(var_state, var_name, value) class OnLoadInternalState(State): @@ -2353,41 +2368,49 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ + # ``__init_subclass__`` replaces the method below with an ``EventHandler`` + # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers + # (e.g. ``format_event_handler``) see the post-rewrite type. + if TYPE_CHECKING: + on_load_internal: EventHandler + # Cannot properly annotate this as `App` due to circular import issues. _app_ref: ClassVar[Any] = None - def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | None: - """Queue on_load handlers for the current page. - - Returns: - The list of events to queue for on load handling. - - Raises: - TypeError: If the app reference is not of type App. - """ - from reflex.app import App + if not TYPE_CHECKING: - app = type(self)._app_ref or prerequisites.get_and_validate_app().app - if not isinstance(app, App): - msg = ( - f"Expected app to be of type {App.__name__}, got {type(app).__name__}." - ) - raise TypeError(msg) - # Cache the app reference for subsequent calls. - if type(self)._app_ref is None: - type(self)._app_ref = app - load_events = app.get_load_events(self.router.url.path) - if not load_events: - self.is_hydrated = True - return None # Fast path for navigation with no on_load events defined. - self.is_hydrated = False - return [ - *Event.from_event_type( - load_events, - router_data=self.router_data, - ), - State.set_is_hydrated(True), - ] + def on_load_internal( + self, + ) -> list[Event | EventSpec | event.EventCallback] | None: + """Queue on_load handlers for the current page. + + Returns: + The list of events to queue for on load handling. + + Raises: + TypeError: If the app reference is not of type App. + """ + from reflex.app import App + + app = type(self)._app_ref or prerequisites.get_and_validate_app().app + if not isinstance(app, App): + msg = f"Expected app to be of type {App.__name__}, got {type(app).__name__}." + raise TypeError(msg) + # Cache the app reference for subsequent calls. + if type(self)._app_ref is None: + type(self)._app_ref = app + load_events = app.get_load_events(self.router.url.path) + if not load_events: + self.is_hydrated = True + return None # Fast path for navigation with no on_load events defined. + self.is_hydrated = False + return [ + *Event.from_event_type( + load_events, + router_data=self.router_data, + ), + State.set_is_hydrated(True), + ] class ComponentState(State, mixin=True): From 05fde773f93fdd22d37a4f62b32208401c515a29 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 22:02:32 +0200 Subject: [PATCH 65/74] cleanup --- reflex/minify.py | 43 ++++++++++++++++--------------------------- reflex/state.py | 25 ++++++++----------------- 2 files changed, 24 insertions(+), 44 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 75d72008aea..0f0e350b7a5 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -35,22 +35,18 @@ class MinifyConfig(TypedDict): def _get_minify_json_path() -> Path: - """Get the path to the minify.json file. - - Returns: - Path to minify.json in the current working directory. - """ + """Return the path to ``minify.json`` in the current working directory.""" return Path.cwd() / MINIFY_JSON def _load_minify_config_uncached() -> MinifyConfig | None: - """Load minify configuration from minify.json. + """Load and validate ``minify.json`` from disk. Returns: - The parsed configuration, or None if file doesn't exist. + The parsed config, or ``None`` if the file is absent. Raises: - ValueError: If the file exists but has an invalid format. + ValueError: If the file exists but is malformed. """ path = _get_minify_json_path() if not path.exists(): @@ -196,13 +192,9 @@ class MinifyNameResolver: @classmethod def from_disk(cls) -> MinifyNameResolver: - """Build a resolver from the current ``minify.json`` and env vars. + """Build a resolver from ``minify.json`` (uncached) and env vars. - Bypasses the lru_cache on :func:`get_minify_config` so the snapshot - reflects the cwd at install time. A malformed config becomes - ``config=None`` (graceful degradation — the app still runs without - minification) and a warning is logged once per install so the user - notices. + Malformed configs degrade gracefully to ``config=None`` with a warning. Returns: A configured resolver. @@ -224,7 +216,7 @@ def from_disk(cls) -> MinifyNameResolver: ) def _is_minify_allowed(self, state_cls: type[BaseState], enabled: bool) -> bool: - """Whether minification applies to ``state_cls`` for the given mode. + """Whether ``state_cls`` is eligible for minification under the given mode. Args: state_cls: The state class being resolved. @@ -289,10 +281,9 @@ def _is_framework_state(state_cls: type[BaseState]) -> bool: def install_minify_resolver() -> None: """Install a fresh :class:`MinifyNameResolver` into the active context. - Must run before any state class is registered: - :func:`reflex_base.vars.base.VarData.from_state` captures the state's - full name at Var-creation time, so a later install would leave dangling - references in the generated frontend. + Must run before any state class registers — :func:`VarData.from_state` + captures the state's full name at Var-creation time, so a later install + leaves dangling references in the generated frontend. """ from reflex_base.registry import RegistrationContext @@ -301,22 +292,20 @@ def install_minify_resolver() -> None: def ensure_minify_resolver_for_active_context() -> None: - """Install a :class:`MinifyNameResolver` for the active context if needed. + """Install a :class:`MinifyNameResolver` if one isn't already in place. - Idempotent in the steady state — safe to wire into hot paths - (e.g. :func:`reflex.utils.prerequisites.get_app`, which can be re-entered - at runtime from a different cwd than the one that loaded the config). - Re-installs only when something on disk could have changed: a config - appearing where there wasn't one, or a non-minify resolver in the slot. + Idempotent — safe to wire into hot paths like + :func:`reflex.utils.prerequisites.get_app`. Re-installs only when on-disk + state could have changed (config appearing, or non-minify resolver). """ from reflex_base.registry import RegistrationContext ctx = RegistrationContext.ensure_context() if isinstance(ctx.name_resolver, MinifyNameResolver): if ctx.name_resolver.config is not None: - return # already loaded with a config — no work to do + return if not _get_minify_json_path().exists(): - return # no config and none on disk; nothing changed + return ctx.set_name_resolver(MinifyNameResolver.from_disk()) diff --git a/reflex/state.py b/reflex/state.py index 14cffc6b29b..430fe9ec138 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -494,7 +494,6 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs): super().__init_subclass__(**kwargs) - # Mixin states are not initialized if cls._mixin: return @@ -1004,11 +1003,9 @@ def get_class_substate( Args: path: The path to the substate. - _skip_self: Internal recursion flag — leave at the default. Only - the root call strips a leading segment that matches - ``cls.get_name()``; recursion passes ``False`` so a child - that shares a minified name with its parent (``"a.b.b"``) - still resolves to the child. + _skip_self: Internal recursion flag; leave at the default. Allows + ``"a.b.b"`` to resolve to a child that shares its parent's + minified name. Returns: The class substate. @@ -1538,11 +1535,9 @@ def get_substate(self, path: Sequence[str], _skip_self: bool = True) -> BaseStat Args: path: The path to the substate. - _skip_self: Internal recursion flag — leave at the default. Only - the root call strips a leading segment that matches - ``self.get_name()``; recursion passes ``False`` so a child - that shares a minified name with its parent (``"a.b.b"``) - still resolves to the child. + _skip_self: Internal recursion flag; leave at the default. Allows + ``"a.b.b"`` to resolve to a child that shares its parent's + minified name. Returns: The substate. @@ -2334,9 +2329,7 @@ def handle_frontend_exception( class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - # ``__init_subclass__`` replaces the method below with an ``EventHandler`` - # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers - # (e.g. ``format_event_handler``) see the post-rewrite type. + # See ``FrontendEventExceptionState`` for why this is split. if TYPE_CHECKING: update_vars_internal: EventHandler else: @@ -2368,9 +2361,7 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ - # ``__init_subclass__`` replaces the method below with an ``EventHandler`` - # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers - # (e.g. ``format_event_handler``) see the post-rewrite type. + # See ``FrontendEventExceptionState`` for why this is split. if TYPE_CHECKING: on_load_internal: EventHandler From 9b57ec1242d61bd090c41d3807ac10133ad50f87 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 26 Apr 2026 22:53:18 +0200 Subject: [PATCH 66/74] wip --- .../src/reflex_base/event/__init__.py | 21 + reflex/minify.py | 34 +- reflex/state.py | 174 ++++---- tests/integration/test_minification.py | 375 +++++------------- tests/units/test_minification.py | 255 ++++-------- 5 files changed, 290 insertions(+), 569 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/event/__init__.py b/packages/reflex-base/src/reflex_base/event/__init__.py index 8762e694d18..e0ce3e5bd90 100644 --- a/packages/reflex-base/src/reflex_base/event/__init__.py +++ b/packages/reflex-base/src/reflex_base/event/__init__.py @@ -470,6 +470,26 @@ def __call__(self, *args: Any, **kwargs: Any) -> "EventSpec": ) +if TYPE_CHECKING: + + def typing_event(fn: Callable[..., Any]) -> EventHandler: + """Mark ``fn`` so pyright sees it as the ``EventHandler`` that + ``BaseState.__init_subclass__`` will rewrite it into. No-op at runtime. + + Args: + fn: The method to mark. + + Returns: + ``fn`` typed as :class:`EventHandler`. + """ + ... + +else: + + def typing_event(fn: Callable[..., Any]) -> Callable[..., Any]: # noqa: D103 + return fn + + @dataclasses.dataclass( init=True, frozen=True, @@ -2833,6 +2853,7 @@ def BaseState(self) -> "type[BaseState]": # noqa: N802 event = EventNamespace event.event = event # pyright: ignore[reportAttributeAccessIssue] +event.typing_event = staticmethod(typing_event) # pyright: ignore[reportAttributeAccessIssue] _this = sys.modules[__name__] event.__path__ = _this.__path__ # pyright: ignore[reportAttributeAccessIssue] event.__spec__ = _this.__spec__ # pyright: ignore[reportAttributeAccessIssue] diff --git a/reflex/minify.py b/reflex/minify.py index 0f0e350b7a5..5512836e5b5 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -108,18 +108,8 @@ def get_minify_config() -> MinifyConfig | None: return _load_minify_config_uncached() -def is_minify_enabled() -> bool: - """Whether either state or event minification is enabled. - - Returns: - ``True`` if either ``REFLEX_MINIFY_STATES`` or ``REFLEX_MINIFY_EVENTS`` - is on and a config exists. - """ - return is_state_minify_enabled() or is_event_minify_enabled() - - @functools.cache -def _is_mode_enabled(env_var_name: str) -> bool: +def is_mode_enabled(env_var_name: str) -> bool: """Whether the given ``REFLEX_MINIFY_*`` env var is on and a config exists. Args: @@ -135,22 +125,16 @@ def _is_mode_enabled(env_var_name: str) -> bool: return env_var.get() == MinifyMode.ENABLED and get_minify_config() is not None -def is_state_minify_enabled() -> bool: - """Whether state-id minification is enabled. - - Returns: - ``True`` if ``REFLEX_MINIFY_STATES=enabled`` and ``minify.json`` exists. - """ - return _is_mode_enabled("REFLEX_MINIFY_STATES") - - -def is_event_minify_enabled() -> bool: - """Whether event-id minification is enabled. +def is_minify_enabled() -> bool: + """Whether either state or event minification is enabled. Returns: - ``True`` if ``REFLEX_MINIFY_EVENTS=enabled`` and ``minify.json`` exists. + ``True`` when ``REFLEX_MINIFY_STATES`` or ``REFLEX_MINIFY_EVENTS`` is + on and ``minify.json`` exists. """ - return _is_mode_enabled("REFLEX_MINIFY_EVENTS") + return is_mode_enabled("REFLEX_MINIFY_STATES") or is_mode_enabled( + "REFLEX_MINIFY_EVENTS" + ) def save_minify_config(config: MinifyConfig) -> None: @@ -316,7 +300,7 @@ def clear_config_cache() -> None: ``REFLEX_MINIFY_*`` env vars at runtime. """ get_minify_config.cache_clear() - _is_mode_enabled.cache_clear() + is_mode_enabled.cache_clear() install_minify_resolver() diff --git a/reflex/state.py b/reflex/state.py index 430fe9ec138..ed452d1a692 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -36,6 +36,7 @@ EventHandler, EventSpec, call_script, + typing_event, ) from reflex_base.utils.exceptions import ( ComputedVarShadowsBaseVarsError, @@ -2268,12 +2269,6 @@ def wrapper() -> Component: class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" - # ``__init_subclass__`` replaces the method below with an ``EventHandler`` - # instance on the class. Splitting via ``TYPE_CHECKING`` lets callers - # (e.g. ``format_event_handler``) see the post-rewrite type. - if TYPE_CHECKING: - handle_frontend_exception: EventHandler - # If the frontend error message contains any of these strings, automatically reload the page. auto_reload_on_errors: ClassVar[list[re.Pattern]] = [ re.compile( # Chrome/Edge @@ -2291,68 +2286,62 @@ class FrontendEventExceptionState(State): ), ] - if not TYPE_CHECKING: + @typing_event + def handle_frontend_exception( + self, info: str, component_stack: str + ) -> Iterator[EventSpec]: + """Handle frontend exceptions. - @event - def handle_frontend_exception( - self, info: str, component_stack: str - ) -> Iterator[EventSpec]: - """Handle frontend exceptions. - - If a frontend exception handler is provided, it will be called. - Otherwise, the default frontend exception handler will be called. - - Args: - info: The exception information. - component_stack: The stack trace of the component where the exception occurred. - - Yields: - Optional auto-reload event for certain errors outside cooldown period. - """ - # Handle automatic reload for certain errors. - if type(self).auto_reload_on_errors and any( - error.search(info) for error in type(self).auto_reload_on_errors - ): - yield call_script( - f"const last_reload = parseInt(window.sessionStorage.getItem('{LAST_RELOADED_KEY}')) || 0;" - f"if (Date.now() - last_reload > {environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS.get()})" - "{" - f"window.sessionStorage.setItem('{LAST_RELOADED_KEY}', Date.now().toString());" - "window.location.reload();" - "}" - ) - prerequisites.get_and_validate_app().app.frontend_exception_handler( - Exception(info) + If a frontend exception handler is provided, it will be called. + Otherwise, the default frontend exception handler will be called. + + Args: + info: The exception information. + component_stack: The stack trace of the component where the exception occurred. + + Yields: + Optional auto-reload event for certain errors outside cooldown period. + """ + # Handle automatic reload for certain errors. + if type(self).auto_reload_on_errors and any( + error.search(info) for error in type(self).auto_reload_on_errors + ): + yield call_script( + f"const last_reload = parseInt(window.sessionStorage.getItem('{LAST_RELOADED_KEY}')) || 0;" + f"if (Date.now() - last_reload > {environment.REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS.get()})" + "{" + f"window.sessionStorage.setItem('{LAST_RELOADED_KEY}', Date.now().toString());" + "window.location.reload();" + "}" ) + prerequisites.get_and_validate_app().app.frontend_exception_handler( + Exception(info) + ) class UpdateVarsInternalState(State): """Substate for handling internal state var updates.""" - # See ``FrontendEventExceptionState`` for why this is split. - if TYPE_CHECKING: - update_vars_internal: EventHandler - else: + @typing_event + async def update_vars_internal(self, vars: dict[str, Any]) -> None: + """Apply updates to fully qualified state vars. - async def update_vars_internal(self, vars: dict[str, Any]) -> None: - """Apply updates to fully qualified state vars. + The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`, + and each value will be set on the appropriate substate instance. - The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`, - and each value will be set on the appropriate substate instance. + This function is primarily used to apply cookie and local storage + updates from the frontend to the appropriate substate. - This function is primarily used to apply cookie and local storage - updates from the frontend to the appropriate substate. - - Args: - vars: The fully qualified vars and values to update. - """ - for var, value in vars.items(): - state_name, _, var_name = var.rpartition(".") - var_name = var_name.removesuffix(FIELD_MARKER) - var_state_cls = State.get_class_substate(state_name) - if var_state_cls._is_client_storage(var_name): - var_state = await self.get_state(var_state_cls) - setattr(var_state, var_name, value) + Args: + vars: The fully qualified vars and values to update. + """ + for var, value in vars.items(): + state_name, _, var_name = var.rpartition(".") + var_name = var_name.removesuffix(FIELD_MARKER) + var_state_cls = State.get_class_substate(state_name) + if var_state_cls._is_client_storage(var_name): + var_state = await self.get_state(var_state_cls) + setattr(var_state, var_name, value) class OnLoadInternalState(State): @@ -2361,47 +2350,42 @@ class OnLoadInternalState(State): This is a separate substate to avoid deserializing the entire state tree for every page navigation. """ - # See ``FrontendEventExceptionState`` for why this is split. - if TYPE_CHECKING: - on_load_internal: EventHandler - # Cannot properly annotate this as `App` due to circular import issues. _app_ref: ClassVar[Any] = None - if not TYPE_CHECKING: + @typing_event + def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | None: + """Queue on_load handlers for the current page. + + Returns: + The list of events to queue for on load handling. + + Raises: + TypeError: If the app reference is not of type App. + """ + from reflex.app import App - def on_load_internal( - self, - ) -> list[Event | EventSpec | event.EventCallback] | None: - """Queue on_load handlers for the current page. - - Returns: - The list of events to queue for on load handling. - - Raises: - TypeError: If the app reference is not of type App. - """ - from reflex.app import App - - app = type(self)._app_ref or prerequisites.get_and_validate_app().app - if not isinstance(app, App): - msg = f"Expected app to be of type {App.__name__}, got {type(app).__name__}." - raise TypeError(msg) - # Cache the app reference for subsequent calls. - if type(self)._app_ref is None: - type(self)._app_ref = app - load_events = app.get_load_events(self.router.url.path) - if not load_events: - self.is_hydrated = True - return None # Fast path for navigation with no on_load events defined. - self.is_hydrated = False - return [ - *Event.from_event_type( - load_events, - router_data=self.router_data, - ), - State.set_is_hydrated(True), - ] + app = type(self)._app_ref or prerequisites.get_and_validate_app().app + if not isinstance(app, App): + msg = ( + f"Expected app to be of type {App.__name__}, got {type(app).__name__}." + ) + raise TypeError(msg) + # Cache the app reference for subsequent calls. + if type(self)._app_ref is None: + type(self)._app_ref = app + load_events = app.get_load_events(self.router.url.path) + if not load_events: + self.is_hydrated = True + return None # Fast path for navigation with no on_load events defined. + self.is_hydrated = False + return [ + *Event.from_event_type( + load_events, + router_data=self.router_data, + ), + State.set_is_hydrated(True), + ] class ComponentState(State, mixin=True): diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 19199676839..60a3718ea9c 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -18,40 +18,27 @@ def MinificationApp(): - """Test app for state and event handler minification. - - This app is used to test that: - 1. Without minify.json, full state/event names are used - 2. With minify.json, minified names are used based on the config - """ + """Test app with one root + one substate, each with one event handler.""" import reflex as rx from reflex.utils import format class RootState(rx.State): - """Root state for testing.""" - count: int = 0 @rx.event def increment(self): - """Increment the count.""" self.count += 1 class SubState(RootState): - """Sub state for testing.""" - message: str = "hello" @rx.event def update_message(self): - """Update the message.""" parent = self.parent_state assert parent is not None assert isinstance(parent, RootState) self.message = f"count is {parent.count}" - # Get formatted event handler names for display - # Use event_handlers dict to get the actual EventHandler objects increment_handler_name = format.format_event_handler( RootState.event_handlers["increment"] ) @@ -72,13 +59,8 @@ def index() -> rx.Component: f"Increment handler: {increment_handler_name}", id="increment_handler_name", ), - rx.text( - f"Update handler: {update_handler_name}", - id="update_handler_name", - ), - rx.text("Count: ", id="count_label"), + rx.text(f"Update handler: {update_handler_name}", id="update_handler_name"), rx.text(RootState.count, id="count_value"), - rx.text("Message: ", id="message_label"), rx.text(SubState.message, id="message_value"), rx.button("Increment", on_click=RootState.increment, id="increment_btn"), rx.button( @@ -90,304 +72,133 @@ def index() -> rx.Component: app.add_page(index) -@pytest.fixture -def minify_disabled_app( - app_harness_env: type[AppHarness], - tmp_path_factory: pytest.TempPathFactory, -) -> Generator[AppHarness, None, None]: - """Start app WITHOUT minify.json (full names). - - Args: - app_harness_env: AppHarness or AppHarnessProd - tmp_path_factory: pytest tmp_path_factory fixture - - Yields: - Running AppHarness instance - """ - # Clear minify config cache to ensure clean state - clear_config_cache() - - # No minify.json file - full names will be used - with app_harness_env.create( - root=tmp_path_factory.mktemp("minify_disabled"), - app_name="minify_disabled", - app_source=MinificationApp, - ) as harness: - yield harness +# Framework state classes (e.g. ``reflex.state.State``) are deliberately +# absent — the resolver never minifies them. +_MINIFY_CONFIG = { + "version": 1, + "states": { + "minify_enabled.minify_enabled.State.RootState": "k", # 10 + "minify_enabled.minify_enabled.State.RootState.SubState": "l", # 11 + }, + "events": { + "minify_enabled.minify_enabled.State.RootState": {"increment": "f"}, # 5 + "minify_enabled.minify_enabled.State.RootState.SubState": { + "update_message": "h" # 7 + }, + }, +} -@pytest.fixture -def minify_enabled_app( +@pytest.fixture(params=[False, True], ids=["disabled", "enabled"]) +def minify_app( + request: pytest.FixtureRequest, app_harness_env: type[AppHarness], tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch, -) -> Generator[AppHarness, None, None]: - """Start app WITH minify.json and env vars enabled (minified names). - - Args: - app_harness_env: AppHarness or AppHarnessProd - tmp_path_factory: pytest tmp_path_factory fixture - monkeypatch: pytest monkeypatch fixture +) -> Generator[tuple[bool, AppHarness], None, None]: + """Run :func:`MinificationApp` with minification on (parametrized). Yields: - Running AppHarness instance + ``(minify_enabled, harness)``. """ - # Enable minification via env vars (required in addition to minify.json) - monkeypatch.setenv(environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value) - monkeypatch.setenv(environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value) - - # Clear minify config cache to ensure clean state + enabled: bool = request.param + if enabled: + monkeypatch.setenv( + environment.REFLEX_MINIFY_STATES.name, MinifyMode.ENABLED.value + ) + monkeypatch.setenv( + environment.REFLEX_MINIFY_EVENTS.name, MinifyMode.ENABLED.value + ) clear_config_cache() - app_root = tmp_path_factory.mktemp("minify_enabled") - - # Create the harness object (but don't start yet) + app_name = "minify_enabled" if enabled else "minify_disabled" + app_root = tmp_path_factory.mktemp(app_name) harness = app_harness_env.create( - root=app_root, - app_name="minify_enabled", - app_source=MinificationApp, + root=app_root, app_name=app_name, app_source=MinificationApp ) - - # Create minify.json with explicit IDs for our states and events - # The state paths need to match what get_state_full_path() returns - # Format: module.StateHierarchy (e.g., "minify_enabled.minify_enabled.State.RootState") - # Note: RootState extends rx.State, so the path includes State in the hierarchy - # Version 2 format: string IDs and nested events - app_module = "minify_enabled.minify_enabled" - root_state_path = f"{app_module}.State.RootState" - sub_state_path = f"{app_module}.State.RootState.SubState" - # Framework state classes (e.g. ``reflex.state.State``) are deliberately - # absent — the resolver never minifies them. - minify_config = { - "version": 1, - "states": { - root_state_path: "k", # int_to_minified_name(10) = 'k' - sub_state_path: "l", # int_to_minified_name(11) = 'l' - }, - "events": { - root_state_path: { - "increment": "f", # int_to_minified_name(5) = 'f' - }, - sub_state_path: { - "update_message": "h", # int_to_minified_name(7) = 'h' - }, - }, - } - - # Write minify.json to the app root directory - minify_path = app_root / MINIFY_JSON - minify_path.write_text(json.dumps(minify_config, indent=2)) + if enabled: + (app_root / MINIFY_JSON).write_text(json.dumps(_MINIFY_CONFIG)) with harness: - yield harness + yield enabled, harness @pytest.fixture -def driver_disabled( - minify_disabled_app: AppHarness, +def driver( + minify_app: tuple[bool, AppHarness], ) -> Generator[WebDriver, None, None]: - """Get browser instance for disabled mode app. - - Args: - minify_disabled_app: harness for the app + """WebDriver scoped to the parametrized :func:`minify_app`. Yields: - WebDriver instance. + A WebDriver pointed at the running app. """ - assert minify_disabled_app.app_instance is not None, "app is not running" - driver = minify_disabled_app.frontend() + _enabled, harness = minify_app + assert harness.app_instance is not None, "app is not running" + drv = harness.frontend() try: - yield driver + yield drv finally: - driver.quit() + drv.quit() -@pytest.fixture -def driver_enabled( - minify_enabled_app: AppHarness, -) -> Generator[WebDriver, None, None]: - """Get browser instance for enabled mode app. +def _text_after_colon(text: str) -> str: + """Strip the ``"label: "`` prefix from a UI element's text. Args: - minify_enabled_app: harness for the app + text: The element text. - Yields: - WebDriver instance. + Returns: + The substring after the first ``": "``, or ``text`` unchanged. """ - assert minify_enabled_app.app_instance is not None, "app is not running" - driver = minify_enabled_app.frontend() - try: - yield driver - finally: - driver.quit() - - -def test_minification_disabled( - minify_disabled_app: AppHarness, - driver_disabled: WebDriver, -) -> None: - """Test that without minify.json, full state and event names are used. - - Args: - minify_disabled_app: harness for the app - driver_disabled: WebDriver instance - """ - assert minify_disabled_app.app_instance is not None - - # Wait for the app to load - token_input = AppHarness.poll_for_or_raise_timeout( - lambda: driver_disabled.find_element(By.ID, "token") - ) - assert token_input - token = minify_disabled_app.poll_for_value(token_input) - assert token - - # Check state names are full names (not minified) - root_state_name_el = driver_disabled.find_element(By.ID, "root_state_name") - sub_state_name_el = driver_disabled.find_element(By.ID, "sub_state_name") - - root_state_name = root_state_name_el.text - sub_state_name = sub_state_name_el.text - - # In disabled mode, names should be the full module___class_name format - assert "root_state" in root_state_name.lower() - assert "sub_state" in sub_state_name.lower() - # Full names should be long (not single char minified names) - # Extract just the state name part after "Root state name: " - root_name_only = ( - root_state_name.split(": ")[-1] if ": " in root_state_name else root_state_name - ) - sub_name_only = ( - sub_state_name.split(": ")[-1] if ": " in sub_state_name else sub_state_name - ) - assert len(root_name_only) > 5, f"Expected long name, got: {root_name_only}" - assert len(sub_name_only) > 5, f"Expected long name, got: {sub_name_only}" - - # Check event handler names are full names (not minified) - increment_handler_el = driver_disabled.find_element(By.ID, "increment_handler_name") - update_handler_el = driver_disabled.find_element(By.ID, "update_handler_name") - - increment_handler = increment_handler_el.text - update_handler = update_handler_el.text - - # In disabled mode, event handler names should contain the full method names - assert "increment" in increment_handler.lower() - assert "update_message" in update_handler.lower() - # The format should be "state_name.method_name", so check for the dot - assert "." in increment_handler - assert "." in update_handler + return text.split(": ", 1)[-1] if ": " in text else text - # Test that state updates work - count_value = driver_disabled.find_element(By.ID, "count_value") - assert count_value.text == "0" - increment_btn = driver_disabled.find_element(By.ID, "increment_btn") - increment_btn.click() - - # Wait for count to update - AppHarness._poll_for(lambda: count_value.text == "1") - assert count_value.text == "1" - - -def test_minification_enabled( - minify_enabled_app: AppHarness, - driver_enabled: WebDriver, +def test_minification( + minify_app: tuple[bool, AppHarness], + driver: WebDriver, ) -> None: - """Test that with minify.json, minified state and event names are used. - - Args: - minify_enabled_app: harness for the app - driver_enabled: WebDriver instance - """ - assert minify_enabled_app.app_instance is not None + """State and event handler names follow the minify config when enabled.""" + enabled, harness = minify_app + assert harness.app_instance is not None - # Wait for the app to load token_input = AppHarness.poll_for_or_raise_timeout( - lambda: driver_enabled.find_element(By.ID, "token") - ) - assert token_input - token = minify_enabled_app.poll_for_value(token_input) - assert token - - # Check state names are minified - root_state_name_el = driver_enabled.find_element(By.ID, "root_state_name") - sub_state_name_el = driver_enabled.find_element(By.ID, "sub_state_name") - - root_state_name = root_state_name_el.text - sub_state_name = sub_state_name_el.text - - # In enabled mode with minify.json, names should be minified - # RootState has state_id=10 -> 'k' - # SubState has state_id=11 -> 'l' - expected_root_minified = int_to_minified_name(10) - expected_sub_minified = int_to_minified_name(11) - - assert expected_root_minified in root_state_name - assert expected_sub_minified in sub_state_name - - # Check event handler names are minified - increment_handler_el = driver_enabled.find_element(By.ID, "increment_handler_name") - update_handler_el = driver_enabled.find_element(By.ID, "update_handler_name") - - increment_handler_text = increment_handler_el.text - update_handler_text = update_handler_el.text - - # Extract just the handler name part after "Increment handler: " - increment_handler = ( - increment_handler_text.split(": ")[-1] - if ": " in increment_handler_text - else increment_handler_text - ) - update_handler = ( - update_handler_text.split(": ")[-1] - if ": " in update_handler_text - else update_handler_text + lambda: driver.find_element(By.ID, "token") ) + assert harness.poll_for_value(token_input) - # In enabled mode with minify.json: - # - increment has event_id=5 -> 'f' - # - update_message has event_id=7 -> 'h' - expected_increment_minified = int_to_minified_name(5) - expected_update_minified = int_to_minified_name(7) - - # Event handler format: "state_name.event_name" - # For increment: "k.f" (state_id=10 -> 'k', event_id=5 -> 'f') - # For update_message: "k.l.h" (state_id=10.11 -> 'k.l', event_id=7 -> 'h') - assert increment_handler.endswith(f".{expected_increment_minified}"), ( - f"Expected minified event name ending with '.{expected_increment_minified}', " - f"got: {increment_handler}" + root_name = driver.find_element(By.ID, "root_state_name").text + sub_name = driver.find_element(By.ID, "sub_state_name").text + increment_name = _text_after_colon( + driver.find_element(By.ID, "increment_handler_name").text ) - assert update_handler.endswith(f".{expected_update_minified}"), ( - f"Expected minified event name ending with '.{expected_update_minified}', " - f"got: {update_handler}" + update_name = _text_after_colon( + driver.find_element(By.ID, "update_handler_name").text ) - # The handler names should NOT contain the original method names - assert "increment" not in increment_handler.lower(), ( - f"Expected minified name without 'increment', got: {increment_handler}" - ) - assert "update_message" not in update_handler.lower(), ( - f"Expected minified name without 'update_message', got: {update_handler}" - ) - - # Test that state updates work with minified names - count_value = driver_enabled.find_element(By.ID, "count_value") - assert count_value.text == "0" - - increment_btn = driver_enabled.find_element(By.ID, "increment_btn") - increment_btn.click() - - # Wait for count to update - AppHarness._poll_for(lambda: count_value.text == "1") - assert count_value.text == "1" - - # Test substate event handler works with minified names - message_value = driver_enabled.find_element(By.ID, "message_value") - assert message_value.text == "hello" - - update_msg_btn = driver_enabled.find_element(By.ID, "update_msg_btn") - update_msg_btn.click() - - # Wait for message to update - AppHarness._poll_for(lambda: "count is 1" in message_value.text) - assert message_value.text == "count is 1" + if enabled: + assert int_to_minified_name(10) in root_name + assert int_to_minified_name(11) in sub_name + assert increment_name.endswith(f".{int_to_minified_name(5)}") + assert update_name.endswith(f".{int_to_minified_name(7)}") + assert "increment" not in increment_name.lower() + assert "update_message" not in update_name.lower() + else: + assert "root_state" in root_name.lower() + assert "sub_state" in sub_name.lower() + assert "increment" in increment_name.lower() + assert "update_message" in update_name.lower() + assert "." in increment_name + assert "." in update_name + + # Event dispatch sanity check (must work regardless of minification). + count = driver.find_element(By.ID, "count_value") + assert count.text == "0" + driver.find_element(By.ID, "increment_btn").click() + AppHarness._poll_for(lambda: count.text == "1") + + if enabled: + # Substate handler dispatch through minified names. + message = driver.find_element(By.ID, "message_value") + driver.find_element(By.ID, "update_msg_btn").click() + AppHarness._poll_for(lambda: "count is 1" in message.text) + assert message.text == "count is 1" diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 516862e0333..d7d9c0744c5 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -22,9 +22,8 @@ get_minify_config, get_state_full_path, int_to_minified_name, - is_event_minify_enabled, is_minify_enabled, - is_state_minify_enabled, + is_mode_enabled, minified_name_to_int, save_minify_config, sync_minify_config, @@ -133,6 +132,33 @@ def cli_runner(monkeypatch: pytest.MonkeyPatch) -> CliRunner: return CliRunner() +def _stub_resolver( + *, + state_name: str | None = None, + target: type[BaseState] = State, + handler_prefix: str | None = None, +) -> NameResolver: + """Build a tiny one-off :class:`NameResolver` for tests. + + Args: + state_name: Override returned for ``target`` (else ``None``). + target: Which state class the override scopes to. + handler_prefix: When set, prefixes every handler name. + + Returns: + A resolver with the requested behavior. + """ + + class _Stub: + def resolve_state_name(self, state_cls): + return state_name if state_cls is target else None + + def resolve_handler_name(self, state_cls, handler_name): + return f"{handler_prefix}{handler_name}" if handler_prefix else None + + return _Stub() + + class TestIntToMinifiedName: """Tests for int_to_minified_name function.""" @@ -266,7 +292,7 @@ def test_invalid_version_raises(self, temp_minify_json, monkeypatch): clear_config_cache() with pytest.raises(ValueError, match=r"Unsupported.*version"): - is_state_minify_enabled() + is_mode_enabled("REFLEX_MINIFY_STATES") def test_missing_states_raises(self, temp_minify_json, monkeypatch): """Test that missing 'states' key raises ValueError.""" @@ -279,7 +305,7 @@ def test_missing_states_raises(self, temp_minify_json, monkeypatch): clear_config_cache() with pytest.raises(ValueError, match="'states' must be"): - is_state_minify_enabled() + is_mode_enabled("REFLEX_MINIFY_STATES") class TestGenerateMinifyConfig: @@ -488,47 +514,34 @@ class ChildB(ParentState): class TestStateMinification: """Tests for state name minification with minify.json.""" - def test_state_uses_full_name_without_config(self, temp_minify_json): - """Test that states use full names when no minify.json exists.""" - - class TestState(BaseState): - pass - - # Should be the full name (snake_case module___class) - assert "test_state" in TestState.get_name().lower() - - def test_state_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): - """Test that states use minified names when minify.json exists and env var is enabled.""" - _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) - - class TestState(BaseState): - pass - - _install_config(states={get_state_full_path(TestState): "f"}) - - assert TestState.get_name() == "f" - - def test_state_uses_full_name_when_env_disabled( - self, temp_minify_json, monkeypatch + @pytest.mark.parametrize( + ("mode", "expect_minified"), + [(None, False), (MinifyMode.ENABLED, True), (MinifyMode.DISABLED, False)], + ) + def test_state_name_resolution( + self, temp_minify_json, monkeypatch, mode, expect_minified ): - """Test that states use full names when env var is disabled even with minify.json.""" - _set_minify_modes(monkeypatch, states=MinifyMode.DISABLED) + """Minified name only when env is ENABLED and config has the entry.""" + if mode is not None: + _set_minify_modes(monkeypatch, states=mode) class TestState(BaseState): pass - _install_config(states={get_state_full_path(TestState): "f"}) + if mode is not None: + _install_config(states={get_state_full_path(TestState): "f"}) name = TestState.get_name() - assert name != "f" - assert "test_state" in name.lower() + assert (name == "f") is expect_minified + if not expect_minified: + assert "test_state" in name.lower() class TestEventMinification: """Tests for event handler name minification with minify.json.""" def test_event_uses_full_name_without_config(self, temp_minify_json): - """Test that event handlers use full names when no minify.json exists.""" + """No minify.json → handlers keep their Python names.""" import reflex as rx from reflex.utils.format import get_event_handler_parts @@ -537,66 +550,56 @@ class TestState(BaseState): def my_handler(self): pass - handler = TestState.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) + _, event_name = get_event_handler_parts(TestState.event_handlers["my_handler"]) assert event_name == "my_handler" def test_event_uses_minified_name_with_config(self, temp_minify_json, monkeypatch): - """Test that event handlers use minified names when minify.json exists and env var is enabled.""" + """Handler name follows the config when ``REFLEX_MINIFY_EVENTS`` is on.""" import reflex as rx from reflex.utils.format import get_event_handler_parts _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) - # The config must exist before the class is defined: registration - # captures the resolved name once during __init_subclass__. - state_path = "tests.units.test_minification.State.TestStateWithMinifiedEvent" + state_path = "tests.units.test_minification.State.TestStateMinifiedEvent" _install_config( states={state_path: "b"}, events={state_path: {"my_handler": "d"}}, include_state_root=True, ) - class TestStateWithMinifiedEvent(State): + class TestStateMinifiedEvent(State): @rx.event def my_handler(self): pass - assert get_state_full_path(TestStateWithMinifiedEvent) == state_path - assert _resolved_event_id(TestStateWithMinifiedEvent, "my_handler") == "d" - - handler = TestStateWithMinifiedEvent.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) - assert event_name == "d" + _, name = get_event_handler_parts( + TestStateMinifiedEvent.event_handlers["my_handler"] + ) + assert name == "d" def test_event_uses_full_name_when_env_disabled( self, temp_minify_json, monkeypatch ): - """Test that event handlers use full names when env var is disabled even with minify.json.""" + """``REFLEX_MINIFY_EVENTS=disabled`` keeps full handler names.""" import reflex as rx from reflex.utils.format import get_event_handler_parts _set_minify_modes(monkeypatch, events=MinifyMode.DISABLED) - state_path = ( - "tests.units.test_minification.State.TestStateWithMinifiedEventDisabled" - ) + state_path = "tests.units.test_minification.State.TestStateMinifiedEventOff" _install_config( states={state_path: "b"}, events={state_path: {"my_handler": "d"}}, include_state_root=True, ) - class TestStateWithMinifiedEventDisabled(State): + class TestStateMinifiedEventOff(State): @rx.event def my_handler(self): pass - assert ( - _resolved_event_id(TestStateWithMinifiedEventDisabled, "my_handler") is None + _, name = get_event_handler_parts( + TestStateMinifiedEventOff.event_handlers["my_handler"] ) - - handler = TestStateWithMinifiedEventDisabled.event_handlers["my_handler"] - _, event_name = get_event_handler_parts(handler) - assert event_name == "my_handler" + assert name == "my_handler" class TestDynamicHandlerMinification: @@ -756,73 +759,34 @@ class LateBornState(State): class TestMinifyModeEnvVars: """Tests for REFLEX_MINIFY_STATES and REFLEX_MINIFY_EVENTS env vars.""" - def test_state_minify_disabled_by_default(self, temp_minify_json): - """Test that state minification is disabled by default.""" - _install_config(states={"test.module.MyState": "a"}) - assert is_state_minify_enabled() is False - - def test_event_minify_disabled_by_default(self, temp_minify_json): - """Test that event minification is disabled by default.""" - _install_config(events={"test.module.MyState": {"handler": "a"}}) - assert is_event_minify_enabled() is False - - def test_state_minify_enabled_with_env_and_config( - self, temp_minify_json, monkeypatch - ): - """Test that state minification is enabled when env var is enabled and config exists.""" - _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) - _install_config(states={"test.module.MyState": "a"}) - assert is_state_minify_enabled() is True + @pytest.mark.parametrize("var", ["REFLEX_MINIFY_STATES", "REFLEX_MINIFY_EVENTS"]) + def test_disabled_by_default(self, temp_minify_json, var): + """Both modes default to disabled even with a config present.""" + _install_config(states={"x": "a"}, events={"x": {"h": "a"}}) + assert is_mode_enabled(var) is False - def test_event_minify_enabled_with_env_and_config( - self, temp_minify_json, monkeypatch - ): - """Test that event minification is enabled when env var is enabled and config exists.""" - _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) - _install_config(events={"test.module.MyState": {"handler": "a"}}) - assert is_event_minify_enabled() is True - - def test_state_minify_disabled_without_config(self, temp_minify_json, monkeypatch): - """Test that state minification is disabled when env var is enabled but no config exists.""" - _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) - clear_config_cache() - assert is_state_minify_enabled() is False - - def test_event_minify_disabled_without_config(self, temp_minify_json, monkeypatch): - """Test that event minification is disabled when env var is enabled but no config exists.""" - _set_minify_modes(monkeypatch, events=MinifyMode.ENABLED) + @pytest.mark.parametrize("var", ["REFLEX_MINIFY_STATES", "REFLEX_MINIFY_EVENTS"]) + def test_enabled_requires_env_and_config(self, temp_minify_json, monkeypatch, var): + """Each mode flips True only when its env var is on AND a config exists.""" + monkeypatch.setenv(getattr(environment, var).name, MinifyMode.ENABLED.value) clear_config_cache() - assert is_event_minify_enabled() is False + assert is_mode_enabled(var) is False # env on, no config + _install_config(states={"x": "a"}, events={"x": {"h": "a"}}) + assert is_mode_enabled(var) is True - def test_independent_state_and_event_toggles(self, temp_minify_json, monkeypatch): - """Test that state and event minification can be toggled independently.""" + def test_modes_toggle_independently(self, temp_minify_json, monkeypatch): + """States can be on while events stay off (or vice versa).""" _set_minify_modes( monkeypatch, states=MinifyMode.ENABLED, events=MinifyMode.DISABLED ) - _install_config( - states={"test.module.MyState": "a"}, - events={"test.module.MyState": {"handler": "a"}}, - ) - assert is_state_minify_enabled() is True - assert is_event_minify_enabled() is False - assert is_minify_enabled() is True - - def test_is_minify_enabled_true_when_either_enabled( - self, temp_minify_json, monkeypatch - ): - """Test that is_minify_enabled returns True when either state or event is enabled.""" - _set_minify_modes( - monkeypatch, states=MinifyMode.DISABLED, events=MinifyMode.ENABLED - ) - _install_config(events={"test.module.MyState": {"handler": "a"}}) + _install_config(states={"x": "a"}, events={"x": {"h": "a"}}) + assert is_mode_enabled("REFLEX_MINIFY_STATES") is True + assert is_mode_enabled("REFLEX_MINIFY_EVENTS") is False assert is_minify_enabled() is True def test_is_minify_enabled_false_when_both_disabled(self, temp_minify_json): - """Test that is_minify_enabled returns False when both are disabled (default).""" - _install_config( - states={"test.module.MyState": "a"}, - events={"test.module.MyState": {"handler": "a"}}, - ) + """Default (no env) → ``is_minify_enabled`` is False even with config.""" + _install_config(states={"x": "a"}, events={"x": {"h": "a"}}) assert is_minify_enabled() is False @@ -1005,62 +969,25 @@ def test_get_handler_name_falls_back_to_default(self): ctx = RegistrationContext.get() assert ctx.get_handler_name(State, "some_handler") == "some_handler" - def test_set_name_resolver_propagates_through_get_name(self, monkeypatch): - """Installing a custom resolver swaps ``BaseState.get_name`` output. - - The resolver overrides only ``State`` so other registered classes - retain their default names — without this scoping, every class would - collapse to one entry in the registry's name-keyed dicts. - """ - - class FixedStateNameResolver: - def resolve_state_name(self, state_cls): - return "fixed_name" if state_cls is State else None - - def resolve_handler_name(self, state_cls, handler_name): - return None - - with _temporary_resolver(FixedStateNameResolver()): + def test_set_name_resolver_propagates_through_get_name(self): + """A custom resolver swaps ``BaseState.get_name`` for the targeted class.""" + with _temporary_resolver(_stub_resolver(state_name="fixed_name")): assert State.get_name() == "fixed_name" def test_set_name_resolver_propagates_through_format_event_handler(self): - """Installing a custom resolver swaps the formatted handler name.""" + """A custom resolver swaps the formatted handler name.""" from reflex.state import OnLoadInternalState from reflex.utils.format import format_event_handler - class HandlerPrefixResolver: - def resolve_state_name(self, state_cls): - return None - - def resolve_handler_name(self, state_cls, handler_name): - return f"px_{handler_name}" - - with _temporary_resolver(HandlerPrefixResolver()): + with _temporary_resolver(_stub_resolver(handler_prefix="px_")): formatted = format_event_handler(OnLoadInternalState.on_load_internal) # pyright: ignore[reportArgumentType] assert formatted.endswith(".px_on_load_internal") def test_resolver_swap_clears_lru_caches(self): - """``set_name_resolver`` invalidates per-class name caches so that - ``get_full_name`` reflects the new resolver immediately. - """ - - class A: - def resolve_state_name(self, state_cls): - return "first" if state_cls is State else None - - def resolve_handler_name(self, state_cls, handler_name): - return None - - class B: - def resolve_state_name(self, state_cls): - return "second" if state_cls is State else None - - def resolve_handler_name(self, state_cls, handler_name): - return None - - with _temporary_resolver(A()) as ctx: + """``set_name_resolver`` invalidates per-class name caches immediately.""" + with _temporary_resolver(_stub_resolver(state_name="first")) as ctx: assert State.get_full_name() == "first" - ctx.set_name_resolver(B()) + ctx.set_name_resolver(_stub_resolver(state_name="second")) assert State.get_full_name() == "second" def test_chain_of_resolvers(self): @@ -1086,14 +1013,8 @@ def resolve_handler_name(self, state_cls, handler_name): return v return None - class FirstOnly: - def resolve_state_name(self, state_cls): - return "from_first" if state_cls is State else None - - def resolve_handler_name(self, state_cls, handler_name): - return None - - with _temporary_resolver(Chain(FirstOnly(), DefaultNameResolver())): + chain = Chain(_stub_resolver(state_name="from_first"), DefaultNameResolver()) + with _temporary_resolver(chain): assert State.get_name() == "from_first" From bc3890aa237d8da5e4b14d64b69713ac63503707 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 6 May 2026 22:07:43 +0200 Subject: [PATCH 67/74] try to improve benchmark --- packages/reflex-base/src/reflex_base/utils/format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index 1e48ef247b2..c0d2f6886f8 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -452,7 +452,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: Returns: ``(state_full_name, handler_name)`` — both resolved. """ - from reflex_base.registry import RegistrationContext + from reflex_base.registry import DefaultNameResolver, RegistrationContext name = handler.fn.__qualname__ if handler.state is None: @@ -461,7 +461,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: state_full_name = handler.state.get_full_name() func_name = name.rpartition(".")[2] ctx = RegistrationContext.try_get() - if ctx is None: + if ctx is None or type(ctx.name_resolver) is DefaultNameResolver: return (state_full_name, func_name) return (state_full_name, ctx.get_handler_name(handler.state, func_name)) From aa328436d23bc0f8874d3c264908914a5a06d4aa Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 6 May 2026 22:22:46 +0200 Subject: [PATCH 68/74] cache event handler names --- packages/reflex-base/src/reflex_base/registry.py | 4 ++++ .../reflex-base/src/reflex_base/utils/format.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 4d4c90eb9eb..8c146ba1341 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -298,11 +298,15 @@ def set_name_resolver(self, resolver: NameResolver) -> None: resolver: The resolver to install. Pass :class:`DefaultNameResolver` to revert to built-in names. """ + from reflex_base.utils.format import _FORMATTED_NAME_CACHE_ATTR + object.__setattr__(self, "name_resolver", resolver) for cls in self.base_states.values(): cls.get_name.cache_clear() cls.get_full_name.cache_clear() cls.get_class_substate.cache_clear() + for reg in self.event_handlers.values(): + reg.handler.__dict__.pop(_FORMATTED_NAME_CACHE_ATTR, None) self.refresh_keys() def refresh_keys(self) -> None: diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index c0d2f6886f8..368174a6a06 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -466,19 +466,29 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]: return (state_full_name, ctx.get_handler_name(handler.state, func_name)) +_FORMATTED_NAME_CACHE_ATTR = "_formatted_name" + + def format_event_handler(handler: EventHandler) -> str: """Format an event handler. + Cached on the handler instance under ``_formatted_name`` to skip the + registry/resolver dispatch on every event. The cache is invalidated by + :meth:`reflex_base.registry.RegistrationContext.set_name_resolver`. + Args: handler: The event handler to format. Returns: The formatted function. """ + cached = handler.__dict__.get(_FORMATTED_NAME_CACHE_ATTR) + if cached is not None: + return cached state, name = get_event_handler_parts(handler) - if state == "": - return name - return f"{state}.{name}" + full = name if state == "" else f"{state}.{name}" + object.__setattr__(handler, _FORMATTED_NAME_CACHE_ATTR, full) + return full def format_event(event_spec: EventSpec) -> str: From 228b8ed526ff2213c13fa4245f3c715ff5514bca Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 9 Jul 2026 23:20:50 +0200 Subject: [PATCH 69/74] fix: record parent path in minify.json state entries so orphaned ids stay reserved per sibling group --- reflex/minify.py | 139 ++++++++---- reflex/reflex.py | 18 +- tests/integration/test_minification.py | 19 +- tests/units/test_minification.py | 296 ++++++++++++++++++++++--- 4 files changed, 393 insertions(+), 79 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 5512836e5b5..a720ba0bd1f 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -26,11 +26,22 @@ SCHEMA_VERSION = 1 +class StateEntry(TypedDict): + """A single state entry in ``minify.json``. + + ``parent`` preserves the sibling scope of an entry even after the class is + renamed or deleted, keeping orphaned ids reserved in their sibling group. + """ + + id: str # minified name, e.g. "a", "bU" + parent: str | None # parent state_path, None for root states + + class MinifyConfig(TypedDict): """Schema for ``minify.json`` (version :data:`SCHEMA_VERSION`).""" version: int - states: dict[str, str] # state_path -> minified_name + states: dict[str, StateEntry] # state_path -> {id, parent} events: dict[str, dict[str, str]] # state_path -> {handler_name -> minified_name} @@ -75,10 +86,16 @@ def _load_minify_config_uncached() -> MinifyConfig | None: msg = f"Invalid {MINIFY_JSON}: 'events' must be a dictionary." raise ValueError(msg) - # Validate states: all values must be strings + # Validate states: all values must be {id: str, parent: str | None} entries for key, value in data["states"].items(): - if not isinstance(value, str): - msg = f"Invalid {MINIFY_JSON}: state '{key}' has non-string id: {value}" + if not isinstance(value, dict) or not isinstance(value.get("id"), str): + msg = f"Invalid {MINIFY_JSON}: state '{key}' must be an object with a string 'id': {value}" + raise ValueError(msg) + parent = value.get("parent") + if parent is not None and not isinstance(parent, str): + msg = ( + f"Invalid {MINIFY_JSON}: state '{key}' has non-string parent: {parent}" + ) raise ValueError(msg) # Validate events: must be dict of dicts with string values @@ -219,9 +236,11 @@ def resolve_state_name(self, state_cls: type[BaseState]) -> str | None: # noqa: cached = self._state_cache.get(state_cls) if cached is not None: return cached - resolved = self.config["states"].get(get_state_full_path(state_cls)) - if resolved is not None: - self._state_cache[state_cls] = resolved + entry = self.config["states"].get(get_state_full_path(state_cls)) + if entry is None: + return None + resolved = entry["id"] + self._state_cache[state_cls] = resolved return resolved def resolve_handler_name( # noqa: D102 @@ -379,6 +398,19 @@ def get_state_full_path(state_cls: type[BaseState]) -> str: return ".".join([module, *class_hierarchy]) +def get_parent_key(state_cls: type[BaseState]) -> str | None: + """Return the config key of ``state_cls``'s parent state. + + Args: + state_cls: The state class. + + Returns: + The parent's :func:`get_state_full_path`, or ``None`` for root states. + """ + parent = state_cls.get_parent_state() + return get_state_full_path(parent) if parent is not None else None + + def collect_all_states( root_state: type[BaseState] | None = None, ) -> list[type[BaseState]]: @@ -427,7 +459,7 @@ def generate_minify_config( Returns: A complete :class:`MinifyConfig`. """ - states: dict[str, str] = {} + states: dict[str, StateEntry] = {} events: dict[str, dict[str, str]] = {} sibling_counter: dict[type[BaseState] | None, int] = {} @@ -438,7 +470,9 @@ def generate_minify_config( sibling_counter[parent] += 1 state_path = get_state_full_path(state_cls) - states[state_path] = int_to_minified_name(state_id) + states[state_path] = StateEntry( + id=int_to_minified_name(state_id), parent=get_parent_key(state_cls) + ) handler_names = sorted(state_cls.event_handlers.keys()) if handler_names: @@ -519,20 +553,33 @@ def validate_minify_config( * ``missing_entries`` — states/events in code but not in the config """ errors: list[str] = [] + warnings: list[str] = [] all_states = collect_all_states(root_state) - # Group sibling states by their actual parent class (not by string-split - # path) since children of the same parent can live in different modules. + # Group siblings by parent key: live entries via their actual parent class, + # orphans via the parent recorded in the config — so id reuse between a + # dead entry and a live sibling is detected. path_to_cls = {get_state_full_path(s): s for s in all_states} - parent_to_pairs: dict[type[BaseState] | None, list[tuple[str, str]]] = {} - for state_path, minified_name in config["states"].items(): + parent_to_pairs: dict[str | None, list[tuple[str, str]]] = {} + for state_path, entry in config["states"].items(): state_cls = path_to_cls.get(state_path) - parent = state_cls.get_parent_state() if state_cls else None - parent_to_pairs.setdefault(parent, []).append((state_path, minified_name)) + if state_cls is not None: + parent_key = get_parent_key(state_cls) + if entry["parent"] != parent_key: + warnings.append( + f"Stale parent for '{state_path}': config has " + f"{entry['parent']!r}, actual is {parent_key!r}. " + "Run 'reflex minify sync' to fix." + ) + label = state_path + else: + parent_key = entry["parent"] + label = f"{state_path} (orphaned)" + parent_to_pairs.setdefault(parent_key, []).append((label, entry["id"])) - for parent, pairs in parent_to_pairs.items(): - parent_name = parent.__name__ if parent else "root" + for parent_key, pairs in parent_to_pairs.items(): + parent_name = parent_key if parent_key is not None else "root" errors.extend( f"Duplicate state_id='{mid}' under '{parent_name}': {paths}" for mid, paths in _find_duplicate_ids(pairs).items() @@ -563,11 +610,11 @@ def validate_minify_config( ) # Check for orphaned entries (in config but not in code) - warnings: list[str] = [ + warnings.extend( f"Orphaned state in config: {state_path}" for state_path in config["states"] if state_path not in code_state_paths - ] + ) code_event_keys: dict[str, set[str]] = {} for state_cls in all_states: @@ -599,7 +646,8 @@ def sync_minify_config( existing_config: The existing configuration to update. root_state: Optional subtree root. ``None`` syncs against every state in the active context. - reassign_deleted: If True, reassign IDs that are no longer in use. + reassign_deleted: If True, fill id gaps left by removed entries. + Retained orphan ids stay reserved unless pruned. prune: If True, remove entries for states/events that no longer exist. Returns: @@ -614,7 +662,10 @@ def sync_minify_config( state_path = get_state_full_path(state_cls) code_events_by_state[state_path] = set(state_cls.event_handlers.keys()) - new_states = dict(existing_config["states"]) + new_states: dict[str, StateEntry] = { + k: StateEntry(id=v["id"], parent=v["parent"]) + for k, v in existing_config["states"].items() + } new_events: dict[str, dict[str, str]] = { k: dict(v) for k, v in existing_config["events"].items() } @@ -634,31 +685,31 @@ def sync_minify_config( # Remove empty event dicts new_events = {k: v for k, v in new_events.items() if v} - # Build a map from actual parent class to existing sibling minified IDs. - # Using the live state classes avoids the string-split bug where children - # of the same parent class defined in different modules get different - # string-based parent paths and are assigned colliding IDs. - parent_cls_to_existing_ids: dict[type[BaseState] | None, set[int]] = {} - for state_cls in all_states: - state_path = get_state_full_path(state_cls) - if state_path in new_states: - parent = state_cls.get_parent_state() - parent_cls_to_existing_ids.setdefault(parent, set()).add( - minified_name_to_int(new_states[state_path]) - ) + path_to_parent_key = {get_state_full_path(s): get_parent_key(s) for s in all_states} + + # Reserve every configured id within its sibling group — orphans under + # their recorded parent, so their ids are never reused. Live entries get + # their stored parent healed to the actual value. + parent_key_to_existing_ids: dict[str | None, set[int]] = {} + for state_path, entry in new_states.items(): + parent_key = path_to_parent_key.get(state_path, entry["parent"]) + entry["parent"] = parent_key + parent_key_to_existing_ids.setdefault(parent_key, set()).add( + minified_name_to_int(entry["id"]) + ) - # Find states that need IDs assigned, grouped by actual parent class. - parent_cls_to_new_children: dict[type[BaseState] | None, list[str]] = {} - for state_cls in all_states: - state_path = get_state_full_path(state_cls) + # Find states that need IDs assigned, grouped by parent key. + parent_key_to_new_children: dict[str | None, list[str]] = {} + for state_path, parent_key in path_to_parent_key.items(): if state_path not in new_states: - parent = state_cls.get_parent_state() - parent_cls_to_new_children.setdefault(parent, []).append(state_path) - - # Assign new state IDs (unique among siblings of the same parent class). - for parent_cls, children in parent_cls_to_new_children.items(): - existing_ids = parent_cls_to_existing_ids.get(parent_cls, set()) - new_states.update(_assign_next_ids(children, existing_ids, reassign_deleted)) + parent_key_to_new_children.setdefault(parent_key, []).append(state_path) + + # Assign new state IDs (unique among siblings of the same parent). + for parent_key, children in parent_key_to_new_children.items(): + existing_ids = parent_key_to_existing_ids.get(parent_key, set()) + assigned = _assign_next_ids(children, existing_ids, reassign_deleted) + for state_path, minified_name in assigned.items(): + new_states[state_path] = StateEntry(id=minified_name, parent=parent_key) # Assign new event IDs (unique within each state). for state_cls in all_states: diff --git a/reflex/reflex.py b/reflex/reflex.py index 0dc27c2f3bc..9fbb7438936 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -991,7 +991,11 @@ def _open_minify_session(*, require_exists: bool = True) -> MinifyConfig | None: if not exists: return None - config = _load_minify_config_uncached() + try: + config = _load_minify_config_uncached() + except ValueError as e: + console.error(str(e)) + raise SystemExit(1) from e if config is None: console.error(f"Failed to load {MINIFY_JSON}.") raise SystemExit(1) @@ -1137,7 +1141,8 @@ def build_state_tree(state_cls: type[BaseState]) -> StateTreeData: A dictionary containing the state tree data. """ state_path = get_state_full_path(state_cls) - state_id = states_map.get(state_path) + state_entry = states_map.get(state_path) + state_id = state_entry["id"] if state_entry is not None else None handler_ids = events_map.get(state_path, {}) handlers: list[EventHandlerData] = [ @@ -1241,10 +1246,11 @@ def minify_lookup(output_json: bool, minified_path: str): config = _open_minify_session() # Build lookup: full_path -> minified_id (None if no entry). - path_to_id = { - get_state_full_path(s): config["states"].get(get_state_full_path(s)) - for s in collect_all_states(State) - } + path_to_id: dict[str, str | None] = {} + for s in collect_all_states(State): + path = get_state_full_path(s) + entry = config["states"].get(path) + path_to_id[path] = entry["id"] if entry is not None else None parts = minified_path.split(".") result_parts = [] diff --git a/tests/integration/test_minification.py b/tests/integration/test_minification.py index 60a3718ea9c..4e405b8d218 100644 --- a/tests/integration/test_minification.py +++ b/tests/integration/test_minification.py @@ -10,7 +10,12 @@ from selenium.webdriver.common.by import By from reflex.environment import MinifyMode, environment -from reflex.minify import MINIFY_JSON, clear_config_cache, int_to_minified_name +from reflex.minify import ( + MINIFY_JSON, + SCHEMA_VERSION, + clear_config_cache, + int_to_minified_name, +) from reflex.testing import AppHarness if TYPE_CHECKING: @@ -75,10 +80,16 @@ def index() -> rx.Component: # Framework state classes (e.g. ``reflex.state.State``) are deliberately # absent — the resolver never minifies them. _MINIFY_CONFIG = { - "version": 1, + "version": SCHEMA_VERSION, "states": { - "minify_enabled.minify_enabled.State.RootState": "k", # 10 - "minify_enabled.minify_enabled.State.RootState.SubState": "l", # 11 + "minify_enabled.minify_enabled.State.RootState": { + "id": "k", # 10 + "parent": "reflex.state.State", + }, + "minify_enabled.minify_enabled.State.RootState.SubState": { + "id": "l", # 11 + "parent": "minify_enabled.minify_enabled.State.RootState", + }, }, "events": { "minify_enabled.minify_enabled.State.RootState": {"increment": "f"}, # 5 diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index d7d9c0744c5..e12899854d0 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -5,6 +5,7 @@ import contextlib import json from collections.abc import Iterator +from pathlib import Path from unittest import mock import pytest @@ -17,9 +18,11 @@ SCHEMA_VERSION, MinifyConfig, MinifyNameResolver, + StateEntry, clear_config_cache, generate_minify_config, get_minify_config, + get_parent_key, get_state_full_path, int_to_minified_name, is_minify_enabled, @@ -67,7 +70,7 @@ def _set_minify_modes( def _install_config( - states: dict[str, str] | None = None, + states: dict[str, str | StateEntry] | None = None, events: dict[str, dict[str, str]] | None = None, *, include_state_root: bool = False, @@ -79,7 +82,8 @@ def _install_config( ``State.get_name.cache_clear()`` etc. by hand. Args: - states: ``state_path -> minified_id`` map. + states: ``state_path -> minified_id`` map. Plain string values are + wrapped into :class:`StateEntry` with ``parent=None``. events: ``state_path -> {handler -> minified_id}`` map. include_state_root: Add ``"reflex.state.State": "a"`` so subclasses of ``State`` resolve through the root entry. @@ -87,9 +91,12 @@ def _install_config( Returns: The saved config. """ - states_map = dict(states or {}) + states_map: dict[str, StateEntry] = { + path: value if isinstance(value, dict) else StateEntry(id=value, parent=None) + for path, value in (states or {}).items() + } if include_state_root: - states_map.setdefault("reflex.state.State", "a") + states_map.setdefault("reflex.state.State", StateEntry(id="a", parent=None)) config: MinifyConfig = { "version": SCHEMA_VERSION, "states": states_map, @@ -278,7 +285,7 @@ def test_save_and_load_config(self, temp_minify_json, monkeypatch): assert is_minify_enabled() is True loaded = get_minify_config() assert loaded is not None - assert loaded["states"]["test.module.MyState"] == "a" + assert loaded["states"]["test.module.MyState"] == {"id": "a", "parent": None} assert loaded["events"]["test.module.MyState"]["handler"] == "a" def test_invalid_version_raises(self, temp_minify_json, monkeypatch): @@ -307,6 +314,23 @@ def test_missing_states_raises(self, temp_minify_json, monkeypatch): with pytest.raises(ValueError, match="'states' must be"): is_mode_enabled("REFLEX_MINIFY_STATES") + def test_flat_string_states_raise(self, temp_minify_json, monkeypatch): + """Test that legacy flat string state values are rejected.""" + _set_minify_modes(monkeypatch, states=MinifyMode.ENABLED) + config = { + "version": SCHEMA_VERSION, + "states": {"test.module.MyState": "a"}, + "events": {}, + } + path = temp_minify_json / MINIFY_JSON + with path.open("w") as f: + json.dump(config, f) + + clear_config_cache() + + with pytest.raises(ValueError, match="must be an object with a string 'id'"): + is_mode_enabled("REFLEX_MINIFY_STATES") + class TestGenerateMinifyConfig: """Tests for generate_minify_config function.""" @@ -317,6 +341,7 @@ def test_generate_for_root_state(self): assert config["version"] == SCHEMA_VERSION assert "reflex.state.State" in config["states"] + assert config["states"]["reflex.state.State"]["parent"] is None # State should have event handlers like set_is_hydrated state_path = "reflex.state.State" assert state_path in config["events"] @@ -340,12 +365,15 @@ class ChildB(ParentState): child_a_path = get_state_full_path(ChildA) child_b_path = get_state_full_path(ChildB) - child_a_id = config["states"].get(child_a_path) - child_b_id = config["states"].get(child_b_path) + child_a_entry = config["states"].get(child_a_path) + child_b_entry = config["states"].get(child_b_path) - assert child_a_id is not None - assert child_b_id is not None - assert child_a_id != child_b_id + assert child_a_entry is not None + assert child_b_entry is not None + assert child_a_entry["id"] != child_b_entry["id"] + parent_path = get_state_full_path(ParentState) + assert child_a_entry["parent"] == parent_path + assert child_b_entry["parent"] == parent_path class TestValidateMinifyConfig: @@ -364,9 +392,11 @@ def test_duplicate_state_ids_detected(self): config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { - "test.Parent": "a", - "test.Parent.ChildA": "b", - "test.Parent.ChildB": "b", # Duplicate! + "test.Parent": StateEntry(id="a", parent=None), + "test.Parent.ChildA": StateEntry(id="b", parent="test.Parent"), + "test.Parent.ChildB": StateEntry( # Duplicate! + id="b", parent="test.Parent" + ), }, "events": {}, } @@ -379,6 +409,86 @@ class Parent(BaseState): assert any("Duplicate state_id='b'" in e for e in errors) + def test_validate_detects_orphan_sibling_collision(self): + """An orphaned entry sharing an id with a live sibling is an error.""" + + class OrphanCollisionParent(BaseState): + pass + + class OrphanCollisionLiveChild(OrphanCollisionParent): + pass + + parent_path = get_state_full_path(OrphanCollisionParent) + live_path = get_state_full_path(OrphanCollisionLiveChild) + orphan_path = f"{parent_path}.DeadChild" + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + live_path: StateEntry(id="b", parent=parent_path), + orphan_path: StateEntry(id="b", parent=parent_path), # collision! + }, + "events": {}, + } + + errors, _warnings, _missing = validate_minify_config( + config, OrphanCollisionParent + ) + + assert any( + "Duplicate state_id='b'" in e and "(orphaned)" in e for e in errors + ), f"Expected orphan collision error, got: {errors}" + + def test_validate_no_cross_group_orphan_false_positive(self): + """An orphan of a different parent may share an id with a root state.""" + + class OrphanNoFalsePositiveRoot(BaseState): + pass + + root_path = get_state_full_path(OrphanNoFalsePositiveRoot) + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + root_path: StateEntry(id="a", parent=None), + # Orphan from an unrelated (deleted) parent, same id "a". + "gone.module.Gone.Child": StateEntry(id="a", parent="gone.module.Gone"), + }, + "events": {}, + } + + errors, warnings, _missing = validate_minify_config( + config, OrphanNoFalsePositiveRoot + ) + + assert not errors, f"Unexpected errors: {errors}" + assert any("Orphaned state" in w for w in warnings) + + def test_validate_warns_stale_parent(self): + """A live entry whose stored parent disagrees with the code is flagged.""" + + class StaleParentParent(BaseState): + pass + + class StaleParentChild(StaleParentParent): + pass + + parent_path = get_state_full_path(StaleParentParent) + child_path = get_state_full_path(StaleParentChild) + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + child_path: StateEntry(id="a", parent="wrong.Path"), + }, + "events": {}, + } + + errors, warnings, _missing = validate_minify_config(config, StaleParentParent) + + assert not errors, f"Unexpected errors: {errors}" + assert any("Stale parent" in w and child_path in w for w in warnings) + class TestSyncMinifyConfig: """Tests for sync_minify_config function.""" @@ -399,9 +509,10 @@ def handler(self): new_config = sync_minify_config(existing_config, TestState) - # Should have added the state + # Should have added the state with its parent key recorded state_path = get_state_full_path(TestState) assert state_path in new_config["states"] + assert new_config["states"][state_path]["parent"] == get_parent_key(TestState) assert state_path in new_config["events"] assert "handler" in new_config["events"][state_path] @@ -417,17 +528,19 @@ def handler_b(self): state_path = get_state_full_path(TestState) - # Start with partial config (using string IDs in v2 format) + # Start with partial config existing_config: MinifyConfig = { "version": SCHEMA_VERSION, - "states": {state_path: "bU"}, # codespell:ignore + "states": { + state_path: StateEntry(id="bU", parent=None) # codespell:ignore + }, "events": {state_path: {"handler_a": "k"}}, # Another arbitrary name } new_config = sync_minify_config(existing_config, TestState) # Existing IDs should be preserved - assert new_config["states"][state_path] == "bU" # codespell:ignore + assert new_config["states"][state_path]["id"] == "bU" # codespell:ignore assert new_config["events"][state_path]["handler_a"] == "k" # New handler should be added with next ID (k=10, so next is l=11) assert "handler_b" in new_config["events"][state_path] @@ -460,8 +573,8 @@ class ChildB(ParentState): existing_config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { - parent_path: "a", - child_a_path: "a", + parent_path: StateEntry(id="a", parent=None), + child_a_path: StateEntry(id="a", parent=parent_path), }, "events": {}, } @@ -472,10 +585,11 @@ class ChildB(ParentState): child_b_path = get_state_full_path(ChildB) assert child_b_path in new_config["states"] assert ( - new_config["states"][child_a_path] != new_config["states"][child_b_path] + new_config["states"][child_a_path]["id"] + != new_config["states"][child_b_path]["id"] ), ( f"Sibling collision: ChildA and ChildB both got " - f"'{new_config['states'][child_a_path]}'" + f"'{new_config['states'][child_a_path]['id']}'" ) def test_validate_detects_sibling_collision(self): @@ -498,9 +612,9 @@ class ChildB(ParentState): bad_config: MinifyConfig = { "version": SCHEMA_VERSION, "states": { - parent_path: "a", - child_a_path: "a", - child_b_path: "a", # collision! + parent_path: StateEntry(id="a", parent=None), + child_a_path: StateEntry(id="a", parent=parent_path), + child_b_path: StateEntry(id="a", parent=parent_path), # collision! }, "events": {}, } @@ -510,6 +624,119 @@ class ChildB(ParentState): f"Expected duplicate ID error, got: {errors}" ) + def test_sync_reserves_orphan_ids(self): + """A new sibling must not reuse the id of an orphaned entry.""" + + class OrphanReserveParent(BaseState): + pass + + class OrphanReserveRenamedChild(OrphanReserveParent): + pass + + parent_path = get_state_full_path(OrphanReserveParent) + orphan_path = f"{parent_path}.OldChild" + + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + orphan_path: StateEntry(id="a", parent=parent_path), + }, + "events": {}, + } + + new_config = sync_minify_config(existing_config, OrphanReserveParent) + + renamed_path = get_state_full_path(OrphanReserveRenamedChild) + assert new_config["states"][orphan_path] == StateEntry( + id="a", parent=parent_path + ) + assert new_config["states"][renamed_path]["id"] != "a" + + def test_sync_reassign_deleted_keeps_orphan_ids_reserved(self): + """``reassign_deleted`` fills gaps but never reuses retained orphan ids.""" + + class OrphanReassignParent(BaseState): + pass + + class OrphanReassignChild(OrphanReassignParent): + pass + + parent_path = get_state_full_path(OrphanReassignParent) + orphan_path = f"{parent_path}.OldChild" + + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + orphan_path: StateEntry(id="a", parent=parent_path), + }, + "events": {}, + } + + new_config = sync_minify_config( + existing_config, OrphanReassignParent, reassign_deleted=True + ) + + child_path = get_state_full_path(OrphanReassignChild) + assert new_config["states"][child_path]["id"] != "a" + + def test_sync_prune_frees_orphan_ids(self): + """``prune`` removes orphans, freeing their ids for reassignment.""" + + class OrphanPruneParent(BaseState): + pass + + class OrphanPruneChild(OrphanPruneParent): + pass + + parent_path = get_state_full_path(OrphanPruneParent) + orphan_path = f"{parent_path}.OldChild" + + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + orphan_path: StateEntry(id="a", parent=parent_path), + }, + "events": {}, + } + + new_config = sync_minify_config( + existing_config, OrphanPruneParent, reassign_deleted=True, prune=True + ) + + child_path = get_state_full_path(OrphanPruneChild) + assert orphan_path not in new_config["states"] + assert new_config["states"][child_path]["id"] == "a" + + def test_sync_heals_stale_parent(self): + """Sync rewrites a live entry's stored parent to the actual value.""" + + class HealParentParent(BaseState): + pass + + class HealParentChild(HealParentParent): + pass + + parent_path = get_state_full_path(HealParentParent) + child_path = get_state_full_path(HealParentChild) + + existing_config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + parent_path: StateEntry(id="a", parent=None), + child_path: StateEntry(id="a", parent="wrong.Path"), + }, + "events": {}, + } + + new_config = sync_minify_config(existing_config, HealParentParent) + + assert new_config["states"][child_path] == StateEntry( + id="a", parent=parent_path + ) + class TestStateMinification: """Tests for state name minification with minify.json.""" @@ -895,6 +1122,21 @@ def test_lookup_fails_without_minify_json(self, temp_minify_json, cli_runner): assert result.exit_code == 1 assert "minify.json does not exist" in result.output + def test_lookup_fails_for_malformed_config( + self, temp_minify_json: Path, cli_runner: CliRunner + ) -> None: + """Test that a malformed minify.json exits cleanly, not with a traceback.""" + from reflex.reflex import cli + + (temp_minify_json / "minify.json").write_text("{not json", encoding="utf-8") + clear_config_cache() + + result = cli_runner.invoke(cli, ["minify", "lookup", "a.b"]) + + assert result.exit_code == 1 + assert isinstance(result.exception, SystemExit) + assert "Invalid JSON" in result.output + def test_lookup_fails_for_invalid_path(self, temp_minify_json, cli_runner): """Test that lookup fails for non-existent minified path.""" from reflex.reflex import cli @@ -1048,7 +1290,11 @@ class UserStateResolverCacheTest(State): config: MinifyConfig = { "version": SCHEMA_VERSION, - "states": {get_state_full_path(UserStateResolverCacheTest): "rs"}, + "states": { + get_state_full_path(UserStateResolverCacheTest): StateEntry( + id="rs", parent=None + ) + }, "events": {}, } resolver = MinifyNameResolver( From af4219fac8cb8832bedfa843e847c441eb72db75 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 9 Jul 2026 23:24:53 +0200 Subject: [PATCH 70/74] fix codespell --- reflex/minify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/minify.py b/reflex/minify.py index a720ba0bd1f..2d6c20a2b15 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -33,7 +33,7 @@ class StateEntry(TypedDict): renamed or deleted, keeping orphaned ids reserved in their sibling group. """ - id: str # minified name, e.g. "a", "bU" + id: str # minified name, e.g. "a" parent: str | None # parent state_path, None for root states From c5cceb690d637a00e14c684839ca0383a716d0a4 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Mon, 20 Jul 2026 20:33:00 +0200 Subject: [PATCH 71/74] fix: prevent state definition before minify is active --- reflex/state.py | 6 +++++ tests/units/test_minification.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/reflex/state.py b/reflex/state.py index f6149d8bd6c..fc3b66f9300 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -76,12 +76,18 @@ from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy from reflex.istate.proxy import MutableProxy, is_mutable_type from reflex.istate.storage import ClientStorageBase +from reflex.minify import ensure_minify_resolver_for_active_context from reflex.utils import console, format, prerequisites, types from reflex.utils.exec import is_testing_env if TYPE_CHECKING: from reflex_base.components.component import Component +# Must run before any state class registers — a later install renames states +# while names captured by VarData.from_state keep the old value (e.g. granian +# prod workers import the app module directly, bypassing get_app). +ensure_minify_resolver_for_active_context() + Delta = dict[str, dict[str, Any]] DeltaMapping = Mapping[str, Mapping[str, Any]] diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index e12899854d0..229eeab01b4 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -1336,3 +1336,43 @@ def test_from_disk_handles_malformed_config(self, temp_minify_json): assert resolver.config is None assert resolver.resolve_state_name(State) is None assert resolver.resolve_handler_name(State, "any") is None + + +class TestImportTimeInstall: + """Importing reflex.state must install the resolver before states register.""" + + def test_resolver_active_before_any_state_registers(self, tmp_path): + """A fresh interpreter registers user states under their minified name.""" + import os + import subprocess + import sys + + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": { + "myapp.State.Foo": StateEntry(id="f", parent="reflex.state.State") + }, + "events": {}, + } + (tmp_path / MINIFY_JSON).write_text(json.dumps(config)) + (tmp_path / "myapp.py").write_text( + "\n".join([ + "from reflex_base.registry import RegistrationContext", + "import reflex.state", + "resolver = RegistrationContext.ensure_context().name_resolver", + "assert type(resolver).__name__ == 'MinifyNameResolver', resolver", + "class Foo(reflex.state.State):", + " pass", + "assert Foo.get_name() == 'f', Foo.get_name()", + ]) + ) + + result = subprocess.run( + [sys.executable, "-c", "import myapp"], + cwd=tmp_path, + env={**os.environ, "REFLEX_MINIFY_STATES": MinifyMode.ENABLED.value}, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr From 1bafa2d0f907f3486dc51b3666e8122f250f9d56 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Mon, 20 Jul 2026 20:39:11 +0200 Subject: [PATCH 72/74] fix ruff --- tests/units/test_minification.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index 229eeab01b4..c65f712a0ec 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -1346,6 +1346,7 @@ def test_resolver_active_before_any_state_registers(self, tmp_path): import os import subprocess import sys + import textwrap config: MinifyConfig = { "version": SCHEMA_VERSION, @@ -1356,15 +1357,15 @@ def test_resolver_active_before_any_state_registers(self, tmp_path): } (tmp_path / MINIFY_JSON).write_text(json.dumps(config)) (tmp_path / "myapp.py").write_text( - "\n".join([ - "from reflex_base.registry import RegistrationContext", - "import reflex.state", - "resolver = RegistrationContext.ensure_context().name_resolver", - "assert type(resolver).__name__ == 'MinifyNameResolver', resolver", - "class Foo(reflex.state.State):", - " pass", - "assert Foo.get_name() == 'f', Foo.get_name()", - ]) + textwrap.dedent(""" + from reflex_base.registry import RegistrationContext + import reflex.state + resolver = RegistrationContext.ensure_context().name_resolver + assert type(resolver).__name__ == "MinifyNameResolver", resolver + class Foo(reflex.state.State): + pass + assert Foo.get_name() == "f", Foo.get_name() + """) ) result = subprocess.run( From c3e0f1c00d3884ab477b5076a900d9b547ad9c7b Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Mon, 20 Jul 2026 21:27:18 +0200 Subject: [PATCH 73/74] greptile --- reflex/minify.py | 12 +++++++++--- reflex/reflex.py | 6 ++++-- tests/units/test_minification.py | 25 +++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 2d6c20a2b15..4bef1ac1a86 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -591,16 +591,22 @@ def validate_minify_config( for mid, handlers in _find_duplicate_ids(state_events.items()).items() ) - # Check for missing states (in code but not in config) code_state_paths = {get_state_full_path(s) for s in all_states} + + # Only user-defined states are minified at runtime (see + # ``MinifyNameResolver``), so framework states absent from a user-curated + # config are not "missing" — skip them when reporting missing entries. + user_states = [s for s in all_states if not _is_framework_state(s)] + + # Check for missing states (in code but not in config) missing: list[str] = [ f"state:{state_path}" - for state_path in code_state_paths + for state_path in (get_state_full_path(s) for s in user_states) if state_path not in config["states"] ] # Check for missing events (in code but not in config) - for state_cls in all_states: + for state_cls in user_states: state_path = get_state_full_path(state_cls) state_events = config["events"].get(state_path, {}) missing.extend( diff --git a/reflex/reflex.py b/reflex/reflex.py index 9fbb7438936..db7d28ae34b 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -1193,7 +1193,9 @@ def print_state_tree( has_substates = len(substates) > 0 if handlers: - console.log(f"{child_prefix}|-- Event Handlers:") + console.log( + f"{child_prefix}{'|' if has_substates else '`'}-- Event Handlers:" + ) handler_prefix = child_prefix + ("| " if has_substates else " ") for i, handler in enumerate(handlers): is_last_handler = i == len(handlers) - 1 @@ -1217,7 +1219,7 @@ def print_state_tree( if output_json: import json - console.log(json.dumps(tree_data, indent=2)) + click.echo(json.dumps(tree_data, indent=2)) else: if config is not None: console.log("State Tree (minify.json loaded)") diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index c65f712a0ec..d16812b8f3e 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -464,6 +464,31 @@ class OrphanNoFalsePositiveRoot(BaseState): assert not errors, f"Unexpected errors: {errors}" assert any("Orphaned state" in w for w in warnings) + def test_validate_ignores_framework_states_in_missing(self): + """Framework states/handlers omitted from a user-only config aren't missing.""" + + class UserOnlyState(State): + def do_thing(self): + pass + + user_path = get_state_full_path(UserOnlyState) + # Config intentionally omits framework ``reflex.state.State`` entries. + config: MinifyConfig = { + "version": SCHEMA_VERSION, + "states": {user_path: StateEntry(id="a", parent="reflex.state.State")}, + "events": {user_path: {"do_thing": "a"}}, + } + + _errors, _warnings, missing = validate_minify_config(config, State) + + # Framework ``reflex.state.State`` and its handlers must not be flagged + # missing even though the user-only config omits them. + assert not any(entry.startswith("state:reflex.state.") for entry in missing) + assert not any(entry.startswith("event:reflex.state.") for entry in missing) + # The user state itself is present, so it isn't reported missing either. + assert f"state:{user_path}" not in missing + assert f"event:{user_path}.do_thing" not in missing + def test_validate_warns_stale_parent(self): """A live entry whose stored parent disagrees with the code is flagged.""" From 44fefa1182e4e3b45f3d1ac1ffb6eefeb21a4fc1 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 21 Jul 2026 01:16:20 +0200 Subject: [PATCH 74/74] cleanup --- reflex/minify.py | 4 +--- tests/units/test_minification.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/reflex/minify.py b/reflex/minify.py index 4bef1ac1a86..87db86aaef0 100644 --- a/reflex/minify.py +++ b/reflex/minify.py @@ -593,9 +593,7 @@ def validate_minify_config( code_state_paths = {get_state_full_path(s) for s in all_states} - # Only user-defined states are minified at runtime (see - # ``MinifyNameResolver``), so framework states absent from a user-curated - # config are not "missing" — skip them when reporting missing entries. + # Framework states are never minified, so don't report them as missing. user_states = [s for s in all_states if not _is_framework_state(s)] # Check for missing states (in code but not in config) diff --git a/tests/units/test_minification.py b/tests/units/test_minification.py index d16812b8f3e..396246e169d 100644 --- a/tests/units/test_minification.py +++ b/tests/units/test_minification.py @@ -472,7 +472,7 @@ def do_thing(self): pass user_path = get_state_full_path(UserOnlyState) - # Config intentionally omits framework ``reflex.state.State`` entries. + # Config omits framework reflex.state.State entries. config: MinifyConfig = { "version": SCHEMA_VERSION, "states": {user_path: StateEntry(id="a", parent="reflex.state.State")}, @@ -481,11 +481,8 @@ def do_thing(self): _errors, _warnings, missing = validate_minify_config(config, State) - # Framework ``reflex.state.State`` and its handlers must not be flagged - # missing even though the user-only config omits them. assert not any(entry.startswith("state:reflex.state.") for entry in missing) assert not any(entry.startswith("event:reflex.state.") for entry in missing) - # The user state itself is present, so it isn't reported missing either. assert f"state:{user_path}" not in missing assert f"event:{user_path}.do_thing" not in missing