reflex 0.4.5a2__py3-none-any.whl → 0.4.6a1__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 +109 -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 +313 -20
  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.6a1.dist-info}/METADATA +33 -31
  192. {reflex-0.4.5a2.dist-info → reflex-0.4.6a1.dist-info}/RECORD +195 -195
  193. reflex/page.pyi +0 -17
  194. {reflex-0.4.5a2.dist-info → reflex-0.4.6a1.dist-info}/LICENSE +0 -0
  195. {reflex-0.4.5a2.dist-info → reflex-0.4.6a1.dist-info}/WHEEL +0 -0
  196. {reflex-0.4.5a2.dist-info → reflex-0.4.6a1.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):
@@ -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,260 @@ 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=["n", "y"],
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.print("We recommend that you deploy a demo app showcasing the component.")
811
+ console.print("If not already, please deploy it first.")
812
+ if console.ask("Continue?", choices=["y", "n"], default="y") != "y":
813
+ return
814
+
815
+ console.rule("[bold]Authentication with Reflex Services")
816
+ console.print("First let's log in to Reflex backend services.")
817
+ access_token = _login()
818
+
819
+ console.rule("[bold]Custom Component Information")
820
+ params = {}
821
+ package_name = None
822
+ try:
823
+ with open(CustomComponents.PYPROJECT_TOML, "rb") as f:
824
+ project_toml = tomlkit.parse(f.read())
825
+ package_name = project_toml.get("project", {})["name"]
826
+ except (OSError, TOMLKitError, KeyError) as ex:
827
+ console.debug(
828
+ f"Unable to read from pyproject.toml in current directory due to {ex}"
829
+ )
830
+ package_name = console.ask("[ Published python package name ]")
831
+ console.print(f"[ Custom component package name ] : {package_name}")
832
+
833
+ # Check the backend services if the user is allowed to update information of this package is already shared.
834
+ expected_status_code = False
835
+ try:
836
+ console.debug(
837
+ f"Checking if user has permission to modify information for {package_name} if already exists."
838
+ )
839
+ response = httpx.get(
840
+ f"{GET_CUSTOM_COMPONENTS_GALLERY_BY_NAME_ENDPOINT}/{package_name}",
841
+ headers={"Authorization": f"Bearer {access_token}"},
842
+ )
843
+ if response.status_code == httpx.codes.FORBIDDEN:
844
+ console.error(
845
+ f"{package_name} is owned by another user. Unable to update the information for it."
846
+ )
847
+ raise typer.Exit(code=1)
848
+ elif response.status_code == httpx.codes.NOT_FOUND:
849
+ console.debug(f"{package_name} is not found. This is a new share.")
850
+ expected_status_code = True
851
+
852
+ response.raise_for_status()
853
+ console.debug(f"{package_name} is found. This is an update.")
854
+ except httpx.HTTPError as he:
855
+ if not expected_status_code:
856
+ console.error(f"Unable to complete request due to {he}.")
857
+ raise typer.Exit(code=1) from he
858
+
859
+ params["package_name"] = package_name
860
+
861
+ demo_url = None
862
+ while True:
863
+ demo_url = (
864
+ console.ask(
865
+ "[ Full URL of deployed demo app, e.g. `https://my-app.reflex.run` ] (enter to skip)"
866
+ )
867
+ or None
868
+ )
869
+ if _validate_url_with_protocol_prefix(demo_url):
870
+ break
871
+ if demo_url:
872
+ params["demo_url"] = demo_url
873
+
874
+ display_name = (
875
+ console.ask("[ Friendly display name for your component ] (enter to skip)")
876
+ or None
877
+ )
878
+ if display_name:
879
+ params["display_name"] = display_name
880
+
881
+ files = []
882
+ if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
883
+ files.append(
884
+ ("files", (image_file_and_extension[1], image_file_and_extension[0]))
885
+ )
886
+
887
+ # Now send the post request to Reflex backend services.
888
+ try:
889
+ console.debug(f"Sending custom component data: {params}")
890
+ response = httpx.post(
891
+ POST_CUSTOM_COMPONENTS_GALLERY_ENDPOINT,
892
+ headers={"Authorization": f"Bearer {access_token}"},
893
+ data=params,
894
+ files=files,
895
+ timeout=POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT,
896
+ )
897
+ response.raise_for_status()
898
+
899
+ except httpx.HTTPError as he:
900
+ console.error(f"Unable to complete request due to {he}.")
901
+ raise typer.Exit(code=1) from he
902
+
903
+ console.info("Custom component information successfully shared!")
904
+
905
+
906
+ def _validate_url_with_protocol_prefix(url: str | None) -> bool:
907
+ """Validate the URL with protocol prefix. Empty string is acceptable.
908
+
909
+ Args:
910
+ url: the URL string to check.
911
+
912
+ Returns:
913
+ Whether the entered URL is acceptable.
914
+ """
915
+ return not url or (url.startswith("http://") or url.startswith("https://"))
916
+
917
+
918
+ def _get_file_from_prompt_in_loop() -> Tuple[bytes, str] | None:
919
+ image_file = file_extension = None
920
+ while image_file is None:
921
+ image_filepath = console.ask(
922
+ f"Local path to a preview gif or image (enter to skip)"
923
+ )
924
+ if not image_filepath:
925
+ break
926
+ file_extension = image_filepath.split(".")[-1]
927
+ try:
928
+ with open(image_filepath, "rb") as f:
929
+ image_file = f.read()
930
+ return image_file, file_extension
931
+ except OSError as ose:
932
+ console.error(f"Unable to read the {file_extension} file due to {ose}")
933
+ raise typer.Exit(code=1) from ose
934
+
935
+ console.debug(f"File extension detected: {file_extension}")
936
+ return None
937
+
938
+
939
+ @custom_components_cli.command(name="share")
940
+ def share_more_detail(
941
+ loglevel: constants.LogLevel = typer.Option(
942
+ config.loglevel, help="The log level to use."
943
+ ),
944
+ ):
945
+ """Collect more details on the published package for gallery.
946
+
947
+ Args:
948
+ loglevel: The log level to use.
949
+ """
950
+ console.set_log_level(loglevel)
951
+
952
+ _collect_details_for_gallery()