reflex 0.7.4a2__py3-none-any.whl → 0.7.5__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 (162) hide show
  1. reflex/__init__.py +1 -0
  2. reflex/__init__.pyi +1 -0
  3. reflex/app.py +10 -8
  4. reflex/app_mixins/middleware.py +13 -20
  5. reflex/compiler/compiler.py +10 -3
  6. reflex/compiler/utils.py +4 -4
  7. reflex/components/base/app_wrap.pyi +7 -3
  8. reflex/components/base/body.pyi +7 -3
  9. reflex/components/base/document.pyi +27 -7
  10. reflex/components/base/error_boundary.pyi +7 -3
  11. reflex/components/base/fragment.pyi +7 -3
  12. reflex/components/base/head.pyi +12 -4
  13. reflex/components/base/link.pyi +12 -4
  14. reflex/components/base/meta.pyi +22 -6
  15. reflex/components/base/script.pyi +7 -3
  16. reflex/components/base/strict_mode.pyi +7 -3
  17. reflex/components/component.py +64 -23
  18. reflex/components/core/auto_scroll.pyi +7 -3
  19. reflex/components/core/banner.py +6 -2
  20. reflex/components/core/banner.pyi +32 -8
  21. reflex/components/core/client_side_routing.pyi +12 -4
  22. reflex/components/core/clipboard.pyi +7 -3
  23. reflex/components/core/debounce.pyi +7 -3
  24. reflex/components/core/foreach.py +5 -1
  25. reflex/components/core/html.pyi +7 -3
  26. reflex/components/core/match.py +5 -5
  27. reflex/components/core/sticky.pyi +21 -6
  28. reflex/components/core/upload.pyi +27 -7
  29. reflex/components/datadisplay/code.pyi +12 -4
  30. reflex/components/datadisplay/dataeditor.py +2 -2
  31. reflex/components/datadisplay/dataeditor.pyi +17 -3
  32. reflex/components/datadisplay/shiki_code_block.pyi +17 -4
  33. reflex/components/el/__init__.pyi +1 -1
  34. reflex/components/el/element.pyi +7 -3
  35. reflex/components/el/elements/__init__.py +3 -1
  36. reflex/components/el/elements/__init__.pyi +3 -2
  37. reflex/components/el/elements/base.pyi +7 -3
  38. reflex/components/el/elements/forms.py +1 -1
  39. reflex/components/el/elements/forms.pyi +72 -16
  40. reflex/components/el/elements/inline.pyi +142 -30
  41. reflex/components/el/elements/media.pyi +127 -27
  42. reflex/components/el/elements/metadata.pyi +32 -8
  43. reflex/components/el/elements/other.pyi +37 -9
  44. reflex/components/el/elements/scripts.pyi +17 -5
  45. reflex/components/el/elements/sectioning.pyi +77 -17
  46. reflex/components/el/elements/tables.pyi +52 -12
  47. reflex/components/el/elements/typography.pyi +77 -17
  48. reflex/components/gridjs/datatable.py +2 -2
  49. reflex/components/gridjs/datatable.pyi +12 -4
  50. reflex/components/lucide/icon.py +7 -6
  51. reflex/components/lucide/icon.pyi +22 -7
  52. reflex/components/markdown/markdown.py +1 -1
  53. reflex/components/markdown/markdown.pyi +7 -3
  54. reflex/components/moment/moment.pyi +7 -3
  55. reflex/components/next/base.pyi +7 -3
  56. reflex/components/next/image.pyi +7 -3
  57. reflex/components/next/link.pyi +7 -3
  58. reflex/components/next/video.pyi +7 -3
  59. reflex/components/plotly/plotly.pyi +47 -11
  60. reflex/components/radix/primitives/accordion.pyi +37 -9
  61. reflex/components/radix/primitives/base.pyi +12 -4
  62. reflex/components/radix/primitives/drawer.pyi +57 -13
  63. reflex/components/radix/primitives/form.pyi +52 -12
  64. reflex/components/radix/primitives/progress.pyi +27 -7
  65. reflex/components/radix/primitives/slider.pyi +27 -7
  66. reflex/components/radix/themes/base.pyi +41 -10
  67. reflex/components/radix/themes/color_mode.py +2 -2
  68. reflex/components/radix/themes/color_mode.pyi +17 -5
  69. reflex/components/radix/themes/components/alert_dialog.pyi +36 -9
  70. reflex/components/radix/themes/components/aspect_ratio.pyi +7 -3
  71. reflex/components/radix/themes/components/avatar.pyi +6 -3
  72. reflex/components/radix/themes/components/badge.pyi +6 -3
  73. reflex/components/radix/themes/components/button.pyi +6 -3
  74. reflex/components/radix/themes/components/callout.pyi +26 -7
  75. reflex/components/radix/themes/components/card.pyi +6 -3
  76. reflex/components/radix/themes/components/checkbox.pyi +16 -5
  77. reflex/components/radix/themes/components/checkbox_cards.pyi +11 -4
  78. reflex/components/radix/themes/components/checkbox_group.pyi +11 -4
  79. reflex/components/radix/themes/components/context_menu.pyi +66 -15
  80. reflex/components/radix/themes/components/data_list.pyi +21 -6
  81. reflex/components/radix/themes/components/dialog.pyi +36 -9
  82. reflex/components/radix/themes/components/dropdown_menu.pyi +41 -10
  83. reflex/components/radix/themes/components/hover_card.pyi +21 -6
  84. reflex/components/radix/themes/components/icon_button.pyi +6 -3
  85. reflex/components/radix/themes/components/inset.pyi +6 -3
  86. reflex/components/radix/themes/components/popover.pyi +21 -6
  87. reflex/components/radix/themes/components/progress.pyi +6 -3
  88. reflex/components/radix/themes/components/radio.pyi +6 -3
  89. reflex/components/radix/themes/components/radio_cards.pyi +11 -4
  90. reflex/components/radix/themes/components/radio_group.py +6 -1
  91. reflex/components/radix/themes/components/radio_group.pyi +21 -6
  92. reflex/components/radix/themes/components/scroll_area.pyi +7 -3
  93. reflex/components/radix/themes/components/segmented_control.pyi +11 -4
  94. reflex/components/radix/themes/components/select.pyi +46 -11
  95. reflex/components/radix/themes/components/separator.pyi +6 -3
  96. reflex/components/radix/themes/components/skeleton.pyi +6 -3
  97. reflex/components/radix/themes/components/slider.pyi +6 -3
  98. reflex/components/radix/themes/components/spinner.pyi +6 -3
  99. reflex/components/radix/themes/components/switch.pyi +6 -3
  100. reflex/components/radix/themes/components/table.pyi +36 -9
  101. reflex/components/radix/themes/components/tabs.pyi +26 -7
  102. reflex/components/radix/themes/components/text_area.pyi +6 -3
  103. reflex/components/radix/themes/components/text_field.py +3 -2
  104. reflex/components/radix/themes/components/text_field.pyi +16 -5
  105. reflex/components/radix/themes/components/tooltip.pyi +7 -3
  106. reflex/components/radix/themes/layout/base.pyi +6 -3
  107. reflex/components/radix/themes/layout/box.pyi +7 -3
  108. reflex/components/radix/themes/layout/center.pyi +6 -3
  109. reflex/components/radix/themes/layout/container.pyi +6 -3
  110. reflex/components/radix/themes/layout/flex.pyi +6 -3
  111. reflex/components/radix/themes/layout/grid.pyi +6 -3
  112. reflex/components/radix/themes/layout/list.pyi +27 -7
  113. reflex/components/radix/themes/layout/section.pyi +6 -3
  114. reflex/components/radix/themes/layout/spacer.pyi +6 -3
  115. reflex/components/radix/themes/layout/stack.pyi +16 -5
  116. reflex/components/radix/themes/typography/blockquote.pyi +6 -3
  117. reflex/components/radix/themes/typography/code.pyi +6 -3
  118. reflex/components/radix/themes/typography/heading.pyi +6 -3
  119. reflex/components/radix/themes/typography/link.pyi +6 -3
  120. reflex/components/radix/themes/typography/text.pyi +36 -9
  121. reflex/components/react_player/audio.pyi +7 -3
  122. reflex/components/react_player/react_player.pyi +7 -3
  123. reflex/components/react_player/video.pyi +7 -3
  124. reflex/components/recharts/cartesian.pyi +97 -21
  125. reflex/components/recharts/charts.pyi +62 -14
  126. reflex/components/recharts/general.pyi +32 -8
  127. reflex/components/recharts/polar.py +1 -1
  128. reflex/components/recharts/polar.pyi +33 -9
  129. reflex/components/recharts/recharts.pyi +12 -4
  130. reflex/components/sonner/toast.pyi +7 -2
  131. reflex/components/suneditor/editor.pyi +7 -3
  132. reflex/config.py +18 -1
  133. reflex/constants/installer.py +22 -1
  134. reflex/custom_components/custom_components.py +12 -7
  135. reflex/event.py +26 -10
  136. reflex/experimental/__init__.py +17 -6
  137. reflex/experimental/layout.pyi +27 -7
  138. reflex/model.py +3 -3
  139. reflex/reflex.py +33 -18
  140. reflex/state.py +4 -4
  141. reflex/style.py +2 -2
  142. reflex/testing.py +17 -5
  143. reflex/utils/console.py +2 -3
  144. reflex/utils/exec.py +4 -0
  145. reflex/utils/imports.py +14 -7
  146. reflex/utils/net.py +107 -18
  147. reflex/utils/prerequisites.py +92 -13
  148. reflex/utils/processes.py +52 -19
  149. reflex/utils/pyi_generator.py +66 -53
  150. reflex/utils/redir.py +3 -1
  151. reflex/utils/registry.py +15 -5
  152. reflex/utils/serializers.py +1 -2
  153. reflex/utils/types.py +4 -4
  154. reflex/vars/base.py +58 -22
  155. reflex/vars/number.py +23 -6
  156. reflex/vars/sequence.py +2 -0
  157. {reflex-0.7.4a2.dist-info → reflex-0.7.5.dist-info}/METADATA +2 -2
  158. {reflex-0.7.4a2.dist-info → reflex-0.7.5.dist-info}/RECORD +162 -162
  159. /reflex/{experimental → utils}/misc.py +0 -0
  160. {reflex-0.7.4a2.dist-info → reflex-0.7.5.dist-info}/WHEEL +0 -0
  161. {reflex-0.7.4a2.dist-info → reflex-0.7.5.dist-info}/entry_points.txt +0 -0
  162. {reflex-0.7.4a2.dist-info → reflex-0.7.5.dist-info}/licenses/LICENSE +0 -0
