reflex 0.4.5a2__py3-none-any.whl → 0.4.6__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 (196) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +5 -15
  2. reflex/.templates/jinja/web/pages/index.js.jinja2 +4 -0
  3. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
  4. reflex/.templates/web/utils/state.js +3 -0
  5. reflex/__init__.py +12 -2
  6. reflex/__init__.pyi +4 -0
  7. reflex/app.py +21 -1
  8. reflex/base.py +16 -4
  9. reflex/compiler/compiler.py +1 -0
  10. reflex/compiler/utils.py +11 -2
  11. reflex/components/base/app_wrap.pyi +1 -1
  12. reflex/components/base/bare.py +3 -4
  13. reflex/components/base/body.pyi +1 -1
  14. reflex/components/base/document.pyi +1 -1
  15. reflex/components/base/fragment.pyi +1 -1
  16. reflex/components/base/head.pyi +1 -1
  17. reflex/components/base/link.pyi +1 -1
  18. reflex/components/base/meta.pyi +1 -1
  19. reflex/components/base/script.pyi +1 -1
  20. reflex/components/chakra/base.pyi +1 -1
  21. reflex/components/chakra/datadisplay/badge.pyi +1 -1
  22. reflex/components/chakra/datadisplay/code.pyi +1 -1
  23. reflex/components/chakra/datadisplay/divider.pyi +1 -1
  24. reflex/components/chakra/datadisplay/keyboard_key.pyi +1 -1
  25. reflex/components/chakra/datadisplay/list.pyi +1 -1
  26. reflex/components/chakra/datadisplay/stat.pyi +1 -1
  27. reflex/components/chakra/datadisplay/table.pyi +1 -1
  28. reflex/components/chakra/datadisplay/tag.pyi +1 -1
  29. reflex/components/chakra/disclosure/accordion.pyi +1 -1
  30. reflex/components/chakra/disclosure/tabs.pyi +1 -1
  31. reflex/components/chakra/disclosure/transition.pyi +1 -1
  32. reflex/components/chakra/disclosure/visuallyhidden.pyi +1 -1
  33. reflex/components/chakra/feedback/alert.pyi +1 -1
  34. reflex/components/chakra/feedback/circularprogress.pyi +1 -1
  35. reflex/components/chakra/feedback/progress.pyi +1 -1
  36. reflex/components/chakra/feedback/skeleton.pyi +1 -1
  37. reflex/components/chakra/feedback/spinner.pyi +1 -1
  38. reflex/components/chakra/forms/button.pyi +1 -1
  39. reflex/components/chakra/forms/checkbox.pyi +1 -1
  40. reflex/components/chakra/forms/colormodeswitch.pyi +1 -1
  41. reflex/components/chakra/forms/date_picker.pyi +1 -1
  42. reflex/components/chakra/forms/date_time_picker.pyi +1 -1
  43. reflex/components/chakra/forms/editable.pyi +1 -1
  44. reflex/components/chakra/forms/email.pyi +1 -1
  45. reflex/components/chakra/forms/form.pyi +1 -1
  46. reflex/components/chakra/forms/iconbutton.pyi +1 -1
  47. reflex/components/chakra/forms/input.pyi +1 -1
  48. reflex/components/chakra/forms/numberinput.pyi +1 -1
  49. reflex/components/chakra/forms/password.pyi +1 -1
  50. reflex/components/chakra/forms/pininput.pyi +1 -1
  51. reflex/components/chakra/forms/radio.pyi +1 -1
  52. reflex/components/chakra/forms/rangeslider.pyi +1 -1
  53. reflex/components/chakra/forms/select.pyi +1 -1
  54. reflex/components/chakra/forms/slider.pyi +1 -1
  55. reflex/components/chakra/forms/switch.pyi +1 -1
  56. reflex/components/chakra/forms/textarea.pyi +1 -1
  57. reflex/components/chakra/forms/time_picker.pyi +1 -1
  58. reflex/components/chakra/layout/aspect_ratio.pyi +1 -1
  59. reflex/components/chakra/layout/box.pyi +1 -1
  60. reflex/components/chakra/layout/card.pyi +1 -1
  61. reflex/components/chakra/layout/center.pyi +1 -1
  62. reflex/components/chakra/layout/container.pyi +1 -1
  63. reflex/components/chakra/layout/flex.pyi +1 -1
  64. reflex/components/chakra/layout/grid.pyi +1 -1
  65. reflex/components/chakra/layout/spacer.pyi +1 -1
  66. reflex/components/chakra/layout/stack.pyi +1 -1
  67. reflex/components/chakra/layout/wrap.pyi +1 -1
  68. reflex/components/chakra/media/avatar.pyi +1 -1
  69. reflex/components/chakra/media/icon.pyi +1 -1
  70. reflex/components/chakra/media/image.pyi +1 -1
  71. reflex/components/chakra/navigation/breadcrumb.pyi +1 -1
  72. reflex/components/chakra/navigation/link.pyi +1 -1
  73. reflex/components/chakra/navigation/linkoverlay.pyi +1 -1
  74. reflex/components/chakra/navigation/stepper.pyi +1 -1
  75. reflex/components/chakra/overlay/alertdialog.pyi +1 -1
  76. reflex/components/chakra/overlay/drawer.pyi +1 -1
  77. reflex/components/chakra/overlay/menu.pyi +1 -1
  78. reflex/components/chakra/overlay/modal.pyi +1 -1
  79. reflex/components/chakra/overlay/popover.pyi +1 -1
  80. reflex/components/chakra/overlay/tooltip.pyi +1 -1
  81. reflex/components/chakra/typography/heading.pyi +1 -1
  82. reflex/components/chakra/typography/highlight.pyi +1 -1
  83. reflex/components/chakra/typography/span.pyi +1 -1
  84. reflex/components/chakra/typography/text.pyi +1 -1
  85. reflex/components/component.py +82 -30
  86. reflex/components/core/banner.py +1 -2
  87. reflex/components/core/banner.pyi +1 -2
  88. reflex/components/core/client_side_routing.pyi +1 -1
  89. reflex/components/core/cond.py +1 -1
  90. reflex/components/core/debounce.pyi +1 -1
  91. reflex/components/core/html.pyi +1 -1
  92. reflex/components/core/responsive.py +1 -1
  93. reflex/components/core/upload.pyi +1 -1
  94. reflex/components/datadisplay/code.py +17 -9
  95. reflex/components/datadisplay/code.pyi +3 -1
  96. reflex/components/datadisplay/dataeditor.pyi +1 -1
  97. reflex/components/el/element.pyi +1 -1
  98. reflex/components/el/elements/base.pyi +1 -1
  99. reflex/components/el/elements/forms.py +78 -1
  100. reflex/components/el/elements/forms.pyi +10 -2
  101. reflex/components/el/elements/inline.pyi +1 -1
  102. reflex/components/el/elements/media.pyi +1 -1
  103. reflex/components/el/elements/metadata.pyi +1 -1
  104. reflex/components/el/elements/other.pyi +1 -1
  105. reflex/components/el/elements/scripts.pyi +1 -1
  106. reflex/components/el/elements/sectioning.pyi +1 -1
  107. reflex/components/el/elements/tables.pyi +1 -1
  108. reflex/components/el/elements/typography.pyi +1 -1
  109. reflex/components/gridjs/datatable.pyi +1 -1
  110. reflex/components/lucide/icon.py +275 -115
  111. reflex/components/lucide/icon.pyi +259 -114
  112. reflex/components/markdown/markdown.pyi +1 -1
  113. reflex/components/moment/moment.pyi +1 -1
  114. reflex/components/next/base.pyi +1 -1
  115. reflex/components/next/image.pyi +1 -1
  116. reflex/components/next/link.pyi +1 -1
  117. reflex/components/next/video.pyi +1 -1
  118. reflex/components/plotly/plotly.pyi +1 -1
  119. reflex/components/radix/primitives/accordion.pyi +1 -1
  120. reflex/components/radix/primitives/base.pyi +1 -1
  121. reflex/components/radix/primitives/drawer.pyi +1 -1
  122. reflex/components/radix/primitives/form.pyi +1 -1
  123. reflex/components/radix/primitives/progress.pyi +1 -1
  124. reflex/components/radix/primitives/slider.pyi +1 -1
  125. reflex/components/radix/themes/base.pyi +1 -1
  126. reflex/components/radix/themes/color_mode.pyi +1 -1
  127. reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
  128. reflex/components/radix/themes/components/aspect_ratio.pyi +1 -1
  129. reflex/components/radix/themes/components/avatar.pyi +1 -1
  130. reflex/components/radix/themes/components/badge.pyi +1 -1
  131. reflex/components/radix/themes/components/button.pyi +1 -1
  132. reflex/components/radix/themes/components/callout.pyi +1 -1
  133. reflex/components/radix/themes/components/card.pyi +1 -1
  134. reflex/components/radix/themes/components/checkbox.pyi +1 -1
  135. reflex/components/radix/themes/components/context_menu.pyi +1 -1
  136. reflex/components/radix/themes/components/dialog.pyi +1 -1
  137. reflex/components/radix/themes/components/dropdown_menu.pyi +1 -1
  138. reflex/components/radix/themes/components/hover_card.pyi +1 -1
  139. reflex/components/radix/themes/components/icon_button.pyi +1 -1
  140. reflex/components/radix/themes/components/inset.pyi +1 -1
  141. reflex/components/radix/themes/components/popover.pyi +1 -1
  142. reflex/components/radix/themes/components/radio_group.pyi +1 -1
  143. reflex/components/radix/themes/components/scroll_area.pyi +1 -1
  144. reflex/components/radix/themes/components/select.pyi +1 -1
  145. reflex/components/radix/themes/components/separator.pyi +1 -1
  146. reflex/components/radix/themes/components/slider.pyi +1 -1
  147. reflex/components/radix/themes/components/switch.pyi +1 -1
  148. reflex/components/radix/themes/components/table.pyi +1 -1
  149. reflex/components/radix/themes/components/tabs.pyi +1 -1
  150. reflex/components/radix/themes/components/text_area.pyi +5 -1
  151. reflex/components/radix/themes/components/text_field.pyi +1 -1
  152. reflex/components/radix/themes/components/tooltip.pyi +1 -1
  153. reflex/components/radix/themes/layout/__init__.py +5 -4
  154. reflex/components/radix/themes/layout/base.pyi +1 -1
  155. reflex/components/radix/themes/layout/box.pyi +1 -1
  156. reflex/components/radix/themes/layout/center.pyi +1 -1
  157. reflex/components/radix/themes/layout/container.pyi +1 -1
  158. reflex/components/radix/themes/layout/flex.pyi +1 -1
  159. reflex/components/radix/themes/layout/grid.pyi +1 -1
  160. reflex/components/radix/themes/layout/list.py +21 -13
  161. reflex/components/radix/themes/layout/list.pyi +139 -481
  162. reflex/components/radix/themes/layout/section.pyi +1 -1
  163. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  164. reflex/components/radix/themes/layout/stack.pyi +1 -1
  165. reflex/components/radix/themes/typography/blockquote.pyi +1 -1
  166. reflex/components/radix/themes/typography/code.pyi +1 -1
  167. reflex/components/radix/themes/typography/heading.pyi +1 -1
  168. reflex/components/radix/themes/typography/link.pyi +1 -1
  169. reflex/components/radix/themes/typography/text.pyi +1 -1
  170. reflex/components/react_player/audio.pyi +1 -1
  171. reflex/components/react_player/react_player.pyi +1 -1
  172. reflex/components/react_player/video.pyi +1 -1
  173. reflex/components/recharts/cartesian.pyi +1 -1
  174. reflex/components/recharts/charts.pyi +1 -1
  175. reflex/components/recharts/general.pyi +1 -1
  176. reflex/components/recharts/polar.pyi +1 -1
  177. reflex/components/recharts/recharts.pyi +1 -1
  178. reflex/components/suneditor/editor.pyi +1 -1
  179. reflex/config.py +12 -2
  180. reflex/custom_components/custom_components.py +305 -21
  181. reflex/event.py +36 -46
  182. reflex/reflex.py +19 -13
  183. reflex/state.py +184 -39
  184. reflex/testing.py +15 -11
  185. reflex/utils/console.py +15 -7
  186. reflex/utils/prerequisites.py +11 -0
  187. reflex/utils/processes.py +8 -25
  188. reflex/utils/pyi_generator.py +842 -0
  189. reflex/utils/types.py +14 -2
  190. reflex/vars.py +33 -3
  191. {reflex-0.4.5a2.dist-info → reflex-0.4.6.dist-info}/METADATA +31 -29
  192. {reflex-0.4.5a2.dist-info → reflex-0.4.6.dist-info}/RECORD +195 -195
  193. reflex/page.pyi +0 -17
  194. {reflex-0.4.5a2.dist-info → reflex-0.4.6.dist-info}/LICENSE +0 -0
  195. {reflex-0.4.5a2.dist-info → reflex-0.4.6.dist-info}/WHEEL +0 -0
  196. {reflex-0.4.5a2.dist-info → reflex-0.4.6.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/layout/section.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/layout/spacer.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/layout/stack.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/typography/blockquote.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/typography/code.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/typography/heading.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/typography/link.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/radix/themes/typography/text.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/react_player/audio.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/react_player/react_player.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/react_player/video.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/recharts/cartesian.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/recharts/charts.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/recharts/general.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/recharts/polar.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/recharts/recharts.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
@@ -1,6 +1,6 @@
1
1
  """Stub file for reflex/components/suneditor/editor.py"""
2
2
  # ------------------- DO NOT EDIT ----------------------
3
- # This file was generated by `scripts/pyi_generator.py`!
3
+ # This file was generated by `reflex/utils/pyi_generator.py`!
4
4
  # ------------------------------------------------------
5
5
 
6
6
  from typing import Any, Dict, Literal, Optional, Union, overload
reflex/config.py CHANGED
@@ -6,9 +6,19 @@ import importlib
6
6
  import os
7
7
  import sys
8
8
  import urllib.parse
9
- from typing import Any, Dict, List, Optional, Set
9
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
10
+
11
+ try:
12
+ # TODO The type checking guard can be removed once
13
+ # reflex-hosting-cli tools are compatible with pydantic v2
14
+
15
+ if not TYPE_CHECKING:
16
+ import pydantic.v1 as pydantic
17
+ else:
18
+ raise ModuleNotFoundError
19
+ except ModuleNotFoundError:
20
+ import pydantic
10
21
 
11
- import pydantic
12
22
  from reflex_cli.constants.hosting import Hosting
13
23
 
14
24
  from reflex import constants
@@ -9,18 +9,32 @@ import sys
9
9
  from collections import namedtuple
10
10
  from contextlib import contextmanager
11
11
  from pathlib import Path
12
- from typing import Optional
12
+ from typing import Optional, Tuple
13
13
 
14
+ import httpx
15
+ import tomlkit
14
16
  import typer
17
+ from tomlkit.exceptions import TOMLKitError
15
18
 
16
19
  from reflex import constants
17
20
  from reflex.config import get_config
18
21
  from reflex.constants import CustomComponents
19
22
  from reflex.utils import console
23
+ from reflex.utils.pyi_generator import PyiGenerator
20
24
 
21
25
  config = get_config()
22
26
  custom_components_cli = typer.Typer()
23
27
 
28
+ POST_CUSTOM_COMPONENTS_GALLERY_ENDPOINT = (
29
+ f"{config.cp_backend_url}/custom-components/gallery"
30
+ )
31
+
32
+ GET_CUSTOM_COMPONENTS_GALLERY_BY_NAME_ENDPOINT = (
33
+ f"{config.cp_backend_url}/custom-components/gallery"
34
+ )
35
+
36
+ POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
37
+
24
38
 
25
39
  @contextmanager
26
40
  def set_directory(working_directory: str):
@@ -400,6 +414,18 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool:
400
414
  return False
401
415
 
402
416
 
417
+ def _make_pyi_files():
418
+ """Create pyi files for the custom component."""
419
+ from glob import glob
420
+
421
+ package_name = glob("custom_components/*.egg-info")[0].replace(".egg-info", "")
422
+
423
+ for dir, _, _ in os.walk(f"./{package_name}"):
424
+ if "__pycache__" in dir:
425
+ continue
426
+ PyiGenerator().scan_all([dir])
427
+
428
+
403
429
  def _run_build():
404
430
  """Run the build command.
405
431
 
@@ -408,6 +434,8 @@ def _run_build():
408
434
  """
409
435
  console.print("Building custom component...")
410
436
 
437
+ _make_pyi_files()
438
+
411
439
  cmds = [sys.executable, "-m", "build", "."]
412
440
  if _run_commands_in_subprocess(cmds):
413
441
  console.info("Custom component built successfully!")
@@ -493,21 +521,15 @@ def _get_version_to_publish() -> str:
493
521
  The version to publish.
494
522
  """
495
523
  # Get the version from the pyproject.toml.
496
- version_to_publish = None
497
- with open(CustomComponents.PYPROJECT_TOML, "r") as f:
498
- pyproject_toml = f.read()
499
- # Note below does not capture non-matching quotes. Not performing full syntax check here.
500
- match = re.search(r'version\s*=\s*["\'](.*?)["\']', pyproject_toml)
501
- if match:
502
- version_to_publish = match.group(1)
503
- console.debug(f"Version to be published: {version_to_publish}")
504
- if not version_to_publish:
524
+ try:
525
+ with open(CustomComponents.PYPROJECT_TOML, "rb") as f:
526
+ project_toml = tomlkit.parse(f.read())
527
+ return project_toml.get("project", {})["version"]
528
+ except (OSError, KeyError, TOMLKitError) as ex:
505
529
  console.error(
506
- f"Could not find the version to be published in {CustomComponents.PYPROJECT_TOML}"
530
+ f"Cannot find the version in {CustomComponents.PYPROJECT_TOML} due to {ex}"
507
531
  )
508
- raise typer.Exit(code=1)
509
-
510
- return version_to_publish
532
+ raise typer.Exit(code=1) from ex
511
533
 
512
534
 
513
535
  def _ensure_dist_dir(version_to_publish: str, build: bool):
@@ -542,7 +564,7 @@ def _ensure_dist_dir(version_to_publish: str, build: bool):
542
564
  needs_rebuild = (
543
565
  console.ask(
544
566
  "Distribution files for the version to be published already exist. Do you want to rebuild?",
545
- choices=["n", "y"],
567
+ choices=["y", "n"],
546
568
  default="n",
547
569
  )
548
570
  == "y"
@@ -601,6 +623,14 @@ def publish(
601
623
  True,
602
624
  help="Whether to build the package before publishing. If the package is already built, set this to False.",
603
625
  ),
626
+ share: bool = typer.Option(
627
+ True,
628
+ help="Whether to prompt to share more details on the published package. Only applicable when published to PyPI. Defaults to True.",
629
+ ),
630
+ validate_project_info: bool = typer.Option(
631
+ True,
632
+ help="Whether to interactively validate the project information in the pyproject.toml file.",
633
+ ),
604
634
  loglevel: constants.LogLevel = typer.Option(
605
635
  config.loglevel, help="The log level to use."
606
636
  ),
@@ -613,6 +643,8 @@ def publish(
613
643
  username: The username to use for authentication on python package repository.
614
644
  password: The password to use for authentication on python package repository.
615
645
  build: Whether to build the distribution files. Defaults to True.
646
+ share: Whether to prompt to share more details on the published package. Defaults to True.
647
+ validate_project_info: whether to interactively validate the project information in the pyproject.toml file. Defaults to True.
616
648
  loglevel: The log level to use.
617
649
 
618
650
  Raises:
@@ -633,12 +665,16 @@ def publish(
633
665
  # Validate the distribution directory.
634
666
  _ensure_dist_dir(version_to_publish=version_to_publish, build=build)
635
667
 
636
- # We install twine on the fly if required so it is not a stable dependency of reflex.
637
- try:
638
- import twine # noqa: F401 # type: ignore
639
- except (ImportError, ModuleNotFoundError) as ex:
640
- if not _pip_install_on_demand("twine"):
641
- raise typer.Exit(code=1) from ex
668
+ if validate_project_info and (
669
+ console.ask(
670
+ "Would you like to interactively review the package information?",
671
+ choices=["y", "n"],
672
+ default="y",
673
+ )
674
+ == "y"
675
+ ):
676
+ _validate_project_info()
677
+
642
678
  publish_cmds = [
643
679
  sys.executable,
644
680
  "-m",
@@ -657,3 +693,251 @@ def publish(
657
693
  console.info("Custom component published successfully!")
658
694
  else:
659
695
  raise typer.Exit(1)
696
+
697
+ # Only prompt to share more details on the published package if it is published to PyPI.
698
+ if repository != "pypi" or not share:
699
+ return
700
+
701
+ # Ask user to share more details on the published package.
702
+ if (
703
+ console.ask(
704
+ "Would you like to include your published component on our gallery?",
705
+ choices=["y", "n"],
706
+ default="y",
707
+ )
708
+ == "n"
709
+ ):
710
+ console.print(
711
+ "If you decide to do this later, you can run `reflex component share` command. Thank you!"
712
+ )
713
+ return
714
+
715
+ _collect_details_for_gallery()
716
+
717
+
718
+ def _process_entered_list(input: str) -> list | None:
719
+ """Process the user entered comma separated list into a list if applicable.
720
+
721
+ Args:
722
+ input: the user entered comma separated list
723
+
724
+ Returns:
725
+ The list of items or None.
726
+ """
727
+ return [t.strip() for t in input.split(",") if t if input] or None
728
+
729
+
730
+ def _validate_project_info():
731
+ """Validate the project information in the pyproject.toml file.
732
+
733
+ Raises:
734
+ Exit: If the pyproject.toml file is ill-formed.
735
+ """
736
+ try:
737
+ with open(CustomComponents.PYPROJECT_TOML, "rb") as f:
738
+ pyproject_toml = tomlkit.parse(f.read())
739
+ except TOMLKitError as ex:
740
+ console.error(f"Unable to read from pyproject.toml due to {ex}")
741
+ raise typer.Exit(code=1) from ex
742
+
743
+ try:
744
+ project = pyproject_toml.get("project", {})
745
+ if not project:
746
+ console.error("The project section not found in pyproject.toml")
747
+ raise typer.Exit(code=1)
748
+ console.print(
749
+ f'Double check the information before publishing: {project["name"]} version {project["version"]}'
750
+ )
751
+ except KeyError as ke:
752
+ console.error(f"The pyproject.toml is possibly ill-formed due to {ke}")
753
+ raise typer.Exit(code=1) from ke
754
+
755
+ console.print("Update or enter to keep the current information.")
756
+ project["description"] = console.ask(
757
+ "short description", default=project.get("description", "")
758
+ )
759
+ # PyPI only shows the first author.
760
+ author = project.get("authors", [{}])[0]
761
+ author["name"] = console.ask(f"Author Name", default=author.get("name", ""))
762
+ author["email"] = console.ask(f"Author Email", default=author.get("email", ""))
763
+
764
+ console.print(f'Current keywords are: {project.get("keywords") or []}')
765
+ keyword_action = console.ask(
766
+ "Keep, replace or append?", choices=["k", "r", "a"], default="k"
767
+ )
768
+ new_keywords = []
769
+ if keyword_action == "r":
770
+ new_keywords = (
771
+ _process_entered_list(
772
+ console.ask("Enter new set of keywords separated by commas")
773
+ )
774
+ or []
775
+ )
776
+ elif keyword_action == "a":
777
+ new_keywords = (
778
+ _process_entered_list(
779
+ console.ask("Enter new set of keywords separated by commas")
780
+ )
781
+ or []
782
+ )
783
+ project["keywords"] = project.get("keywords", []) + new_keywords
784
+
785
+ if not project.get("urls"):
786
+ project["urls"] = {}
787
+ project["urls"]["homepage"] = console.ask(
788
+ "homepage URL", default=project["urls"].get("homepage", "")
789
+ )
790
+ project["urls"]["source"] = console.ask(
791
+ "source code URL", default=project["urls"].get("source", "")
792
+ )
793
+ pyproject_toml["project"] = project
794
+ try:
795
+ with open(CustomComponents.PYPROJECT_TOML, "w") as f:
796
+ tomlkit.dump(pyproject_toml, f)
797
+ except (OSError, TOMLKitError) as ex:
798
+ console.error(f"Unable to read from pyproject.toml due to {ex}")
799
+ raise typer.Exit(code=1) from ex
800
+
801
+
802
+ def _collect_details_for_gallery():
803
+ """Helper to collect details on the custom component to be included in the gallery.
804
+
805
+ Raises:
806
+ Exit: If pyproject.toml file is ill-formed or the request to the backend services fails.
807
+ """
808
+ from reflex.reflex import _login
809
+
810
+ console.rule("[bold]Authentication with Reflex Services")
811
+ console.print("First let's log in to Reflex backend services.")
812
+ access_token = _login()
813
+
814
+ console.rule("[bold]Custom Component Information")
815
+ params = {}
816
+ package_name = None
817
+ try:
818
+ with open(CustomComponents.PYPROJECT_TOML, "rb") as f:
819
+ project_toml = tomlkit.parse(f.read())
820
+ package_name = project_toml.get("project", {})["name"]
821
+ except (OSError, TOMLKitError, KeyError) as ex:
822
+ console.debug(
823
+ f"Unable to read from pyproject.toml in current directory due to {ex}"
824
+ )
825
+ package_name = console.ask("[ Published python package name ]")
826
+ console.print(f"[ Custom component package name ] : {package_name}")
827
+ params["package_name"] = package_name
828
+
829
+ # Check the backend services if the user is allowed to update information of this package is already shared.
830
+ try:
831
+ console.debug(
832
+ f"Checking if user has permission to upsert information for {package_name} by POST."
833
+ )
834
+ # Send a POST request to achieve two things at once:
835
+ # 1. Check if the package is already shared by the user. If not, the backend will return 403.
836
+ # 2. If this package is not shared before, this request records the package name in the backend.
837
+ response = httpx.post(
838
+ POST_CUSTOM_COMPONENTS_GALLERY_ENDPOINT,
839
+ headers={"Authorization": f"Bearer {access_token}"},
840
+ data=params,
841
+ )
842
+ if response.status_code == httpx.codes.FORBIDDEN:
843
+ console.error(
844
+ f"{package_name} is owned by another user. Unable to update the information for it."
845
+ )
846
+ raise typer.Exit(code=1)
847
+ response.raise_for_status()
848
+ except httpx.HTTPError as he:
849
+ console.error(f"Unable to complete request due to {he}.")
850
+ raise typer.Exit(code=1) from he
851
+
852
+ display_name = (
853
+ console.ask("[ Friendly display name for your component ] (enter to skip)")
854
+ or None
855
+ )
856
+ if display_name:
857
+ params["display_name"] = display_name
858
+
859
+ files = []
860
+ if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
861
+ files.append(
862
+ ("files", (image_file_and_extension[1], image_file_and_extension[0]))
863
+ )
864
+
865
+ demo_url = None
866
+ while True:
867
+ demo_url = (
868
+ console.ask(
869
+ "[ Full URL of deployed demo app, e.g. `https://my-app.reflex.run` ] (enter to skip)"
870
+ )
871
+ or None
872
+ )
873
+ if _validate_url_with_protocol_prefix(demo_url):
874
+ break
875
+ if demo_url:
876
+ params["demo_url"] = demo_url
877
+
878
+ # Now send the post request to Reflex backend services.
879
+ try:
880
+ console.debug(f"Sending custom component data: {params}")
881
+ response = httpx.post(
882
+ POST_CUSTOM_COMPONENTS_GALLERY_ENDPOINT,
883
+ headers={"Authorization": f"Bearer {access_token}"},
884
+ data=params,
885
+ files=files,
886
+ timeout=POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT,
887
+ )
888
+ response.raise_for_status()
889
+
890
+ except httpx.HTTPError as he:
891
+ console.error(f"Unable to complete request due to {he}.")
892
+ raise typer.Exit(code=1) from he
893
+
894
+ console.info("Custom component information successfully shared!")
895
+
896
+
897
+ def _validate_url_with_protocol_prefix(url: str | None) -> bool:
898
+ """Validate the URL with protocol prefix. Empty string is acceptable.
899
+
900
+ Args:
901
+ url: the URL string to check.
902
+
903
+ Returns:
904
+ Whether the entered URL is acceptable.
905
+ """
906
+ return not url or (url.startswith("http://") or url.startswith("https://"))
907
+
908
+
909
+ def _get_file_from_prompt_in_loop() -> Tuple[bytes, str] | None:
910
+ image_file = file_extension = None
911
+ while image_file is None:
912
+ image_filepath = console.ask(
913
+ f"Upload a preview image of your demo app (enter to skip)"
914
+ )
915
+ if not image_filepath:
916
+ break
917
+ file_extension = image_filepath.split(".")[-1]
918
+ try:
919
+ with open(image_filepath, "rb") as f:
920
+ image_file = f.read()
921
+ return image_file, file_extension
922
+ except OSError as ose:
923
+ console.error(f"Unable to read the {file_extension} file due to {ose}")
924
+ raise typer.Exit(code=1) from ose
925
+
926
+ console.debug(f"File extension detected: {file_extension}")
927
+ return None
928
+
929
+
930
+ @custom_components_cli.command(name="share")
931
+ def share_more_detail(
932
+ loglevel: constants.LogLevel = typer.Option(
933
+ config.loglevel, help="The log level to use."
934
+ ),
935
+ ):
936
+ """Collect more details on the published package for gallery.
937
+
938
+ Args:
939
+ loglevel: The log level to use.
940
+ """
941
+ console.set_log_level(loglevel)
942
+
943
+ _collect_details_for_gallery()