reflex-hosting-cli 0.1.13__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.
reflex_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CLI library for the hosting service."""
reflex_cli/cli.py ADDED
@@ -0,0 +1,362 @@
1
+ """CLI for the hosting service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ from datetime import datetime
9
+ from typing import Callable
10
+
11
+ from reflex_cli import constants
12
+ from reflex_cli.utils import console
13
+
14
+
15
+ def login(
16
+ loglevel: str = constants.LogLevel.INFO.value,
17
+ ):
18
+ """Authenticate with Reflex hosting service.
19
+
20
+ Args:
21
+ loglevel: The log level to use.
22
+
23
+ Raises:
24
+ SystemExit: If the command fails.
25
+ """
26
+ from reflex_cli.utils import hosting
27
+
28
+ # Set the log level.
29
+ console.set_log_level(constants.LogLevel(loglevel))
30
+
31
+ access_token, invitation_code = hosting.authenticated_token()
32
+ if access_token:
33
+ console.print("You already logged in.")
34
+ return
35
+
36
+ # If not already logged in, open a browser window/tab to the login page.
37
+ access_token = hosting.authenticate_on_browser(invitation_code)
38
+
39
+ if not access_token:
40
+ console.error(f"Unable to authenticate. Please try again or contact support.")
41
+ raise SystemExit(1)
42
+
43
+ console.print("Successfully logged in.")
44
+
45
+
46
+ def logout(
47
+ loglevel: str = constants.LogLevel.INFO.value,
48
+ ):
49
+ """Log out of access to Reflex hosting service.
50
+
51
+ Args:
52
+ loglevel: The log level to use.
53
+ """
54
+ from reflex_cli.utils import hosting
55
+
56
+ console.set_log_level(constants.LogLevel(loglevel))
57
+
58
+ hosting.log_out_on_browser()
59
+ console.debug("Deleting access token from config locally")
60
+ hosting.delete_token_from_config(include_invitation_code=True)
61
+
62
+
63
+ def deploy(
64
+ app_name: str,
65
+ export_fn: Callable[[str, str, str, bool, bool, bool], None],
66
+ regions: list[str] | None = None,
67
+ key: str | None = None,
68
+ envs: list[str] | None = None,
69
+ cpus: int | None = None,
70
+ memory_mb: int | None = None,
71
+ auto_start: bool | None = None,
72
+ auto_stop: bool | None = None,
73
+ frontend_hostname: str | None = None,
74
+ interactive: bool = True,
75
+ with_metrics: str | None = None,
76
+ with_tracing: str | None = None,
77
+ share_deployment: bool | None = None,
78
+ loglevel: str = constants.LogLevel.INFO.value,
79
+ ):
80
+ """Deploy the app to the Reflex hosting service.
81
+
82
+ Args:
83
+ app_name: The name of the app.
84
+ export_fn: The function from the reflex main framework to export the app.
85
+ regions: The regions to deploy to.
86
+ key: The deployment key.
87
+ envs: The environment variables to set.
88
+ cpus: The number of CPUs to allocate.
89
+ memory_mb: The amount of memory to allocate in MB.
90
+ auto_start: Whether to automatically start the deployment.
91
+ auto_stop: Whether to automatically stop the deployment.
92
+ frontend_hostname: The hostname to use for the frontend.
93
+ interactive: Whether to use interactive mode.
94
+ with_metrics: The metrics prefix to use if enabling metrics.
95
+ with_tracing: The tracing prefix to use if enabling tracing.
96
+ share_deployment: Whether to share the deployment. If None, prompt the user when in interactive mode.
97
+ loglevel: The log level to use.
98
+
99
+ Raises:
100
+ SystemExit: If the command fails.
101
+ """
102
+ from reflex_cli.utils import hosting
103
+
104
+ # Set the log level.
105
+ console.set_log_level(constants.LogLevel(loglevel))
106
+
107
+ envs = envs or []
108
+ api_url = ""
109
+ deploy_url = ""
110
+
111
+ if not interactive and not key:
112
+ console.error(
113
+ "Please provide a name for the deployed instance when not in interactive mode."
114
+ )
115
+ raise SystemExit(1)
116
+
117
+ if interactive and share_deployment is None:
118
+ if (
119
+ console.ask(
120
+ "Do you want to share your app in the Gallery?",
121
+ choices=["y", "n"],
122
+ default="y",
123
+ )
124
+ == "y"
125
+ ):
126
+ share_deployment = True
127
+ console.print(
128
+ "We will ask for more information later. Let's proceed to deploy."
129
+ )
130
+ else:
131
+ share_deployment = False
132
+ console.print(
133
+ "No worries. You can do this later by running `reflex deployments share`."
134
+ )
135
+
136
+ enabled_regions = None
137
+ # If there is already a key, then it is passed in from CLI option in the non-interactive mode
138
+ if key is not None and not hosting.is_valid_deployment_key(key):
139
+ console.error(
140
+ f"Deployment key {key} is not valid. Please use only domain name safe characters."
141
+ )
142
+ raise SystemExit(1)
143
+ try:
144
+ # Send a request to server to obtain necessary information
145
+ # in preparation of a deployment. For example,
146
+ # server can return confirmation of a particular deployment key,
147
+ # is available, or suggest a new key, or return an existing deployment.
148
+ # Some of these are used in the interactive mode.
149
+ pre_deploy_response = hosting.prepare_deploy(
150
+ app_name, key=key, frontend_hostname=frontend_hostname
151
+ )
152
+ # Note: we likely won't need to fetch this twice
153
+ if pre_deploy_response.enabled_regions is not None:
154
+ enabled_regions = pre_deploy_response.enabled_regions
155
+
156
+ except Exception as ex:
157
+ console.error(f"Unable to prepare deployment")
158
+ raise SystemExit(1) from ex
159
+
160
+ # The app prefix should not change during the time of preparation
161
+ app_prefix = pre_deploy_response.app_prefix
162
+ if not interactive:
163
+ # in this case, the key was supplied for the pre_deploy call, at this point the reply is expected
164
+ if (reply := pre_deploy_response.reply) is None:
165
+ console.error(f"Unable to deploy at this name {key}.")
166
+ raise SystemExit(1)
167
+ api_url = reply.api_url
168
+ deploy_url = reply.deploy_url
169
+ else:
170
+ (
171
+ key_candidate,
172
+ api_url,
173
+ deploy_url,
174
+ ) = hosting.interactive_get_deployment_key_from_user_input(
175
+ pre_deploy_response, app_name, frontend_hostname=frontend_hostname
176
+ )
177
+ if not key_candidate or not api_url or not deploy_url:
178
+ console.error("Unable to find a suitable deployment key.")
179
+ raise SystemExit(1)
180
+
181
+ # Now copy over the candidate to the key
182
+ key = key_candidate
183
+
184
+ regions = hosting.prompt_for_regions(
185
+ enabled_regions=enabled_regions, regions_args=regions
186
+ )
187
+
188
+ # process the envs
189
+ envs = hosting.interactive_prompt_for_envs()
190
+
191
+ # Check the required params are valid
192
+ console.debug(
193
+ f"{key=}, {regions=}, {app_name=}, {app_prefix=}, {api_url=}, {deploy_url=}"
194
+ )
195
+ if (
196
+ not key
197
+ or not regions
198
+ or not app_name
199
+ or not app_prefix
200
+ or not api_url
201
+ or not deploy_url
202
+ ):
203
+ console.error("Please provide all the required parameters.")
204
+ raise SystemExit(1)
205
+ # Note: if the user uses --no-interactive mode, there was no prepare_deploy call
206
+ # so we do not check the regions until the call to hosting server
207
+
208
+ processed_envs = hosting.process_envs(envs) if envs else None
209
+
210
+ # Compile the app in production mode: backend first then frontend.
211
+ tmp_dir = tempfile.mkdtemp()
212
+
213
+ # Try zipping backend first
214
+ try:
215
+ export_fn(tmp_dir, api_url, deploy_url, False, True, True)
216
+ except Exception as ex:
217
+ console.error(f"Unable to export due to: {ex}")
218
+ if os.path.exists(tmp_dir):
219
+ shutil.rmtree(tmp_dir)
220
+ raise SystemExit(1) from ex
221
+
222
+ backend_file_name = constants.ComponentName.BACKEND.zip()
223
+
224
+ console.print("Uploading Backend code and sending request ...")
225
+ backend_deploy_requested_at = datetime.now().astimezone()
226
+ console.debug(f"{backend_deploy_requested_at=}")
227
+ try:
228
+ backend_deploy_response = hosting.deploy_backend(
229
+ backend_file_name=backend_file_name,
230
+ export_dir=tmp_dir,
231
+ key=key,
232
+ app_name=app_name,
233
+ regions=regions,
234
+ app_prefix=app_prefix,
235
+ cpus=cpus,
236
+ memory_mb=memory_mb,
237
+ auto_start=auto_start,
238
+ auto_stop=auto_stop,
239
+ envs=processed_envs,
240
+ with_metrics=with_metrics,
241
+ with_tracing=with_tracing,
242
+ )
243
+ except Exception as ex:
244
+ console.error(f"Unable to deploy due to: {ex}")
245
+ if os.path.exists(tmp_dir):
246
+ shutil.rmtree(tmp_dir)
247
+ raise SystemExit(1) from ex
248
+
249
+ if backend_deploy_response.sidecar_url is None:
250
+ console.error(
251
+ "Deploy backend response from server missing sidecar_url. This is unexpected. Contact support."
252
+ )
253
+ raise SystemExit(1)
254
+
255
+ # Deployment will actually start when data plane reconciles this request
256
+ console.debug(f"deploy_response: {backend_deploy_response}")
257
+ console.print("[bold]Backend deployment will start shortly.")
258
+
259
+ # Zip frontend
260
+ try:
261
+ export_fn(tmp_dir, api_url, deploy_url, True, False, True)
262
+ except ImportError as ie:
263
+ console.error(
264
+ f"Encountered ImportError, did you install all the dependencies? {ie}"
265
+ )
266
+ if os.path.exists(tmp_dir):
267
+ shutil.rmtree(tmp_dir)
268
+ raise SystemExit(1) from ie
269
+ except Exception as ex:
270
+ console.error(f"Unable to export due to: {ex}")
271
+ if os.path.exists(tmp_dir):
272
+ shutil.rmtree(tmp_dir)
273
+ raise SystemExit(1) from ex
274
+
275
+ frontend_file_name = constants.ComponentName.FRONTEND.zip()
276
+
277
+ console.print("Uploading Frontend code and sending request ...")
278
+ frontend_deploy_requested_at = datetime.now().astimezone()
279
+ console.debug(f"{frontend_deploy_requested_at=}")
280
+ try:
281
+ frontend_deploy_response = hosting.deploy_frontend(
282
+ frontend_file_name=frontend_file_name,
283
+ export_dir=tmp_dir,
284
+ initiator_event_id=backend_deploy_response.event_id,
285
+ app_prefix=app_prefix,
286
+ frontend_hostname=frontend_hostname,
287
+ )
288
+ except Exception as ex:
289
+ console.error(f"Unable to deploy due to: {ex}")
290
+ raise SystemExit(1) from ex
291
+ finally:
292
+ if os.path.exists(tmp_dir):
293
+ shutil.rmtree(tmp_dir)
294
+ console.debug(f"deploy_response: {frontend_deploy_response}")
295
+ console.print("[bold]Frontend deployment will start shortly.")
296
+
297
+ console.rule("[bold]Deploying production app.")
298
+ console.print(
299
+ f"[bold]Deployment will start shortly: {frontend_deploy_response.url} \nClosing this command now will not affect your deployment."
300
+ )
301
+
302
+ # If user elected to share the deployment, instead of just waiting, collect information from them.
303
+ if share_deployment:
304
+ hosting.collect_deployment_info_interactive(
305
+ demo_url=frontend_deploy_response.url
306
+ )
307
+ else:
308
+ # It takes a few seconds for the deployment request to be picked up by server
309
+ hosting.wait_for_server_to_pick_up_request()
310
+
311
+ console.print("Waiting for server to report progress ...")
312
+ # Display the key events such as build, deploy, etc based on the deploy event IDs from hosting service
313
+ server_report_deploy_success = hosting.poll_deploy_milestones(
314
+ key,
315
+ from_iso_timestamp=backend_deploy_requested_at,
316
+ deploy_event_ids=[
317
+ backend_deploy_response.event_id,
318
+ frontend_deploy_response.event_id,
319
+ ],
320
+ )
321
+
322
+ if server_report_deploy_success is None:
323
+ console.warn("The deployment may still be in progress. Proceeding ...")
324
+ elif not server_report_deploy_success:
325
+ console.error("Hosting server reports failure.")
326
+ console.error(
327
+ f"Check for more server logs using `reflex deployments build-logs {key}`"
328
+ )
329
+ raise SystemExit(1)
330
+
331
+ console.print("Waiting for the new deployment to come up")
332
+ backend_reachable, backend_poll_err = hosting.poll_backend_with_retries(
333
+ key=key,
334
+ from_iso_timestamp=datetime.now().astimezone(),
335
+ backend_url=backend_deploy_response.url,
336
+ sidecar_url=backend_deploy_response.sidecar_url,
337
+ )
338
+ if not backend_reachable:
339
+ if backend_poll_err is not None:
340
+ print(backend_poll_err)
341
+ console.error("Backend unreachable")
342
+ console.warn(
343
+ f"Check for more logs in your App, using `reflex deployments logs {key}`"
344
+ )
345
+ raise SystemExit(1)
346
+
347
+ if not (
348
+ frontend_reachable := hosting.poll_frontend_with_retries(
349
+ frontend_url=frontend_deploy_response.url
350
+ )
351
+ ):
352
+ console.error("Frontend unreachable")
353
+ raise SystemExit(1)
354
+
355
+ if backend_reachable and frontend_reachable:
356
+ console.print(
357
+ f"Your site [ {key} ] at {regions} is up: {frontend_deploy_response.url}"
358
+ )
359
+ return
360
+ console.warn(f"Your deployment is taking unusually long.")
361
+ console.warn(f"Check back later on its status: `reflex deployments status {key}`")
362
+ console.warn(f"For logs: `reflex deployments logs {key}`")
@@ -0,0 +1,8 @@
1
+ """The constants package."""
2
+
3
+
4
+ from .base import LogLevel, Reflex
5
+ from .compiler import ComponentName
6
+ from .hosting import Hosting, ReflexHostingCli, RequirementsTxt
7
+
8
+ __ALL__ = [Hosting, LogLevel, Reflex, ComponentName, ReflexHostingCli, RequirementsTxt]
@@ -0,0 +1,47 @@
1
+ """Base file for constants that don't fit any other categories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from types import SimpleNamespace
7
+
8
+ from platformdirs import PlatformDirs
9
+
10
+
11
+ class Reflex(SimpleNamespace):
12
+ """Base constants concerning Reflex. This is duplicate of the same class in reflex main."""
13
+
14
+ # The name of the Reflex package.
15
+ MODULE_NAME = "reflex"
16
+
17
+ # Files and directories used to init a new project.
18
+ # The directory to store reflex dependencies.
19
+ DIR = (
20
+ # on windows, we use C:/Users/<username>/AppData/Local/reflex.
21
+ # on macOS, we use ~/Library/Application Support/reflex.
22
+ # on linux, we use ~/.local/share/reflex.
23
+ PlatformDirs(MODULE_NAME, False).user_data_dir
24
+ )
25
+
26
+
27
+ # Log levels
28
+ class LogLevel(str, Enum):
29
+ """The log levels."""
30
+
31
+ DEBUG = "debug"
32
+ INFO = "info"
33
+ WARNING = "warning"
34
+ ERROR = "error"
35
+ CRITICAL = "critical"
36
+
37
+ def __le__(self, other: LogLevel) -> bool:
38
+ """Compare log levels.
39
+
40
+ Args:
41
+ other: The other log level.
42
+
43
+ Returns:
44
+ True if the log level is less than or equal to the other log level.
45
+ """
46
+ levels = list(LogLevel)
47
+ return levels.index(self) <= levels.index(other)
@@ -0,0 +1,31 @@
1
+ """Compiler variables."""
2
+ from enum import Enum
3
+ from types import SimpleNamespace
4
+
5
+
6
+ class Ext(SimpleNamespace):
7
+ """Extension used in Reflex."""
8
+
9
+ # The extension for JS files.
10
+ JS = ".js"
11
+ # The extension for python files.
12
+ PY = ".py"
13
+ # The extension for css files.
14
+ CSS = ".css"
15
+ # The extension for zip files.
16
+ ZIP = ".zip"
17
+
18
+
19
+ class ComponentName(Enum):
20
+ """Component names."""
21
+
22
+ BACKEND = "Backend"
23
+ FRONTEND = "Frontend"
24
+
25
+ def zip(self):
26
+ """Give the zip filename for the component.
27
+
28
+ Returns:
29
+ The lower-case filename with zip extension.
30
+ """
31
+ return self.value.lower() + Ext.ZIP
@@ -0,0 +1,45 @@
1
+ """Constants related to hosting."""
2
+ import os
3
+ from types import SimpleNamespace
4
+
5
+ from reflex_cli.constants.base import Reflex
6
+
7
+
8
+ class ReflexHostingCli(SimpleNamespace):
9
+ """Constants related to reflex-hosting-cli."""
10
+
11
+ MODULE_NAME = "reflex-hosting-cli"
12
+
13
+
14
+ class Hosting(SimpleNamespace):
15
+ """Constants related to hosting."""
16
+
17
+ # The hosting config json file
18
+ HOSTING_JSON = os.path.join(Reflex.DIR, "hosting_v0.json")
19
+ # The hosting service backend URL
20
+ CP_BACKEND_URL = os.environ.get(
21
+ "CP_BACKEND_URL", "https://rxcp-prod-control-plane.fly.dev"
22
+ )
23
+ # The hosting service webpage URL
24
+ CP_WEB_URL = os.environ.get("CP_WEB_URL", "https://control-plane.reflex.run")
25
+
26
+ # The number of times to try and wait for the user to complete web authentication.
27
+ WEB_AUTH_RETRIES = 60
28
+ # The time to sleep between requests to check if for authentication completion. In seconds.
29
+ WEB_AUTH_SLEEP_DURATION = 5
30
+ # The time to wait for the reflex app sidecar to come up. In seconds.
31
+ BACKEND_SIDECAR_WAIT_AND_PING_DURATION = 40
32
+ # The number of iterations to try reflex app /ping endpoint and query logs printed from app.
33
+ BACKEND_REFLEX_APP_PING_LOG_QUERY_RETRIES = 30
34
+ # The time to wait for the frontend to come up after user initiates deployment. In seconds.
35
+ FRONTEND_POLL_DURATION = 10
36
+
37
+
38
+ class RequirementsTxt(SimpleNamespace):
39
+ """Requirements.txt constants."""
40
+
41
+ # The requirements.txt file.
42
+ FILE = "requirements.txt"
43
+
44
+ # Number of unused packages for which we will throw a warning.
45
+ UNUSED_WARN_THRESHOLD = 20