reflex 0.8.6a0__py3-none-any.whl → 0.8.7__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 (42) hide show
  1. reflex/.templates/jinja/web/vite.config.js.jinja2 +4 -1
  2. reflex/.templates/web/app/routes.js +0 -1
  3. reflex/.templates/web/utils/state.js +1 -11
  4. reflex/app.py +27 -23
  5. reflex/components/base/error_boundary.py +2 -0
  6. reflex/components/component.py +6 -4
  7. reflex/components/lucide/icon.py +4 -1
  8. reflex/components/lucide/icon.pyi +4 -1
  9. reflex/components/plotly/plotly.py +9 -9
  10. reflex/components/recharts/recharts.py +2 -2
  11. reflex/components/sonner/toast.py +7 -7
  12. reflex/components/sonner/toast.pyi +8 -8
  13. reflex/config.py +9 -2
  14. reflex/constants/base.py +2 -0
  15. reflex/constants/installer.py +6 -6
  16. reflex/constants/state.py +1 -0
  17. reflex/custom_components/custom_components.py +3 -3
  18. reflex/istate/manager.py +2 -1
  19. reflex/plugins/__init__.py +2 -0
  20. reflex/plugins/_screenshot.py +144 -0
  21. reflex/plugins/base.py +14 -1
  22. reflex/reflex.py +7 -6
  23. reflex/route.py +4 -0
  24. reflex/state.py +2 -2
  25. reflex/testing.py +3 -5
  26. reflex/utils/build.py +21 -3
  27. reflex/utils/exec.py +11 -11
  28. reflex/utils/frontend_skeleton.py +254 -0
  29. reflex/utils/js_runtimes.py +411 -0
  30. reflex/utils/prerequisites.py +17 -1383
  31. reflex/utils/rename.py +170 -0
  32. reflex/utils/telemetry.py +101 -10
  33. reflex/utils/templates.py +443 -0
  34. reflex/vars/base.py +3 -3
  35. {reflex-0.8.6a0.dist-info → reflex-0.8.7.dist-info}/METADATA +2 -2
  36. {reflex-0.8.6a0.dist-info → reflex-0.8.7.dist-info}/RECORD +39 -37
  37. reflex/.templates/web/utils/client_side_routing.js +0 -45
  38. reflex/components/core/client_side_routing.py +0 -70
  39. reflex/components/core/client_side_routing.pyi +0 -68
  40. {reflex-0.8.6a0.dist-info → reflex-0.8.7.dist-info}/WHEEL +0 -0
  41. {reflex-0.8.6a0.dist-info → reflex-0.8.7.dist-info}/entry_points.txt +0 -0
  42. {reflex-0.8.6a0.dist-info → reflex-0.8.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,443 @@
1
+ """This module provides utilities for managing Reflex app templates."""
2
+
3
+ import dataclasses
4
+ import re
5
+ import shutil
6
+ import tempfile
7
+ import zipfile
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+
11
+ import click
12
+
13
+ from reflex import constants
14
+ from reflex.config import get_config
15
+ from reflex.utils import console, net, path_ops, redir
16
+
17
+
18
+ @dataclasses.dataclass(frozen=True)
19
+ class Template:
20
+ """A template for a Reflex app."""
21
+
22
+ name: str
23
+ description: str
24
+ code_url: str
25
+
26
+
27
+ def create_config(app_name: str):
28
+ """Create a new rxconfig file.
29
+
30
+ Args:
31
+ app_name: The name of the app.
32
+ """
33
+ # Import here to avoid circular imports.
34
+ from reflex.compiler import templates
35
+
36
+ config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config"
37
+
38
+ console.debug(f"Creating {constants.Config.FILE}")
39
+ constants.Config.FILE.write_text(
40
+ templates.RXCONFIG.render(app_name=app_name, config_name=config_name)
41
+ )
42
+
43
+
44
+ def initialize_app_directory(
45
+ app_name: str,
46
+ template_name: str = constants.Templates.DEFAULT,
47
+ template_code_dir_name: str | None = None,
48
+ template_dir: Path | None = None,
49
+ ):
50
+ """Initialize the app directory on reflex init.
51
+
52
+ Args:
53
+ app_name: The name of the app.
54
+ template_name: The name of the template to use.
55
+ template_code_dir_name: The name of the code directory in the template.
56
+ template_dir: The directory of the template source files.
57
+
58
+ Raises:
59
+ Exit: If template_name, template_code_dir_name, template_dir combination is not supported.
60
+ """
61
+ console.log("Initializing the app directory.")
62
+
63
+ # By default, use the blank template from local assets.
64
+ if template_name == constants.Templates.DEFAULT:
65
+ if template_code_dir_name is not None or template_dir is not None:
66
+ console.error(
67
+ f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
68
+ )
69
+ raise click.exceptions.Exit(1)
70
+ template_code_dir_name = constants.Templates.Dirs.CODE
71
+ template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
72
+ else:
73
+ if template_code_dir_name is None or template_dir is None:
74
+ console.error(
75
+ f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
76
+ )
77
+ raise click.exceptions.Exit(1)
78
+
79
+ console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
80
+
81
+ # Remove __pycache__ dirs in template directory and current directory.
82
+ for pycache_dir in [
83
+ *template_dir.glob("**/__pycache__"),
84
+ *Path.cwd().glob("**/__pycache__"),
85
+ ]:
86
+ shutil.rmtree(pycache_dir, ignore_errors=True)
87
+
88
+ for file in template_dir.iterdir():
89
+ # Copy the file to current directory but keep the name the same.
90
+ path_ops.cp(str(file), file.name)
91
+
92
+ # Rename the template app to the app name.
93
+ path_ops.mv(template_code_dir_name, app_name)
94
+ path_ops.mv(
95
+ Path(app_name) / (template_name + constants.Ext.PY),
96
+ Path(app_name) / (app_name + constants.Ext.PY),
97
+ )
98
+
99
+ # Fix up the imports.
100
+ path_ops.find_replace(
101
+ app_name,
102
+ f"from {template_name}",
103
+ f"from {app_name}",
104
+ )
105
+
106
+
107
+ def initialize_default_app(app_name: str):
108
+ """Initialize the default app.
109
+
110
+ Args:
111
+ app_name: The name of the app.
112
+ """
113
+ create_config(app_name)
114
+ initialize_app_directory(app_name)
115
+
116
+
117
+ def create_config_init_app_from_remote_template(app_name: str, template_url: str):
118
+ """Create new rxconfig and initialize app using a remote template.
119
+
120
+ Args:
121
+ app_name: The name of the app.
122
+ template_url: The path to the template source code as a zip file.
123
+
124
+ Raises:
125
+ Exit: If any download, file operations fail or unexpected zip file format.
126
+
127
+ """
128
+ import httpx
129
+
130
+ # Create a temp directory for the zip download.
131
+ try:
132
+ temp_dir = tempfile.mkdtemp()
133
+ except OSError as ose:
134
+ console.error(f"Failed to create temp directory for download: {ose}")
135
+ raise click.exceptions.Exit(1) from ose
136
+
137
+ # Use httpx GET with redirects to download the zip file.
138
+ zip_file_path: Path = Path(temp_dir) / "template.zip"
139
+ try:
140
+ # Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
141
+ response = net.get(template_url, follow_redirects=True)
142
+ console.debug(f"Server responded download request: {response}")
143
+ response.raise_for_status()
144
+ except httpx.HTTPError as he:
145
+ console.error(f"Failed to download the template: {he}")
146
+ raise click.exceptions.Exit(1) from he
147
+ try:
148
+ zip_file_path.write_bytes(response.content)
149
+ console.debug(f"Downloaded the zip to {zip_file_path}")
150
+ except OSError as ose:
151
+ console.error(f"Unable to write the downloaded zip to disk {ose}")
152
+ raise click.exceptions.Exit(1) from ose
153
+
154
+ # Create a temp directory for the zip extraction.
155
+ try:
156
+ unzip_dir = Path(tempfile.mkdtemp())
157
+ except OSError as ose:
158
+ console.error(f"Failed to create temp directory for extracting zip: {ose}")
159
+ raise click.exceptions.Exit(1) from ose
160
+
161
+ try:
162
+ zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
163
+ # The zip file downloaded from github looks like:
164
+ # repo-name-branch/**/*, so we need to remove the top level directory.
165
+ except Exception as uze:
166
+ console.error(f"Failed to unzip the template: {uze}")
167
+ raise click.exceptions.Exit(1) from uze
168
+
169
+ if len(subdirs := list(unzip_dir.iterdir())) != 1:
170
+ console.error(f"Expected one directory in the zip, found {subdirs}")
171
+ raise click.exceptions.Exit(1)
172
+
173
+ template_dir = unzip_dir / subdirs[0]
174
+ console.debug(f"Template folder is located at {template_dir}")
175
+
176
+ # Move the rxconfig file here first.
177
+ path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
178
+ new_config = get_config(reload=True)
179
+
180
+ # Get the template app's name from rxconfig in case it is different than
181
+ # the source code repo name on github.
182
+ template_name = new_config.app_name
183
+
184
+ create_config(app_name)
185
+ initialize_app_directory(
186
+ app_name,
187
+ template_name=template_name,
188
+ template_code_dir_name=template_name,
189
+ template_dir=template_dir,
190
+ )
191
+ req_file = Path("requirements.txt")
192
+ if req_file.exists() and len(req_file.read_text().splitlines()) > 1:
193
+ console.info(
194
+ "Run `pip install -r requirements.txt` to install the required python packages for this template."
195
+ )
196
+ # Clean up the temp directories.
197
+ shutil.rmtree(temp_dir)
198
+ shutil.rmtree(unzip_dir)
199
+
200
+
201
+ def validate_and_create_app_using_remote_template(
202
+ app_name: str, template: str, templates: dict[str, Template]
203
+ ):
204
+ """Validate and create an app using a remote template.
205
+
206
+ Args:
207
+ app_name: The name of the app.
208
+ template: The name of the template.
209
+ templates: The available templates.
210
+
211
+ Raises:
212
+ Exit: If the template is not found.
213
+ """
214
+ # If user selects a template, it needs to exist.
215
+ if template in templates:
216
+ from reflex_cli.v2.utils import hosting
217
+
218
+ authenticated_token = hosting.authenticated_token()
219
+ if not authenticated_token or not authenticated_token[0]:
220
+ console.print(
221
+ f"Please use `reflex login` to access the '{template}' template."
222
+ )
223
+ raise click.exceptions.Exit(3)
224
+
225
+ template_url = templates[template].code_url
226
+ else:
227
+ template_parsed_url = urlparse(template)
228
+ # Check if the template is a github repo.
229
+ if template_parsed_url.hostname == "github.com":
230
+ path = template_parsed_url.path.strip("/").removesuffix(".git")
231
+ template_url = f"https://github.com/{path}/archive/main.zip"
232
+ else:
233
+ console.error(f"Template `{template}` not found or invalid.")
234
+ raise click.exceptions.Exit(1)
235
+
236
+ if template_url is None:
237
+ return
238
+
239
+ create_config_init_app_from_remote_template(
240
+ app_name=app_name, template_url=template_url
241
+ )
242
+
243
+
244
+ def fetch_app_templates(version: str) -> dict[str, Template]:
245
+ """Fetch a dict of templates from the templates repo using github API.
246
+
247
+ Args:
248
+ version: The version of the templates to fetch.
249
+
250
+ Returns:
251
+ The dict of templates.
252
+ """
253
+
254
+ def get_release_by_tag(tag: str) -> dict | None:
255
+ response = net.get(constants.Reflex.RELEASES_URL)
256
+ response.raise_for_status()
257
+ releases = response.json()
258
+ for release in releases:
259
+ if release["tag_name"] == f"v{tag}":
260
+ return release
261
+ return None
262
+
263
+ release = get_release_by_tag(version)
264
+ if release is None:
265
+ console.warn(f"No templates known for version {version}")
266
+ return {}
267
+
268
+ assets = release.get("assets", [])
269
+ asset = next((a for a in assets if a["name"] == "templates.json"), None)
270
+ if asset is None:
271
+ console.warn(f"Templates metadata not found for version {version}")
272
+ return {}
273
+ templates_url = asset["browser_download_url"]
274
+
275
+ templates_data = net.get(templates_url, follow_redirects=True).json()["templates"]
276
+
277
+ for template in templates_data:
278
+ if template["name"] == "blank":
279
+ template["code_url"] = ""
280
+ continue
281
+ template["code_url"] = next(
282
+ (
283
+ a["browser_download_url"]
284
+ for a in assets
285
+ if a["name"] == f"{template['name']}.zip"
286
+ ),
287
+ None,
288
+ )
289
+
290
+ filtered_templates = {}
291
+ for tp in templates_data:
292
+ if tp["hidden"] or tp["code_url"] is None:
293
+ continue
294
+ known_fields = {f.name for f in dataclasses.fields(Template)}
295
+ filtered_templates[tp["name"]] = Template(
296
+ **{k: v for k, v in tp.items() if k in known_fields}
297
+ )
298
+ return filtered_templates
299
+
300
+
301
+ def fetch_remote_templates(
302
+ template: str,
303
+ ) -> tuple[str, dict[str, Template]]:
304
+ """Fetch the available remote templates.
305
+
306
+ Args:
307
+ template: The name of the template.
308
+
309
+ Returns:
310
+ The selected template and the available templates.
311
+ """
312
+ available_templates = {}
313
+
314
+ try:
315
+ # Get the available templates
316
+ available_templates = fetch_app_templates(constants.Reflex.VERSION)
317
+ except Exception as e:
318
+ console.warn("Failed to fetch templates. Falling back to default template.")
319
+ console.debug(f"Error while fetching templates: {e}")
320
+ template = constants.Templates.DEFAULT
321
+
322
+ return template, available_templates
323
+
324
+
325
+ def prompt_for_template_options(templates: list[Template]) -> str:
326
+ """Prompt the user to specify a template.
327
+
328
+ Args:
329
+ templates: The templates to choose from.
330
+
331
+ Returns:
332
+ The template name the user selects.
333
+
334
+ Raises:
335
+ Exit: If the user does not select a template.
336
+ """
337
+ # Show the user the URLs of each template to preview.
338
+ console.print("\nGet started with a template:")
339
+
340
+ # Prompt the user to select a template.
341
+ for index, template in enumerate(templates):
342
+ console.print(f"({index}) {template.description}")
343
+
344
+ template = console.ask(
345
+ "Which template would you like to use?",
346
+ choices=[str(i) for i in range(len(templates))],
347
+ show_choices=False,
348
+ default="0",
349
+ )
350
+
351
+ if not template:
352
+ console.error("No template selected.")
353
+ raise click.exceptions.Exit(1)
354
+
355
+ try:
356
+ template_index = int(template)
357
+ except ValueError:
358
+ console.error("Invalid template selected.")
359
+ raise click.exceptions.Exit(1) from None
360
+
361
+ if template_index < 0 or template_index >= len(templates):
362
+ console.error("Invalid template selected.")
363
+ raise click.exceptions.Exit(1)
364
+
365
+ # Return the template.
366
+ return templates[template_index].name
367
+
368
+
369
+ def initialize_app(app_name: str, template: str | None = None) -> str | None:
370
+ """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
371
+
372
+ Args:
373
+ app_name: The name of the app.
374
+ template: The name of the template to use.
375
+
376
+ Returns:
377
+ The name of the template.
378
+
379
+ Raises:
380
+ Exit: If the template is not valid or unspecified.
381
+ """
382
+ # Local imports to avoid circular imports.
383
+ from reflex.utils import telemetry
384
+
385
+ # Check if the app is already initialized.
386
+ if constants.Config.FILE.exists():
387
+ telemetry.send("reinit")
388
+ return None
389
+
390
+ templates: dict[str, Template] = {}
391
+
392
+ # Don't fetch app templates if the user directly asked for DEFAULT.
393
+ if template is not None and (template not in (constants.Templates.DEFAULT,)):
394
+ template, templates = fetch_remote_templates(template)
395
+
396
+ if template is None:
397
+ template = prompt_for_template_options(get_init_cli_prompt_options())
398
+
399
+ if template == constants.Templates.CHOOSE_TEMPLATES:
400
+ redir.reflex_templates()
401
+ raise click.exceptions.Exit(0)
402
+
403
+ if template == constants.Templates.AI:
404
+ redir.reflex_build_redirect()
405
+ raise click.exceptions.Exit(0)
406
+
407
+ # If the blank template is selected, create a blank app.
408
+ if template == constants.Templates.DEFAULT:
409
+ # Default app creation behavior: a blank app.
410
+ initialize_default_app(app_name)
411
+ else:
412
+ validate_and_create_app_using_remote_template(
413
+ app_name=app_name, template=template, templates=templates
414
+ )
415
+
416
+ telemetry.send("init", template=template)
417
+
418
+ return template
419
+
420
+
421
+ def get_init_cli_prompt_options() -> list[Template]:
422
+ """Get the CLI options for initializing a Reflex app.
423
+
424
+ Returns:
425
+ The CLI options.
426
+ """
427
+ return [
428
+ Template(
429
+ name=constants.Templates.DEFAULT,
430
+ description="A blank Reflex app.",
431
+ code_url="",
432
+ ),
433
+ Template(
434
+ name=constants.Templates.AI,
435
+ description="[bold]Try our free AI builder.",
436
+ code_url="",
437
+ ),
438
+ Template(
439
+ name=constants.Templates.CHOOSE_TEMPLATES,
440
+ description="Premade templates built by the Reflex team.",
441
+ code_url="",
442
+ ),
443
+ ]
reflex/vars/base.py CHANGED
@@ -717,7 +717,7 @@ class Var(Generic[VAR_TYPE], metaclass=MetaclassVar):
717
717
  return f"{constants.REFLEX_VAR_OPENING_TAG}{hashed_var}{constants.REFLEX_VAR_CLOSING_TAG}{self._js_expr}"
718
718
 
719
719
  @overload
720
- def to(self, output: type[str]) -> StringVar: ...
720
+ def to(self, output: type[str]) -> StringVar: ... # pyright: ignore[reportOverlappingOverload]
721
721
 
722
722
  @overload
723
723
  def to(self, output: type[bool]) -> BooleanVar: ...
@@ -734,8 +734,8 @@ class Var(Generic[VAR_TYPE], metaclass=MetaclassVar):
734
734
  @overload
735
735
  def to(
736
736
  self,
737
- output: type[list] | type[tuple] | type[set],
738
- ) -> ArrayVar: ...
737
+ output: type[SEQUENCE_TYPE],
738
+ ) -> ArrayVar[SEQUENCE_TYPE]: ...
739
739
 
740
740
  @overload
741
741
  def to(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reflex
3
- Version: 0.8.6a0
3
+ Version: 0.8.7
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
@@ -30,7 +30,7 @@ Requires-Dist: pydantic<3.0,>=1.10.21
30
30
  Requires-Dist: python-multipart<1.0,>=0.0.20
31
31
  Requires-Dist: python-socketio<6.0,>=5.12.0
32
32
  Requires-Dist: redis<7.0,>=5.2.1
33
- Requires-Dist: reflex-hosting-cli>=0.1.53
33
+ Requires-Dist: reflex-hosting-cli>=0.1.54
34
34
  Requires-Dist: rich<15,>=13
35
35
  Requires-Dist: sqlmodel<0.1,>=0.0.24
36
36
  Requires-Dist: starlette>=0.47.0