reflex 0.5.7a1__py3-none-any.whl → 0.5.8a1__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.

Files changed (44) hide show
  1. reflex/.templates/web/utils/state.js +1 -1
  2. reflex/app.py +20 -2
  3. reflex/components/core/banner.py +14 -0
  4. reflex/components/core/banner.pyi +3 -3
  5. reflex/components/core/debounce.py +3 -0
  6. reflex/components/el/__init__.py +1 -0
  7. reflex/components/el/__init__.pyi +8 -5
  8. reflex/components/el/elements/__init__.py +3 -1
  9. reflex/components/el/elements/__init__.pyi +8 -6
  10. reflex/components/el/elements/media.py +98 -18
  11. reflex/components/el/elements/media.pyi +523 -18
  12. reflex/components/el/elements/metadata.py +5 -1
  13. reflex/components/radix/primitives/base.py +1 -1
  14. reflex/components/radix/themes/layout/list.py +0 -2
  15. reflex/components/recharts/cartesian.py +46 -20
  16. reflex/components/recharts/cartesian.pyi +26 -14
  17. reflex/components/recharts/charts.py +4 -0
  18. reflex/components/recharts/charts.pyi +3 -0
  19. reflex/components/recharts/general.py +23 -9
  20. reflex/components/recharts/general.pyi +6 -4
  21. reflex/components/recharts/polar.py +35 -11
  22. reflex/components/recharts/polar.pyi +35 -7
  23. reflex/components/sonner/toast.py +28 -2
  24. reflex/components/sonner/toast.pyi +14 -7
  25. reflex/constants/base.py +21 -0
  26. reflex/constants/event.py +2 -0
  27. reflex/experimental/vars/__init__.py +17 -0
  28. reflex/experimental/vars/base.py +282 -15
  29. reflex/experimental/vars/function.py +214 -0
  30. reflex/experimental/vars/number.py +1295 -0
  31. reflex/experimental/vars/sequence.py +1039 -0
  32. reflex/reflex.py +29 -2
  33. reflex/state.py +59 -10
  34. reflex/utils/imports.py +71 -8
  35. reflex/utils/prerequisites.py +115 -35
  36. reflex/utils/pyi_generator.py +2 -0
  37. reflex/utils/redir.py +52 -0
  38. reflex/vars.py +220 -11
  39. reflex/vars.pyi +20 -2
  40. {reflex-0.5.7a1.dist-info → reflex-0.5.8a1.dist-info}/METADATA +2 -2
  41. {reflex-0.5.7a1.dist-info → reflex-0.5.8a1.dist-info}/RECORD +44 -40
  42. {reflex-0.5.7a1.dist-info → reflex-0.5.8a1.dist-info}/LICENSE +0 -0
  43. {reflex-0.5.7a1.dist-info → reflex-0.5.8a1.dist-info}/WHEEL +0 -0
  44. {reflex-0.5.7a1.dist-info → reflex-0.5.8a1.dist-info}/entry_points.txt +0 -0
reflex/reflex.py CHANGED
@@ -16,7 +16,7 @@ from reflex_cli.utils import dependency
16
16
  from reflex import constants
17
17
  from reflex.config import get_config
18
18
  from reflex.custom_components.custom_components import custom_components_cli
19
- from reflex.utils import console, telemetry
19
+ from reflex.utils import console, redir, telemetry
20
20
 
21
21
  # Disable typer+rich integration for help panels
22
22
  typer.core.rich = False # type: ignore
@@ -65,6 +65,7 @@ def _init(
65
65
  name: str,
66
66
  template: str | None = None,
67
67
  loglevel: constants.LogLevel = config.loglevel,
68
+ ai: bool = False,
68
69
  ):
69
70
  """Initialize a new Reflex app in the given directory."""
70
71
  from reflex.utils import exec, prerequisites
