reflex 0.8.11__py3-none-any.whl → 0.8.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

reflex/reflex.py CHANGED
@@ -422,6 +422,13 @@ def compile(dry: bool, rich: bool):
422
422
  default=constants.Env.PROD.value,
423
423
  help="The environment to export the app in.",
424
424
  )
425
+ @click.option(
426
+ "--exclude-from-backend",
427
+ "backend_excluded_dirs",
428
+ multiple=True,
429
+ type=click.Path(exists=True, path_type=Path, resolve_path=True),
430
+ help="Files or directories to exclude from the backend zip. Can be used multiple times.",
431
+ )
425
432
  def export(
426
433
  zip: bool,
427
434
  frontend_only: bool,
@@ -429,6 +436,7 @@ def export(
429
436
  zip_dest_dir: str,
430
437
  upload_db_file: bool,
431
438
  env: LITERAL_ENV,
439
+ backend_excluded_dirs: tuple[Path, ...] = (),
432
440
  ):
433
441
  """Export the app to a zip file."""
434
442
  from reflex.utils import export as export_utils
@@ -455,6 +463,7 @@ def export(
455
463
  upload_db_file=upload_db_file,
456
464
  env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
457
465
  loglevel=config.loglevel.subprocess_level(),
466
+ backend_excluded_dirs=backend_excluded_dirs,
458
467
  )
459
468
 
460
469
 
@@ -660,6 +669,13 @@ def makemigrations(message: str | None):
660
669
  "--config",
661
670
  help="path to the config file",
662
671
  )
672
+ @click.option(
673
+ "--exclude-from-backend",
674
+ "backend_excluded_dirs",
675
+ multiple=True,
676
+ type=click.Path(exists=True, path_type=Path, resolve_path=True),
677
+ help="Files or directories to exclude from the backend zip. Can be used multiple times.",
678
+ )
663
679
  def deploy(
664
680
  app_name: str | None,
665
681
  app_id: str | None,
@@ -673,6 +689,7 @@ def deploy(
673
689
  project_name: str | None,
674
690
  token: str | None,
675
691
  config_path: str | None,
692
+ backend_excluded_dirs: tuple[Path, ...] = (),
676
693
  ):
677
694
  """Deploy the app to the Reflex hosting service."""
678
695
  from reflex_cli.utils import dependency
@@ -721,6 +738,7 @@ def deploy(
721
738
  zipping=zipping,
722
739
  loglevel=config.loglevel.subprocess_level(),
723
740
  upload_db_file=upload_db,
741
+ backend_excluded_dirs=backend_excluded_dirs,
724
742
  )
725
743
  ),
726
744
  regions=list(region),