reflex/reflex.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import atexit
6
6
  from pathlib import Path
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  import typer
9
10
  import typer.core
@@ -429,7 +430,7 @@ def logout(
429
430
 
430
431
  loglevel = loglevel or get_config().loglevel
431
432
 
432
- logout(loglevel) # pyright: ignore [reportArgumentType]
433
+ logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel))
433
434
 
434
435
 
435
436
  db_cli = typer.Typer()
@@ -581,7 +582,6 @@ def deploy(
581
582
  ),
582
583
  ):
583
584
  """Deploy the app to the Reflex hosting service."""
584
- from reflex_cli.constants.base import LogLevel as HostingLogLevel
585
585
  from reflex_cli.utils import dependency
586
586
  from reflex_cli.v2 import cli as hosting_cli
587
587
  from reflex_cli.v2.deployments import check_version
@@ -604,21 +604,6 @@ def deploy(
604
604
  # Set the log level.
605
605
  console.set_log_level(loglevel)
606
606
 
607
- def convert_reflex_loglevel_to_reflex_cli_loglevel(
608
- loglevel: constants.LogLevel,
609
- ) -> HostingLogLevel:
610
- if loglevel == constants.LogLevel.DEBUG:
611
- return HostingLogLevel.DEBUG
612
- if loglevel == constants.LogLevel.INFO:
613
- return HostingLogLevel.INFO
614
- if loglevel == constants.LogLevel.WARNING:
615
- return HostingLogLevel.WARNING
616
- if loglevel == constants.LogLevel.ERROR:
617
- return HostingLogLevel.ERROR
618
- if loglevel == constants.LogLevel.CRITICAL:
619
- return HostingLogLevel.CRITICAL
620
- return HostingLogLevel.INFO
621
-
622
607
  # Only check requirements if interactive.
623
608
  # There is user interaction for requirements update.
624
609
  if interactive:
@@ -652,7 +637,7 @@ def deploy(
652
637
  envfile=envfile,
653
638
  hostname=hostname,
654
639
  interactive=interactive,
655
- loglevel=convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
640
+ loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
656
641
  token=token,
657
642
  project=project,
658
643
  project_name=project_name,
@@ -676,6 +661,36 @@ def rename(
676
661
  prerequisites.rename_app(new_name, loglevel)
677
662
 
678
663
 
664
+ if TYPE_CHECKING:
665
+ from reflex_cli.constants.base import LogLevel as HostingLogLevel
666
+
667
+
668
+ def _convert_reflex_loglevel_to_reflex_cli_loglevel(
669
+ loglevel: constants.LogLevel,
670
+ ) -> HostingLogLevel:
671
+ """Convert a Reflex log level to a Reflex CLI log level.
672
+
673
+ Args:
674
+ loglevel: The Reflex log level to convert.
675
+
676
+ Returns:
677
+ The converted Reflex CLI log level.
678
+ """
679
+ from reflex_cli.constants.base import LogLevel as HostingLogLevel
680
+
681
+ if loglevel == constants.LogLevel.DEBUG:
682
+ return HostingLogLevel.DEBUG
683
+ if loglevel == constants.LogLevel.INFO:
684
+ return HostingLogLevel.INFO
685
+ if loglevel == constants.LogLevel.WARNING:
686
+ return HostingLogLevel.WARNING
687
+ if loglevel == constants.LogLevel.ERROR:
688
+ return HostingLogLevel.ERROR
689
+ if loglevel == constants.LogLevel.CRITICAL:
690
+ return HostingLogLevel.CRITICAL
691
+ return HostingLogLevel.INFO
692
+
693
+
679
694
  cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
680
695
  cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
681
696
  cli.add_typer(
reflex/state.py CHANGED
@@ -593,8 +593,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
593
593
  if cls._item_is_event_handler(name, fn)
594
594
  }
595
595
 
596
- for mixin in cls._mixins(): # pyright: ignore [reportAssignmentType]
597
- for name, value in mixin.__dict__.items():
596
+ for mixin_cls in cls._mixins():
597
+ for name, value in mixin_cls.__dict__.items():
598
598
  if name in cls.inherited_vars:
599
599
  continue
600
600
  if is_computed_var(value):
@@ -605,7 +605,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
605
605
  cls.computed_vars[newcv._js_expr] = newcv
606
606
  cls.vars[newcv._js_expr] = newcv
607
607
  continue
608
- if types.is_backend_base_variable(name, mixin): # pyright: ignore [reportArgumentType]
608
+ if types.is_backend_base_variable(name, mixin_cls):
609
609
  cls.backend_vars[name] = copy.deepcopy(value)
610
610
  continue
611
611
  if events.get(name) is not None:
@@ -907,7 +907,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
907
907
  raise ValueError(f"Only one parent state is allowed {parent_states}.")
908
908
  # The first non-mixin state in the mro is our parent.
909
909
  for base in cls.mro()[1:]:
910
- if base._mixin or not issubclass(base, BaseState):
910
+ if not issubclass(base, BaseState) or base._mixin:
911
911
  continue
912
912
  if base is BaseState:
913
913
  break
reflex/style.py CHANGED
@@ -237,10 +237,10 @@ def format_style_key(key: str) -> tuple[str, ...]:
237
237
  EMPTY_VAR_DATA = VarData()
238
238
 
239
239
 
240
- class Style(dict):
240
+ class Style(dict[str, Any]):
241
241
  """A style dictionary."""
242
242
 
243
- def __init__(self, style_dict: dict | None = None, **kwargs):
243
+ def __init__(self, style_dict: dict[str, Any] | None = None, **kwargs):
244
244
  """Initialize the style.
245
245
 
246
246
  Args:
reflex/testing.py CHANGED
@@ -27,6 +27,7 @@ from typing import (
27
27
  Callable,
28
28
  Coroutine,
29
29
  Optional,
30
+ Sequence,
30
31
  Type,
31
32
  TypeVar,
32
33
  )
@@ -268,6 +269,10 @@ class AppHarness:
268
269
  loglevel=reflex.constants.LogLevel.INFO,
269
270
  )
270
271
  self.app_module_path.write_text(source_code)
272
+ else:
273
+ # Just initialize the web folder.
274
+ with chdir(self.app_path):
275
+ reflex.utils.prerequisites.initialize_frontend_dependencies()
271
276
  with chdir(self.app_path):
272
277
  # ensure config and app are reloaded when testing different app
273
278
  reflex.config.get_config(reload=True)
@@ -290,8 +295,10 @@ class AppHarness:
290
295
  if self.app_instance and isinstance(
291
296
  self.app_instance._state_manager, StateManagerRedis
292
297
  ):
298
+ if self.app_instance._state is None:
299
+ raise RuntimeError("State is not set.")
293
300
  # Create our own redis connection for testing.
294
- self.state_manager = StateManagerRedis.create(self.app_instance._state) # pyright: ignore [reportArgumentType]
301
+ self.state_manager = StateManagerRedis.create(self.app_instance._state)
295
302
  else:
296
303
  self.state_manager = (
297
304
  self.app_instance._state_manager if self.app_instance else None
@@ -455,6 +462,10 @@ class AppHarness:
455
462
 
456
463
  def stop(self) -> None:
457
464
  """Stop the frontend and backend servers."""
465
+ # Quit browsers first to avoid any lingering events being sent during shutdown.
466
+ for driver in self._frontends:
467
+ driver.quit()
468
+
458
469
  self._reload_state_module()
459
470
 
460
471
  if self.backend is not None:
@@ -485,8 +496,6 @@ class AppHarness:
485
496
  self.backend_thread.join()
486
497
  if self.frontend_output_thread is not None:
487
498
  self.frontend_output_thread.join()
488
- for driver in self._frontends:
489
- driver.quit()
490
499
 
491
500
  # Cleanup decorated pages added during testing
492
501
  for page in self._decorated_pages:
@@ -764,7 +773,7 @@ class AppHarness:
764
773
  self,
765
774
  element: "WebElement",
766
775
  timeout: TimeoutType = None,
767
- exp_not_equal: str = "",
776
+ exp_not_equal: str | Sequence[str] = "",
768
777
  ) -> str | None:
769
778
  """Poll element.get_attribute("value") for change.
770
779
 
@@ -779,8 +788,11 @@ class AppHarness:
779
788
  Raises:
780
789
  TimeoutError: when the timeout expires before value changes
781
790
  """
791
+ exp_not_equal = (
792
+ (exp_not_equal,) if isinstance(exp_not_equal, str) else exp_not_equal
793
+ )
782
794
  if not self._poll_for(
783
- target=lambda: element.get_attribute("value") != exp_not_equal,
795
+ target=lambda: element.get_attribute("value") not in exp_not_equal,
784
796
  timeout=timeout,
785
797
  ):
786
798
  raise TimeoutError(
reflex/utils/console.py CHANGED
@@ -201,10 +201,9 @@ def _get_first_non_framework_frame() -> FrameType | None:
201
201
  # Exclude utility modules that should never be the source of deprecated reflex usage.
202
202
  exclude_modules = [click, rx, typer, typing_extensions]
203
203
  exclude_roots = [
204
- p.parent.resolve()
205
- if (p := Path(m.__file__)).name == "__init__.py" # pyright: ignore [reportArgumentType]
206
- else p.resolve()
204
+ p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve()
207
205
  for m in exclude_modules
206
+ if (file := m.__file__)
208
207
  ]
209
208
  # Specifically exclude the reflex cli module.
210
209
  if reflex_bin := shutil.which(b"reflex"):
reflex/utils/exec.py CHANGED
@@ -330,6 +330,7 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
330
330
  log_level=loglevel.value,
331
331
  reload=True,
332
332
  reload_dirs=list(map(str, get_reload_paths())),
333
+ reload_delay=0.1,
333
334
  )
334
335
 
335
336
 
@@ -356,6 +357,9 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
356
357
  log_level=LogLevels(loglevel.value),
357
358
  reload=True,
358
359
  reload_paths=get_reload_paths(),
360
+ reload_ignore_worker_failure=True,
361
+ reload_tick=100,
362
+ workers_kill_timeout=2,
359
363
  ).serve()
360
364
 
361
365
 
reflex/utils/imports.py CHANGED
@@ -4,11 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import dataclasses
6
6
  from collections import defaultdict
7
- from typing import DefaultDict, Union
7
+ from typing import DefaultDict, Mapping, Sequence, Union
8
8
 
9
9
 
10
10
  def merge_imports(
11
- *imports: ImportDict | ParsedImportDict | ImmutableParsedImportDict,
11
+ *imports: ImportDict | ParsedImportDict | ParsedImportTuple,
12
12
  ) -> ParsedImportDict:
13
13
  """Merge multiple import dicts together.
14
14
 
@@ -43,7 +43,9 @@ def merge_imports(
43
43
  return all_imports
44
44
 
45
45
 
46
- def parse_imports(imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
46
+ def parse_imports(
47
+ imports: ImmutableImportDict | ImmutableParsedImportDict,
48
+ ) -> ParsedImportDict:
47
49
  """Parse the import dict into a standard format.
48
50
 
49
51
  Args:
@@ -53,10 +55,12 @@ def parse_imports(imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
53
55
  The parsed import dict.
54
56
  """
55
57
 
56
- def _make_list(value: ImportTypes) -> list[str | ImportVar] | list[ImportVar]:
58
+ def _make_list(
59
+ value: ImmutableImportTypes,
60
+ ) -> list[str | ImportVar] | list[ImportVar]:
57
61
  if isinstance(value, (str, ImportVar)):
58
62
  return [value]
59
- return value
63
+ return list(value)
60
64
 
61
65
  return {
62
66
  package: [
@@ -68,7 +72,7 @@ def parse_imports(imports: ImportDict | ParsedImportDict) -> ParsedImportDict:
68
72
 
69
73
 
70
74
  def collapse_imports(
71
- imports: ParsedImportDict | ImmutableParsedImportDict,
75
+ imports: ParsedImportDict | ParsedImportTuple,
72
76
  ) -> ParsedImportDict:
73
77
  """Remove all duplicate ImportVar within an ImportDict.
74
78
 
@@ -132,6 +136,9 @@ class ImportVar:
132
136
 
133
137
 
134
138
  ImportTypes = Union[str, ImportVar, list[str | ImportVar], list[ImportVar]]
139
+ ImmutableImportTypes = Union[str, ImportVar, Sequence[str | ImportVar]]
135
140
  ImportDict = dict[str, ImportTypes]
141
+ ImmutableImportDict = Mapping[str, ImmutableImportTypes]
136
142
  ParsedImportDict = dict[str, list[ImportVar]]
137
- ImmutableParsedImportDict = tuple[tuple[str, tuple[ImportVar, ...]], ...]
143
+ ImmutableParsedImportDict = Mapping[str, Sequence[ImportVar]]
144
+ ParsedImportTuple = tuple[tuple[str, tuple[ImportVar, ...]], ...]
reflex/utils/net.py CHANGED
@@ -1,8 +1,13 @@
1
1
  """Helpers for downloading files from the network."""
2
2
 
3
+ import functools
4
+ import time
5
+ from typing import Callable, ParamSpec, TypeVar
6
+
3
7
  import httpx
4
8
 
5
- from ..config import environment
9
+ from reflex.utils.decorator import once
10
+
6
11
  from . import console
7
12
 
8
13
 
@@ -12,30 +17,114 @@ def _httpx_verify_kwarg() -> bool:
12
17
  Returns:
13
18
  True if SSL verification is enabled, False otherwise
14
19
  """
20
+ from ..config import environment
21
+
15
22
  return not environment.SSL_NO_VERIFY.get()
16
23
 
17
24
 
18
- def get(url: str, **kwargs) -> httpx.Response:
19
- """Make an HTTP GET request.
25
+ _P = ParamSpec("_P")
26
+ _T = TypeVar("_T")
27
+
28
+
29
+ def _wrap_https_func(
30
+ func: Callable[_P, _T],
31
+ ) -> Callable[_P, _T]:
32
+ """Wrap an HTTPS function with logging.
20
33
 
21
34
  Args:
22
- url: The URL to request.
23
- **kwargs: Additional keyword arguments to pass to httpx.get.
35
+ func: The function to wrap.
24
36
 
25
37
  Returns:
26
- The response object.
38
+ The wrapped function.
39
+ """
40
+
41
+ @functools.wraps(func)
42
+ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
43
+ url = args[0]
44
+ console.debug(f"Sending HTTPS request to {args[0]}")
45
+ initial_time = time.time()
46
+ try:
47
+ response = func(*args, **kwargs)
48
+ except httpx.ConnectError as err:
49
+ if "CERTIFICATE_VERIFY_FAILED" in str(err):
50
+ # If the error is a certificate verification error, recommend mitigating steps.
51
+ console.error(
52
+ f"Certificate verification failed for {url}. Set environment variable SSL_CERT_FILE to the "
53
+ "path of the certificate file or SSL_NO_VERIFY=1 to disable verification."
54
+ )
55
+ raise
56
+ else:
57
+ console.debug(
58
+ f"Received response from {url} in {time.time() - initial_time:.3f} seconds"
59
+ )
60
+ return response
61
+
62
+ return wrapper
63
+
27
64
 
28
- Raises:
29
- httpx.ConnectError: If the connection cannot be established.
65
+ def _is_ipv4_supported() -> bool:
66
+ """Determine if the system supports IPv4.
67
+
68
+ Returns:
69
+ True if the system supports IPv4, False otherwise.
30
70
  """
31
- kwargs.setdefault("verify", _httpx_verify_kwarg())
32
71
  try:
33
- return httpx.get(url, **kwargs)
34
- except httpx.ConnectError as err:
35
- if "CERTIFICATE_VERIFY_FAILED" in str(err):
36
- # If the error is a certificate verification error, recommend mitigating steps.
37
- console.error(
38
- f"Certificate verification failed for {url}. Set environment variable SSL_CERT_FILE to the "
39
- "path of the certificate file or SSL_NO_VERIFY=1 to disable verification."
40
- )
41
- raise
72
+ httpx.head("http://1.1.1.1", timeout=3)
73
+ except httpx.RequestError:
74
+ return False
75
+ else:
76
+ return True
77
+
78
+
79
+ def _is_ipv6_supported() -> bool:
80
+ """Determine if the system supports IPv6.
81
+
82
+ Returns:
83
+ True if the system supports IPv6, False otherwise.
84
+ """
85
+ try:
86
+ httpx.head("http://[2606:4700:4700::1111]", timeout=3)
87
+ except httpx.RequestError:
88
+ return False
89
+ else:
90
+ return True
91
+
92
+
93
+ def _should_use_ipv6() -> bool:
94
+ """Determine if the system supports IPv6.
95
+
96
+ Returns:
97
+ True if the system supports IPv6, False otherwise.
98
+ """
99
+ return not _is_ipv4_supported() and _is_ipv6_supported()
100
+
101
+
102
+ def _httpx_local_address_kwarg() -> str:
103
+ """Get the value of the HTTPX local_address keyword argument.
104
+
105
+ Returns:
106
+ The local address to bind to
107
+ """
108
+ from ..config import environment
109
+
110
+ return environment.REFLEX_HTTP_CLIENT_BIND_ADDRESS.get() or (
111
+ "::" if _should_use_ipv6() else "0.0.0.0"
112
+ )
113
+
114
+
115
+ @once
116
+ def _httpx_client() -> httpx.Client:
117
+ """Get an HTTPX client.
118
+
119
+ Returns:
120
+ An HTTPX client.
121
+ """
122
+ return httpx.Client(
123
+ transport=httpx.HTTPTransport(
124
+ local_address=_httpx_local_address_kwarg(),
125
+ verify=_httpx_verify_kwarg(),
126
+ )
127
+ )
128
+
129
+
130
+ get = _wrap_https_func(_httpx_client().get)