@@ -91,9 +92,31 @@ def _init(
91
92
  # Set up the web project.
92
93
  prerequisites.initialize_frontend_dependencies()
93
94
 
95
+ # Integrate with reflex.build.
96
+ generation_hash = None
97
+ if ai:
98
+ if template is None:
99
+ # If AI is requested and no template specified, redirect the user to reflex.build.
100
+ generation_hash = redir.reflex_build_redirect()
101
+ elif prerequisites.is_generation_hash(template):
102
+ # Otherwise treat the template as a generation hash.
103
+ generation_hash = template
104
+ else:
105
+ console.error(
106
+ "Cannot use `--template` option with `--ai` option. Please remove `--template` option."
107
+ )
108
+ raise typer.Exit(2)
109
+ template = constants.Templates.DEFAULT
110
+
94
111
  # Initialize the app.
95
112
  prerequisites.initialize_app(app_name, template)
96
113
 
114
+ # If a reflex.build generation hash is available, download the code and apply it to the main module.
115
+ if generation_hash:
116
+ prerequisites.initialize_main_module_index_from_generation(
117
+ app_name, generation_hash=generation_hash
118
+ )
119
+
97
120
  # Migrate Pynecone projects to Reflex.
98
121
  prerequisites.migrate_to_reflex()
99
122
 
@@ -119,9 +142,13 @@ def init(
119
142
  loglevel: constants.LogLevel = typer.Option(
120
143
  config.loglevel, help="The log level to use."
121
144
  ),
145
+ ai: bool = typer.Option(
146
+ False,
147
+ help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
148
+ ),
122
149
  ):
123
150
  """Initialize a new Reflex app in the current directory."""
124
- _init(name, template, loglevel)
151
+ _init(name, template, loglevel, ai)
125
152
 
126
153
 
127
154
  def _run(
reflex/state.py CHANGED
@@ -31,6 +31,8 @@ from typing import (
31
31
  import dill
32
32
  from sqlalchemy.orm import DeclarativeBase
33
33
 
34
+ from reflex.config import get_config
35
+
34
36
  try:
35
37
  import pydantic.v1 as pydantic
36
38
  except ModuleNotFoundError:
@@ -42,7 +44,6 @@ from redis.exceptions import ResponseError
42
44
 
43
45
  from reflex import constants
44
46
  from reflex.base import Base
45
- from reflex.config import get_config
46
47
  from reflex.event import (
47
48
  BACKGROUND_TASK_MARKER,
48
49
  Event,
@@ -201,7 +202,7 @@ def _no_chain_background_task(
201
202
 
202
203
  def _substate_key(
203
204
  token: str,
204
- state_cls_or_name: BaseState | Type[BaseState] | str | list[str],
205
+ state_cls_or_name: BaseState | Type[BaseState] | str | Sequence[str],
205
206
  ) -> str:
206
207
  """Get the substate key.
207
208
 
@@ -2028,19 +2029,38 @@ class StateProxy(wrapt.ObjectProxy):
2028
2029
  self.counter += 1
2029
2030
  """
2030
2031
 
2031
- def __init__(self, state_instance):
2032
+ def __init__(
2033
+ self, state_instance, parent_state_proxy: Optional["StateProxy"] = None
2034
+ ):
2032
2035
  """Create a proxy for a state instance.
2033
2036
 
2037
+ If `get_state` is used on a StateProxy, the resulting state will be
2038
+ linked to the given state via parent_state_proxy. The first state in the
2039
+ chain is the state that initiated the background task.
2040
+
2034
2041
  Args:
2035
2042
  state_instance: The state instance to proxy.
2043
+ parent_state_proxy: The parent state proxy, for linked mutability and context tracking.
2036
2044
  """
2037
2045
  super().__init__(state_instance)
2038
2046
  # compile is not relevant to backend logic
2039
2047
  self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
2040
- self._self_substate_path = state_instance.get_full_name().split(".")
2048
+ self._self_substate_path = tuple(state_instance.get_full_name().split("."))
2041
2049
  self._self_actx = None
2042
2050
  self._self_mutable = False
2043
2051
  self._self_actx_lock = asyncio.Lock()
2052
+ self._self_actx_lock_holder = None
2053
+ self._self_parent_state_proxy = parent_state_proxy
2054
+
2055
+ def _is_mutable(self) -> bool:
2056
+ """Check if the state is mutable.
2057
+
2058
+ Returns:
2059
+ Whether the state is mutable.
2060
+ """
2061
+ if self._self_parent_state_proxy is not None:
2062
+ return self._self_parent_state_proxy._is_mutable()
2063
+ return self._self_mutable
2044
2064
 
2045
2065
  async def __aenter__(self) -> StateProxy:
2046
2066
  """Enter the async context manager protocol.
@@ -2053,8 +2073,31 @@ class StateProxy(wrapt.ObjectProxy):
2053
2073
 
2054
2074
  Returns:
2055
2075
  This StateProxy instance in mutable mode.
2056
- """
2076
+
2077
+ Raises:
2078
+ ImmutableStateError: If the state is already mutable.
2079
+ """
2080
+ if self._self_parent_state_proxy is not None:
2081
+ parent_state = (
2082
+ await self._self_parent_state_proxy.__aenter__()
2083
+ ).__wrapped__
2084
+ super().__setattr__(
2085
+ "__wrapped__",
2086
+ await parent_state.get_state(
2087
+ State.get_class_substate(self._self_substate_path)
2088
+ ),
2089
+ )
2090
+ return self
2091
+ current_task = asyncio.current_task()
2092
+ if (
2093
+ self._self_actx_lock.locked()
2094
+ and current_task == self._self_actx_lock_holder
2095
+ ):
2096
+ raise ImmutableStateError(
2097
+ "The state is already mutable. Do not nest `async with self` blocks."
2098
+ )
2057
2099
  await self._self_actx_lock.acquire()
2100
+ self._self_actx_lock_holder = current_task
2058
2101
  self._self_actx = self._self_app.modify_state(
2059
2102
  token=_substate_key(
2060
2103
  self.__wrapped__.router.session.client_token,
@@ -2076,12 +2119,16 @@ class StateProxy(wrapt.ObjectProxy):
2076
2119
  Args:
2077
2120
  exc_info: The exception info tuple.
2078
2121
  """
2122
+ if self._self_parent_state_proxy is not None:
2123
+ await self._self_parent_state_proxy.__aexit__(*exc_info)
2124
+ return
2079
2125
  if self._self_actx is None:
2080
2126
  return
2081
2127
  self._self_mutable = False
2082
2128
  try:
2083
2129
  await self._self_actx.__aexit__(*exc_info)
2084
2130
  finally:
2131
+ self._self_actx_lock_holder = None
2085
2132
  self._self_actx_lock.release()
2086
2133
  self._self_actx = None
2087
2134
 
@@ -2116,7 +2163,7 @@ class StateProxy(wrapt.ObjectProxy):
2116
2163
  Raises:
2117
2164
  ImmutableStateError: If the state is not in mutable mode.
2118
2165
  """
2119
- if name in ["substates", "parent_state"] and not self._self_mutable:
2166
+ if name in ["substates", "parent_state"] and not self._is_mutable():
2120
2167
  raise ImmutableStateError(
2121
2168
  "Background task StateProxy is immutable outside of a context "
2122
2169
  "manager. Use `async with self` to modify state."
@@ -2156,7 +2203,7 @@ class StateProxy(wrapt.ObjectProxy):
2156
2203
  """
2157
2204
  if (
2158
2205
  name.startswith("_self_") # wrapper attribute
2159
- or self._self_mutable # lock held
2206
+ or self._is_mutable() # lock held
2160
2207
  # non-persisted state attribute
2161
2208
  or name in self.__wrapped__.get_skip_vars()
2162
2209
  ):
@@ -2180,7 +2227,7 @@ class StateProxy(wrapt.ObjectProxy):
2180
2227
  Raises:
2181
2228
  ImmutableStateError: If the state is not in mutable mode.
2182
2229
  """
2183
- if not self._self_mutable:
2230
+ if not self._is_mutable():
2184
2231
  raise ImmutableStateError(
2185
2232
  "Background task StateProxy is immutable outside of a context "
2186
2233
  "manager. Use `async with self` to modify state."
@@ -2199,12 +2246,14 @@ class StateProxy(wrapt.ObjectProxy):
2199
2246
  Raises:
2200
2247
  ImmutableStateError: If the state is not in mutable mode.
2201
2248
  """
2202
- if not self._self_mutable:
2249
+ if not self._is_mutable():
2203
2250
  raise ImmutableStateError(
2204
2251
  "Background task StateProxy is immutable outside of a context "
2205
2252
  "manager. Use `async with self` to modify state."
2206
2253
  )
2207
- return await self.__wrapped__.get_state(state_cls)
2254
+ return type(self)(
2255
+ await self.__wrapped__.get_state(state_cls), parent_state_proxy=self
2256
+ )
2208
2257
 
2209
2258
  def _as_state_update(self, *args, **kwargs) -> StateUpdate:
2210
2259
  """Temporarily allow mutability to access parent_state.
reflex/utils/imports.py CHANGED
@@ -3,12 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import defaultdict
6
- from typing import Dict, List, Optional, Union
6
+ from typing import Dict, List, Optional, Tuple, Union
7
7
 
8
8
  from reflex.base import Base
9
9
 
10
10
 
11
- def merge_imports(*imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
11
+ def merge_imports(
12
+ *imports: ImportDict | ParsedImportDict | ImmutableParsedImportDict,
13
+ ) -> ParsedImportDict:
12
14
  """Merge multiple import dicts together.
13
15
 
14
16
  Args:
@@ -19,7 +21,9 @@ def merge_imports(*imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
19
21
  """
20
22
  all_imports = defaultdict(list)
21
23
  for import_dict in imports:
22
- for lib, fields in import_dict.items():
24
+ for lib, fields in (
25
+ import_dict if isinstance(import_dict, tuple) else import_dict.items()
26
+ ):
23
27
  all_imports[lib].extend(fields)
24
28
  return all_imports
25
29
 
@@ -48,7 +52,9 @@ def parse_imports(imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
48
52
  }
49
53
 
50
54
 
51
- def collapse_imports(imports: ParsedImportDict) -> ParsedImportDict:
55
+ def collapse_imports(
56
+ imports: ParsedImportDict | ImmutableParsedImportDict,
57
+ ) -> ParsedImportDict:
52
58
  """Remove all duplicate ImportVar within an ImportDict.
53
59
 
54
60
  Args:
@@ -58,8 +64,14 @@ def collapse_imports(imports: ParsedImportDict) -> ParsedImportDict:
58
64
  The collapsed import dict.
59
65
  """
60
66
  return {
61
- lib: list(set(import_vars)) if isinstance(import_vars, list) else import_vars
62
- for lib, import_vars in imports.items()
67
+ lib: (
68
+ list(set(import_vars))
69
+ if isinstance(import_vars, list)
70
+ else list(import_vars)
71
+ )
72
+ for lib, import_vars in (
73
+ imports if isinstance(imports, tuple) else imports.items()
74
+ )
63
75
  }
64
76
 
65
77
 
@@ -99,11 +111,61 @@ class ImportVar(Base):
99
111
  else:
100
112
  return self.tag or ""
101
113
 
114
+ def __lt__(self, other: ImportVar) -> bool:
115
+ """Compare two ImportVar objects.
116
+
117
+ Args:
118
+ other: The other ImportVar object to compare.
119
+
120
+ Returns:
121
+ Whether this ImportVar object is less than the other.
122
+ """
123
+ return (
124
+ self.tag,
125
+ self.is_default,
126
+ self.alias,
127
+ self.install,
128
+ self.render,
129
+ self.transpile,
130
+ ) < (
131
+ other.tag,
132
+ other.is_default,
133
+ other.alias,
134
+ other.install,
135
+ other.render,
136
+ other.transpile,
137
+ )
138
+
139
+ def __eq__(self, other: ImportVar) -> bool:
140
+ """Check if two ImportVar objects are equal.
141
+
142
+ Args:
143
+ other: The other ImportVar object to compare.
144
+
145
+ Returns:
146
+ Whether the two ImportVar objects are equal.
147
+ """
148
+ return (
149
+ self.tag,
150
+ self.is_default,
151
+ self.alias,
152
+ self.install,
153
+ self.render,
154
+ self.transpile,
155
+ ) == (
156
+ other.tag,
157
+ other.is_default,
158
+ other.alias,
159
+ other.install,
160
+ other.render,
161
+ other.transpile,
162
+ )
163
+
102
164
  def __hash__(self) -> int:
103
- """Define a hash function for the import var.
165
+ """Hash the ImportVar object.
104
166
 
105
167
  Returns:
106
- The hash of the var.
168
+ The hash of the ImportVar object.
107
169
  """
108
170
  return hash(
109
171
  (
@@ -120,3 +182,4 @@ class ImportVar(Base):
120
182
  ImportTypes = Union[str, ImportVar, List[Union[str, ImportVar]], List[ImportVar]]
121
183
  ImportDict = Dict[str, ImportTypes]
122
184
  ParsedImportDict = Dict[str, List[ImportVar]]
185
+ ImmutableParsedImportDict = Tuple[Tuple[str, Tuple[ImportVar, ...]], ...]
@@ -16,6 +16,7 @@ import shutil
16
16
  import stat
17
17
  import sys
18
18
  import tempfile
19
+ import textwrap
19
20
  import zipfile
20
21
  from datetime import datetime
21
22
  from fileinput import FileInput
@@ -1310,39 +1311,63 @@ def migrate_to_reflex():
1310
1311
  print(line, end="")
1311
1312
 
1312
1313
 
1313
- def fetch_app_templates() -> dict[str, Template]:
1314
- """Fetch the list of app templates from the Reflex backend server.
1314
+ RELEASES_URL = f"https://api.github.com/repos/reflex-dev/templates/releases"
1315
+
1316
+
1317
+ def fetch_app_templates(version: str) -> dict[str, Template]:
1318
+ """Fetch a dict of templates from the templates repo using github API.
1319
+
1320
+ Args:
1321
+ version: The version of the templates to fetch.
1315
1322
 
1316
1323
  Returns:
1317
- The name and download URL as a dictionary.
1324
+ The dict of templates.
1318
1325
  """
1319
- config = get_config()
1320
- if not config.cp_backend_url:
1321
- console.info(
1322
- "Skip fetching App templates. No backend URL is specified in the config."
1323
- )
1324
- return {}
1325
- try:
1326
- response = httpx.get(
1327
- f"{config.cp_backend_url}{constants.Templates.APP_TEMPLATES_ROUTE}"
1328
- )
1326
+
1327
+ def get_release_by_tag(tag: str) -> dict | None:
1328
+ response = httpx.get(RELEASES_URL)
1329
1329
  response.raise_for_status()
1330
- return {
1331
- template["name"]: Template.parse_obj(template)
1332
- for template in response.json()
1333
- }
1334
- except httpx.HTTPError as ex:
1335
- console.info(f"Failed to fetch app templates: {ex}")
1330
+ releases = response.json()
1331
+ for release in releases:
1332
+ if release["tag_name"] == f"v{tag}":
1333
+ return release
1334
+ return None
1335
+
1336
+ release = get_release_by_tag(version)
1337
+ if release is None:
1338
+ console.warn(f"No templates known for version {version}")
1336
1339
  return {}
1337
- except (TypeError, KeyError, json.JSONDecodeError) as tkje:
1338
- console.info(f"Unable to process server response for app templates: {tkje}")
1340
+
1341
+ assets = release.get("assets", [])
1342
+ asset = next((a for a in assets if a["name"] == "templates.json"), None)
1343
+ if asset is None:
1344
+ console.warn(f"Templates metadata not found for version {version}")
1339
1345
  return {}
1346
+ else:
1347
+ templates_url = asset["browser_download_url"]
1340
1348
 
1349
+ templates_data = httpx.get(templates_url, follow_redirects=True).json()["templates"]
1341
1350
 
1342
- def create_config_init_app_from_remote_template(
1343
- app_name: str,
1344
- template_url: str,
1345
- ):
1351
+ for template in templates_data:
1352
+ if template["name"] == "blank":
1353
+ template["code_url"] = ""
1354
+ continue
1355
+ template["code_url"] = next(
1356
+ (
1357
+ a["browser_download_url"]
1358
+ for a in assets
1359
+ if a["name"] == f"{template['name']}.zip"
1360
+ ),
1361
+ None,
1362
+ )
1363
+ return {
1364
+ tp["name"]: Template.parse_obj(tp)
1365
+ for tp in templates_data
1366
+ if not tp["hidden"] and tp["code_url"] is not None
1367
+ }
1368
+
1369
+
1370
+ def create_config_init_app_from_remote_template(app_name: str, template_url: str):
1346
1371
  """Create new rxconfig and initialize app using a remote template.
1347
1372
 
1348
1373
  Args:
@@ -1436,15 +1461,20 @@ def initialize_app(app_name: str, template: str | None = None):
1436
1461
  telemetry.send("reinit")
1437
1462
  return
1438
1463
 
1439
- # Get the available templates
1440
- templates: dict[str, Template] = fetch_app_templates()
1464
+ templates: dict[str, Template] = {}
1441
1465
 
1442
- # Prompt for a template if not provided.
1443
- if template is None and len(templates) > 0:
1444
- template = prompt_for_template(list(templates.values()))
1445
- elif template is None:
1446
- template = constants.Templates.DEFAULT
1447
- assert template is not None
1466
+ # Don't fetch app templates if the user directly asked for DEFAULT.
1467
+ if template is None or (template != constants.Templates.DEFAULT):
1468
+ try:
1469
+ # Get the available templates
1470
+ templates = fetch_app_templates(constants.Reflex.VERSION)
1471
+ if template is None and len(templates) > 0:
1472
+ template = prompt_for_template(list(templates.values()))
1473
+ except Exception as e:
1474
+ console.warn("Failed to fetch templates. Falling back to default template.")
1475
+ console.debug(f"Error while fetching templates: {e}")
1476
+ finally:
1477
+ template = template or constants.Templates.DEFAULT
1448
1478
 
1449
1479
  # If the blank template is selected, create a blank app.
1450
1480
  if template == constants.Templates.DEFAULT:
@@ -1467,14 +1497,52 @@ def initialize_app(app_name: str, template: str | None = None):
1467
1497
  else:
1468
1498
  console.error(f"Template `{template}` not found.")
1469
1499
  raise typer.Exit(1)
1500
+
1501
+ if template_url is None:
1502
+ return
1503
+
1470
1504
  create_config_init_app_from_remote_template(
1471
- app_name=app_name,
1472
- template_url=template_url,
1505
+ app_name=app_name, template_url=template_url
1473
1506
  )
1474
1507
 
1475
1508
  telemetry.send("init", template=template)
1476
1509
 
1477
1510
 
1511
+ def initialize_main_module_index_from_generation(app_name: str, generation_hash: str):
1512
+ """Overwrite the `index` function in the main module with reflex.build generated code.
1513
+
1514
+ Args:
1515
+ app_name: The name of the app.
1516
+ generation_hash: The generation hash from reflex.build.
1517
+ """
1518
+ # Download the reflex code for the generation.
1519
+ resp = httpx.get(
1520
+ constants.Templates.REFLEX_BUILD_CODE_URL.format(
1521
+ generation_hash=generation_hash
1522
+ )
1523
+ ).raise_for_status()
1524
+
1525
+ def replace_content(_match):
1526
+ return "\n".join(
1527
+ [
1528
+ "def index() -> rx.Component:",
1529
+ textwrap.indent("return " + resp.text, " "),
1530
+ "",
1531
+ "",
1532
+ ],
1533
+ )
1534
+
1535
+ main_module_path = Path(app_name, app_name + constants.Ext.PY)
1536
+ main_module_code = main_module_path.read_text()
1537
+ main_module_path.write_text(
1538
+ re.sub(
1539
+ r"def index\(\).*:\n([^\n]\s+.*\n+)+",
1540
+ replace_content,
1541
+ main_module_code,
1542
+ )
1543
+ )
1544
+
1545
+
1478
1546
  def format_address_width(address_width) -> int | None:
1479
1547
  """Cast address width to an int.
1480
1548
 
@@ -1562,3 +1630,15 @@ def is_windows_bun_supported() -> bool:
1562
1630
  and cpu_info.model_name is not None
1563
1631
  and "ARM" not in cpu_info.model_name
1564
1632
  )
1633
+
1634
+
1635
+ def is_generation_hash(template: str) -> bool:
1636
+ """Check if the template looks like a generation hash.
1637
+
1638
+ Args:
1639
+ template: The template name.
1640
+
1641
+ Returns:
1642
+ True if the template is composed of 32 or more hex characters.
1643
+ """
1644
+ return re.match(r"^[0-9a-f]{32,}$", template) is not None
@@ -882,6 +882,7 @@ class PyiGenerator:
882
882
  # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present.
883
883
  sub_mods = getattr(mod, "_SUBMODULES", None)
884
884
  sub_mod_attrs = getattr(mod, "_SUBMOD_ATTRS", None)
885
+ pyright_ignore_imports = getattr(mod, "_PYRIGHT_IGNORE_IMPORTS", [])
885
886
 
886
887
  if not sub_mods and not sub_mod_attrs:
887
888
  return
@@ -901,6 +902,7 @@ class PyiGenerator:
901
902
  # construct the import statement and handle special cases for aliases
902
903
  sub_mod_attrs_imports = [
903
904
  f"from .{path} import {mod if not isinstance(mod, tuple) else mod[0]} as {mod if not isinstance(mod, tuple) else mod[1]}"
905
+ + (" # type: ignore" if mod in pyright_ignore_imports else "")
904
906
  for mod, path in sub_mod_attrs.items()
905
907
  ]
906
908
  sub_mod_attrs_imports.append("")
reflex/utils/redir.py ADDED
@@ -0,0 +1,52 @@
1
+ """Utilities to handle redirection to browser UI."""
2
+
3
+ import time
4
+ import uuid
5
+ import webbrowser
6
+
7
+ import httpx
8
+
9
+ from .. import constants
10
+ from . import console
11
+
12
+
13
+ def open_browser_and_wait(
14
+ target_url: str, poll_url: str, interval: int = 2
15
+ ) -> httpx.Response:
16
+ """Open a browser window to target_url and request poll_url until it returns successfully.
17
+
18
+ Args:
19
+ target_url: The URL to open in the browser.
20
+ poll_url: The URL to poll for success.
21
+ interval: The interval in seconds to wait between polling.
22
+
23
+ Returns:
24
+ The response from the poll_url.
25
+ """
26
+ if not webbrowser.open(target_url):
27
+ console.warn(
28
+ f"Unable to automatically open the browser. Please navigate to {target_url} in your browser."
29
+ )
30
+ console.info("[b]Complete the workflow in the browser to continue.[/b]")
31
+ while True:
32
+ try:
33
+ response = httpx.get(poll_url, follow_redirects=True)
34
+ if response.is_success:
35
+ break
36
+ except httpx.RequestError as err:
37
+ console.info(f"Will retry after error occurred while polling: {err}.")
38
+ time.sleep(interval)
39
+ return response
40
+
41
+
42
+ def reflex_build_redirect() -> str:
43
+ """Open the browser window to reflex.build and wait for the user to select a generation.
44
+
45
+ Returns:
46
+ The selected generation hash.
47
+ """
48
+ token = str(uuid.uuid4())
49
+ target_url = constants.Templates.REFLEX_BUILD_URL.format(reflex_init_token=token)
50
+ poll_url = constants.Templates.REFLEX_BUILD_POLL_URL.format(reflex_init_token=token)
51
+ response = open_browser_and_wait(target_url, poll_url)
52
+ return response.json()["generation_hash"]