reflex/state.py CHANGED
@@ -41,7 +41,7 @@ from reflex.event import (
41
41
  from reflex.istate import HANDLED_PICKLE_ERRORS, debug_failed_pickles
42
42
  from reflex.istate.data import RouterData
43
43
  from reflex.istate.proxy import ImmutableMutableProxy as ImmutableMutableProxy
44
- from reflex.istate.proxy import MutableProxy, StateProxy
44
+ from reflex.istate.proxy import MutableProxy, StateProxy, is_mutable_type
45
45
  from reflex.istate.storage import ClientStorageBase
46
46
  from reflex.model import Model
47
47
  from reflex.utils import console, format, prerequisites, types
@@ -1359,7 +1359,7 @@ class BaseState(EvenMoreBasicBaseState):
1359
1359
  if parent_state is not None:
1360
1360
  return getattr(parent_state, name)
1361
1361
 
1362
- if MutableProxy._is_mutable_type(value) and (
1362
+ if is_mutable_type(type(value)) and (
1363
1363
  name in super().__getattribute__("base_vars") or name in backend_vars
1364
1364
  ):
1365
1365
  # track changes in mutable containers (list, dict, set, etc)
reflex/style.py CHANGED
@@ -120,9 +120,11 @@ def convert_item(
120
120
  Raises:
121
121
  ReflexError: If an EventHandler is used as a style value
122
122
  """
123
- if isinstance(style_item, EventHandler):
123
+ from reflex.components.component import BaseComponent
124
+
125
+ if isinstance(style_item, (EventHandler, BaseComponent)):
124
126
  msg = (
125
- "EventHandlers cannot be used as style values. "
127
+ f"{type(style_item)} cannot be used as style values. "
126
128
  "Please use a Var or a literal value."
127
129
  )
128
130
  raise ReflexError(msg)
reflex/testing.py CHANGED
@@ -47,6 +47,7 @@ from reflex.state import (
47
47
  )
48
48
  from reflex.utils import console, js_runtimes
49
49
  from reflex.utils.export import export
50
+ from reflex.utils.token_manager import TokenManager
50
51
  from reflex.utils.types import ASGIApp
51
52
 
52
53
  try:
@@ -774,6 +775,19 @@ class AppHarness:
774
775
  self.app_instance._state_manager = app_state_manager
775
776
  await self.state_manager.close()
776
777
 
778
+ def token_manager(self) -> TokenManager:
779
+ """Get the token manager for the app instance.
780
+
781
+ Returns:
782
+ The current token_manager attached to the app's EventNamespace.
783
+ """
784
+ assert self.app_instance is not None
785
+ app_event_namespace = self.app_instance.event_namespace
786
+ assert app_event_namespace is not None
787
+ app_token_manager = app_event_namespace._token_manager
788
+ assert app_token_manager is not None
789
+ return app_token_manager
790
+
777
791
  def poll_for_content(
778
792
  self,
779
793
  element: WebElement,
reflex/utils/build.py CHANGED
@@ -26,14 +26,14 @@ def set_env_json():
26
26
 
27
27
 
28
28
  def _zip(
29
+ *,
29
30
  component_name: constants.ComponentName,
30
- target: str | Path,
31
- root_dir: str | Path,
32
- exclude_venv_dirs: bool,
33
- upload_db_file: bool = False,
34
- dirs_to_exclude: set[str] | None = None,
35
- files_to_exclude: set[str] | None = None,
36
- top_level_dirs_to_exclude: set[str] | None = None,
31
+ target: Path,
32
+ root_directory: Path,
33
+ exclude_venv_directories: bool,
34
+ include_db_file: bool = False,
35
+ directory_names_to_exclude: set[str] | None = None,
36
+ files_to_exclude: set[Path] | None = None,
37
37
  globs_to_include: list[str] | None = None,
38
38
  ) -> None:
39
39
  """Zip utility function.
@@ -41,49 +41,62 @@ def _zip(
41
41
  Args:
42
42
  component_name: The name of the component: backend or frontend.
43
43
  target: The target zip file.
44
- root_dir: The root directory to zip.
45
- exclude_venv_dirs: Whether to exclude venv directories.
46
- upload_db_file: Whether to include local sqlite db files.
47
- dirs_to_exclude: The directories to exclude.
44
+ root_directory: The root directory to zip.
45
+ exclude_venv_directories: Whether to exclude venv directories.
46
+ include_db_file: Whether to include local sqlite db files.
47
+ directory_names_to_exclude: The directory names to exclude.
48
48
  files_to_exclude: The files to exclude.
49
- top_level_dirs_to_exclude: The top level directory names immediately under root_dir to exclude. Do not exclude folders by these names further in the sub-directories.
50
- globs_to_include: Apply these globs from the root_dir and always include them in the zip.
49
+ globs_to_include: Apply these globs from the root_directory and always include them in the zip.
51
50
 
52
51
  """
53
52
  target = Path(target)
54
- root_dir = Path(root_dir)
55
- dirs_to_exclude = dirs_to_exclude or set()
53
+ root_directory = Path(root_directory).resolve()
54
+ directory_names_to_exclude = directory_names_to_exclude or set()
56
55
  files_to_exclude = files_to_exclude or set()
57
- files_to_zip: list[str] = []
56
+ files_to_zip: list[Path] = []
58
57
  # Traverse the root directory in a top-down manner. In this traversal order,
59
58
  # we can modify the dirs list in-place to remove directories we don't want to include.
60
- for root, dirs, files in os.walk(root_dir, topdown=True, followlinks=True):
61
- root = Path(root)
59
+ for directory_path, subdirectories_names, subfiles_names in os.walk(
60
+ root_directory, topdown=True, followlinks=True
61
+ ):
62
+ directory_path = Path(directory_path).resolve()
62
63
  # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
63
- dirs[:] = [
64
- d
65
- for d in dirs
66
- if (basename := Path(d).resolve().name) not in dirs_to_exclude
67
- and not basename.startswith(".")
68
- and (not exclude_venv_dirs or not _looks_like_venv_dir(root / d))
64
+ subdirectories_names[:] = [
65
+ subdirectory_name
66
+ for subdirectory_name in subdirectories_names
67
+ if subdirectory_name not in directory_names_to_exclude
68
+ and not any(
69
+ (directory_path / subdirectory_name).samefile(exclude)
70
+ for exclude in files_to_exclude
71
+ if exclude.exists()
72
+ )
73
+ and not subdirectory_name.startswith(".")
74
+ and (
75
+ not exclude_venv_directories
76
+ or not _looks_like_venv_directory(directory_path / subdirectory_name)
77
+ )
69
78
  ]
70
- # If we are at the top level with root_dir, exclude the top level dirs.
71
- if top_level_dirs_to_exclude and root == root_dir:
72
- dirs[:] = [d for d in dirs if d not in top_level_dirs_to_exclude]
73
79
  # Modify the files in-place so the hidden files and db files are excluded.
74
- files[:] = [
75
- f
76
- for f in files
77
- if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
80
+ subfiles_names[:] = [
81
+ subfile_name
82
+ for subfile_name in subfiles_names
83
+ if not subfile_name.startswith(".")
84
+ and (include_db_file or not subfile_name.endswith(".db"))
78
85
  ]
79
86
  files_to_zip += [
80
- str(root / file) for file in files if file not in files_to_exclude
87
+ directory_path / subfile_name
88
+ for subfile_name in subfiles_names
89
+ if not any(
90
+ (directory_path / subfile_name).samefile(excluded_file)
91
+ for excluded_file in files_to_exclude
92
+ if excluded_file.exists()
93
+ )
81
94
  ]
82
95
  if globs_to_include:
83
96
  for glob in globs_to_include:
84
97
  files_to_zip += [
85
- str(file)
86
- for file in root_dir.glob(glob)
98
+ file
99
+ for file in root_directory.glob(glob)
87
100
  if file.name not in files_to_exclude
88
101
  ]
89
102
  # Create a progress bar for zipping the component.
@@ -100,14 +113,15 @@ def _zip(
100
113
  for file in files_to_zip:
101
114
  console.debug(f"{target}: {file}", progress=progress)
102
115
  progress.advance(task)
103
- zipf.write(file, Path(file).relative_to(root_dir))
116
+ zipf.write(file, Path(file).relative_to(root_directory))
104
117
 
105
118
 
106
119
  def zip_app(
107
120
  frontend: bool = True,
108
121
  backend: bool = True,
109
122
  zip_dest_dir: str | Path | None = None,
110
- upload_db_file: bool = False,
123
+ include_db_file: bool = False,
124
+ backend_excluded_dirs: tuple[Path, ...] = (),
111
125
  ):
112
126
  """Zip up the app.
113
127
 
@@ -115,41 +129,41 @@ def zip_app(
115
129
  frontend: Whether to zip up the frontend app.
116
130
  backend: Whether to zip up the backend app.
117
131
  zip_dest_dir: The directory to export the zip file to.
118
- upload_db_file: Whether to upload the database file.
132
+ include_db_file: Whether to include the database file.
133
+ backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
119
134
  """
120
135
  zip_dest_dir = zip_dest_dir or Path.cwd()
121
136
  zip_dest_dir = Path(zip_dest_dir)
122
137
  files_to_exclude = {
123
- constants.ComponentName.FRONTEND.zip(),
124
- constants.ComponentName.BACKEND.zip(),
138
+ Path(constants.ComponentName.FRONTEND.zip()).resolve(),
139
+ Path(constants.ComponentName.BACKEND.zip()).resolve(),
125
140
  }
126
141
 
127
142
  if frontend:
128
143
  _zip(
129
144
  component_name=constants.ComponentName.FRONTEND,
130
145
  target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(),
131
- root_dir=prerequisites.get_web_dir() / constants.Dirs.STATIC,
146
+ root_directory=prerequisites.get_web_dir() / constants.Dirs.STATIC,
132
147
  files_to_exclude=files_to_exclude,
133
- exclude_venv_dirs=False,
148
+ exclude_venv_directories=False,
134
149
  )
135
150
 
136
151
  if backend:
137
152
  _zip(
138
153
  component_name=constants.ComponentName.BACKEND,
139
154
  target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
140
- root_dir=Path.cwd(),
141
- dirs_to_exclude={"__pycache__"},
142
- files_to_exclude=files_to_exclude,
143
- top_level_dirs_to_exclude={"assets"},
144
- exclude_venv_dirs=True,
145
- upload_db_file=upload_db_file,
155
+ root_directory=Path.cwd(),
156
+ directory_names_to_exclude={"__pycache__"},
157
+ files_to_exclude=files_to_exclude | set(backend_excluded_dirs),
158
+ exclude_venv_directories=True,
159
+ include_db_file=include_db_file,
146
160
  globs_to_include=[
147
161
  str(Path(constants.Dirs.WEB) / constants.Dirs.BACKEND / "*")
148
162
  ],
149
163
  )
150
164
 
151
165
 
152
- def _duplicate_index_html_to_parent_dir(directory: Path):
166
+ def _duplicate_index_html_to_parent_directory(directory: Path):
153
167
  """Duplicate index.html in the child directories to the given directory.
154
168
 
155
169
  This makes accessing /route and /route/ work in production.
@@ -169,7 +183,7 @@ def _duplicate_index_html_to_parent_dir(directory: Path):
169
183
  else:
170
184
  console.debug(f"Skipping {index_html}, already exists at {target}")
171
185
  # Recursively call this function for the child directory.
172
- _duplicate_index_html_to_parent_dir(child)
186
+ _duplicate_index_html_to_parent_directory(child)
173
187
 
174
188
 
175
189
  def build():
@@ -200,7 +214,7 @@ def build():
200
214
  },
201
215
  )
202
216
  processes.show_progress("Creating Production Build", process, checkpoints)
203
- _duplicate_index_html_to_parent_dir(wdir / constants.Dirs.STATIC)
217
+ _duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC)
204
218
  path_ops.cp(
205
219
  wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK,
206
220
  wdir / constants.Dirs.STATIC / "404.html",
@@ -247,6 +261,6 @@ def setup_frontend_prod(
247
261
  build()
248
262
 
249
263
 
250
- def _looks_like_venv_dir(dir_to_check: str | Path) -> bool:
251
- dir_to_check = Path(dir_to_check)
252
- return (dir_to_check / "pyvenv.cfg").exists()
264
+ def _looks_like_venv_directory(directory_to_check: str | Path) -> bool:
265
+ directory_to_check = Path(directory_to_check)
266
+ return (directory_to_check / "pyvenv.cfg").exists()
reflex/utils/console.py CHANGED
@@ -22,6 +22,7 @@ from reflex.utils.decorator import once
22
22
 
23
23
  # Console for pretty printing.
24
24
  _console = Console()
25
+ _console_stderr = Console(stderr=True)
25
26
 
26
27
  # The current log level.
27
28
  _LOG_LEVEL = LogLevel.INFO
@@ -96,6 +97,21 @@ def print(msg: str, *, dedupe: bool = False, **kwargs):
96
97
  _console.print(msg, **kwargs)
97
98
 
98
99
 
100
+ def _print_stderr(msg: str, *, dedupe: bool = False, **kwargs):
101
+ """Print a message to stderr.
102
+
103
+ Args:
104
+ msg: The message to print.
105
+ dedupe: If True, suppress multiple console logs of print message.
106
+ kwargs: Keyword arguments to pass to the print function.
107
+ """
108
+ if dedupe:
109
+ if msg in _EMITTED_PRINTS:
110
+ return
111
+ _EMITTED_PRINTS.add(msg)
112
+ _console_stderr.print(msg, **kwargs)
113
+
114
+
99
115
  @once
100
116
  def log_file_console():
101
117
  """Create a console that logs to a file.
@@ -342,7 +358,7 @@ def error(msg: str, *, dedupe: bool = False, **kwargs):
342
358
  if msg in _EMITTED_ERRORS:
343
359
  return
344
360
  _EMITTED_ERRORS.add(msg)
345
- print(f"[red]{msg}[/red]", **kwargs)
361
+ _print_stderr(f"[red]{msg}[/red]", **kwargs)
346
362
  if should_use_log_file_console():
347
363
  print_to_log_file(f"[red]{msg}[/red]", **kwargs)
348
364
 
reflex/utils/exec.py CHANGED
@@ -497,6 +497,7 @@ HOTRELOAD_IGNORE_EXTENSIONS = (
497
497
  "sh",
498
498
  "bash",
499
499
  "log",
500
+ "db",
500
501
  )
501
502
 
502
503
  HOTRELOAD_IGNORE_PATTERNS = (
@@ -524,7 +525,7 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
524
525
  from granian.log import LogLevels
525
526
  from granian.server import Server as Granian
526
527
 
527
- from reflex.environment import _paths_from_environment
528
+ from reflex.environment import _load_dotenv_from_env
528
529
 
529
530
  granian_app = Granian(
530
531
  target=get_app_instance_from_file(),
@@ -538,10 +539,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
538
539
  reload_ignore_worker_failure=True,
539
540
  reload_ignore_patterns=HOTRELOAD_IGNORE_PATTERNS,
540
541
  reload_tick=100,
541
- env_files=_paths_from_environment() or None,
542
542
  workers_kill_timeout=2,
543
543
  )
544
544
 
545
+ granian_app.on_reload(_load_dotenv_from_env)
546
+
545
547
  granian_app.serve()
546
548
 
547
549
 
reflex/utils/export.py CHANGED
@@ -18,6 +18,7 @@ def export(
18
18
  deploy_url: str | None = None,
19
19
  env: constants.Env = constants.Env.PROD,
20
20
  loglevel: constants.LogLevel = console._LOG_LEVEL,
21
+ backend_excluded_dirs: tuple[Path, ...] = (),
21
22
  ):
22
23
  """Export the app to a zip file.
23
24
 
@@ -31,6 +32,7 @@ def export(
31
32
  deploy_url: The deploy URL to use. Defaults to None.
32
33
  env: The environment to use. Defaults to constants.Env.PROD.
33
34
  loglevel: The log level to use. Defaults to console._LOG_LEVEL.
35
+ backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
34
36
  """
35
37
  config = get_config()
36
38
 
@@ -70,7 +72,8 @@ def export(
70
72
  frontend=frontend,
71
73
  backend=backend,
72
74
  zip_dest_dir=zip_dest_dir,
73
- upload_db_file=upload_db_file,
75
+ include_db_file=upload_db_file,
76
+ backend_excluded_dirs=backend_excluded_dirs,
74
77
  )
75
78
 
76
79
  # Post a telemetry event.
@@ -66,6 +66,16 @@ class TokenManager(ABC):
66
66
 
67
67
  return LocalTokenManager()
68
68
 
69
+ async def disconnect_all(self):
70
+ """Disconnect all tracked tokens when the server is going down."""
71
+ token_sid_pairs: set[tuple[str, str]] = set(self.token_to_sid.items())
72
+ token_sid_pairs.update(
73
+ ((token, sid) for sid, token in self.sid_to_token.items())
74
+ )
75
+ # Perform the disconnection logic here
76
+ for token, sid in token_sid_pairs:
77
+ await self.disconnect_token(token, sid)
78
+
69
79
 
70
80
  class LocalTokenManager(TokenManager):
71
81
  """Token manager using local in-memory dictionaries (single worker)."""
reflex/utils/types.py CHANGED
@@ -6,6 +6,7 @@ import dataclasses
6
6
  import sys
7
7
  import types
8
8
  from collections.abc import Callable, Iterable, Mapping, Sequence
9
+ from enum import Enum
9
10
  from functools import cached_property, lru_cache
10
11
  from types import GenericAlias
11
12
  from typing import ( # noqa: UP035
@@ -1241,6 +1242,7 @@ IMMUTABLE_TYPES = (
1241
1242
  frozenset,
1242
1243
  tuple,
1243
1244
  type(None),
1245
+ Enum,
1244
1246
  )
1245
1247
 
1246
1248
 
reflex/vars/base.py CHANGED
@@ -2345,7 +2345,7 @@ class ComputedVar(Var[RETURN_TYPE]):
2345
2345
  def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None:
2346
2346
  if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False):
2347
2347
  console.error(
2348
- f"Computed var '{type(instance).__name__}.{self._js_expr}' must return"
2348
+ f"Computed var '{type(instance).__name__}.{self._name}' must return"
2349
2349
  f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}."
2350
2350
  )
2351
2351
 
@@ -2395,7 +2395,7 @@ class ComputedVar(Var[RETURN_TYPE]):
2395
2395
  except Exception as e:
2396
2396
  console.warn(
2397
2397
  "Failed to automatically determine dependencies for computed var "
2398
- f"{objclass.__name__}.{self._js_expr}: {e}. "
2398
+ f"{objclass.__name__}.{self._name}: {e}. "
2399
2399
  "Provide static_deps and set auto_deps=False to suppress this warning."
2400
2400
  )
2401
2401
  return d
reflex/vars/object.py CHANGED
@@ -479,14 +479,9 @@ def object_keys_operation(value: ObjectVar):
479
479
  Returns:
480
480
  The keys of the object.
481
481
  """
482
- if not types.is_optional(value._var_type):
483
- return var_operation_return(
484
- js_expression=f"Object.keys({value})",
485
- var_type=list[str],
486
- )
487
482
  return var_operation_return(
488
- js_expression=f"((value) => value ?? undefined === undefined ? undefined : Object.keys(value))({value})",
489
- var_type=(list[str] | None),
483
+ js_expression=f"Object.keys({value} ?? {{}})",
484
+ var_type=list[str],
490
485
  )
491
486
 
492
487
 
@@ -500,14 +495,9 @@ def object_values_operation(value: ObjectVar):
500
495
  Returns:
501
496
  The values of the object.
502
497
  """
503
- if not types.is_optional(value._var_type):
504
- return var_operation_return(
505
- js_expression=f"Object.values({value})",
506
- var_type=list[value._value_type()],
507
- )
508
498
  return var_operation_return(
509
- js_expression=f"((value) => value ?? undefined === undefined ? undefined : Object.values(value))({value})",
510
- var_type=(list[value._value_type()] | None),
499
+ js_expression=f"Object.values({value} ?? {{}})",
500
+ var_type=list[value._value_type()],
511
501
  )
512
502
 
513
503
 
@@ -521,14 +511,9 @@ def object_entries_operation(value: ObjectVar):
521
511
  Returns:
522
512
  The entries of the object.
523
513
  """
524
- if not types.is_optional(value._var_type):
525
- return var_operation_return(
526
- js_expression=f"Object.entries({value})",
527
- var_type=list[tuple[str, value._value_type()]],
528
- )
529
514
  return var_operation_return(
530
- js_expression=f"((value) => value ?? undefined === undefined ? undefined : Object.entries(value))({value})",
531
- var_type=(list[tuple[str, value._value_type()]] | None),
515
+ js_expression=f"Object.entries({value} ?? {{}})",
516
+ var_type=list[tuple[str, value._value_type()]],
532
517
  )
533
518
 
534
519
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reflex
3
- Version: 0.8.11
3
+ Version: 0.8.12
4
4
  Summary: Web apps in pure Python.
5
5
  Project-URL: homepage, https://reflex.dev
6
6
  Project-URL: repository, https://github.com/reflex-dev/reflex