reflex-hosting-cli 0.1.63__tar.gz → 0.1.65a1__tar.gz
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_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/PKG-INFO +1 -1
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/hosting.py +26 -9
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/cli.py +45 -58
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/deployments.py +5 -0
- reflex_hosting_cli-0.1.65a1/src/reflex_cli/v2/gcp.py +576 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/.gitignore +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/README.md +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/pyproject.toml +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/__init__.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/cli.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/__init__.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/base.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/compiler.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/hosting.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/core/__init__.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/core/config.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/deployments.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/__init__.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/console.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/dependency.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/exceptions.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/__init__.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/apps.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/project.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/secrets.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/utils.py +0 -0
- {reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/vmtypes_regions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reflex-hosting-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.65a1
|
|
4
4
|
Summary: Reflex Hosting CLI
|
|
5
5
|
Author-email: Nikhil Rao <nikhil@reflex.dev>, Alek Petuskey <alek@reflex.dev>
|
|
6
6
|
Maintainer-email: Simon Young <simon@reflex.dev>, Khaleel Al-Adhami <khaleel@reflex.dev>
|
|
@@ -216,16 +216,15 @@ class ScaleParams:
|
|
|
216
216
|
dict: The object as a dictionary.
|
|
217
217
|
|
|
218
218
|
"""
|
|
219
|
-
|
|
220
|
-
self.type = ScaleType.REGION
|
|
219
|
+
effective_type = self.type or ScaleType.REGION
|
|
221
220
|
return (
|
|
222
221
|
{
|
|
223
|
-
"type": str(
|
|
222
|
+
"type": str(effective_type.value),
|
|
224
223
|
"size": self.vm_type,
|
|
225
224
|
}
|
|
226
|
-
if
|
|
225
|
+
if effective_type == ScaleType.SIZE
|
|
227
226
|
else {
|
|
228
|
-
"type": str(
|
|
227
|
+
"type": str(effective_type.value),
|
|
229
228
|
"regions": {
|
|
230
229
|
region["name"]: region["number_of_machines"]
|
|
231
230
|
for region in self.regions
|
|
@@ -1079,6 +1078,20 @@ def select_project(project: str, token: str | None = None) -> str:
|
|
|
1079
1078
|
return f"{project} is now selected."
|
|
1080
1079
|
|
|
1081
1080
|
|
|
1081
|
+
def normalize_project_id(value: Any) -> str | None:
|
|
1082
|
+
"""Normalize a project ID value, treating empty/whitespace strings and non-strings as None.
|
|
1083
|
+
|
|
1084
|
+
Args:
|
|
1085
|
+
value: The raw project ID value from config, CLI args, or hosting.json.
|
|
1086
|
+
|
|
1087
|
+
Returns:
|
|
1088
|
+
The stripped project ID, or None if the value is missing or blank.
|
|
1089
|
+
"""
|
|
1090
|
+
if isinstance(value, str) and value.strip():
|
|
1091
|
+
return value.strip()
|
|
1092
|
+
return None
|
|
1093
|
+
|
|
1094
|
+
|
|
1082
1095
|
def get_selected_project() -> str | None:
|
|
1083
1096
|
"""Retrieve the currently selected project ID.
|
|
1084
1097
|
|
|
@@ -1089,10 +1102,10 @@ def get_selected_project() -> str | None:
|
|
|
1089
1102
|
try:
|
|
1090
1103
|
with constants.Hosting.HOSTING_JSON.open() as config_file:
|
|
1091
1104
|
hosting_config = json.load(config_file)
|
|
1092
|
-
return hosting_config.get("project")
|
|
1105
|
+
return normalize_project_id(hosting_config.get("project"))
|
|
1093
1106
|
except Exception as ex:
|
|
1094
1107
|
console.debug(
|
|
1095
|
-
f"Unable to
|
|
1108
|
+
f"Unable to read selected project from {constants.Hosting.HOSTING_JSON} due to: {ex}"
|
|
1096
1109
|
)
|
|
1097
1110
|
return None
|
|
1098
1111
|
|
|
@@ -1971,14 +1984,18 @@ def authenticate_on_browser() -> tuple[str, dict[str, Any]]:
|
|
|
1971
1984
|
constants.Hosting.HOSTING_SERVICE_UI, f"/cli/login?request_id={request_id}"
|
|
1972
1985
|
)
|
|
1973
1986
|
|
|
1974
|
-
console.print(f"Opening {auth_url} ...")
|
|
1975
|
-
|
|
1976
1987
|
if not is_valid_url(constants.Hosting.HOSTING_SERVICE_UI):
|
|
1977
1988
|
console.error(
|
|
1978
1989
|
f"Invalid hosting URL: {constants.Hosting.HOSTING_SERVICE_UI}. Ensure the URL is in the correct format and includes a valid scheme"
|
|
1979
1990
|
)
|
|
1980
1991
|
raise click.exceptions.Exit(1)
|
|
1981
1992
|
|
|
1993
|
+
console.print(
|
|
1994
|
+
f"Opening {auth_url} ... By connecting your account, you agree to "
|
|
1995
|
+
"Reflex Cloud [Terms of Service] and [Privacy Policy].",
|
|
1996
|
+
markup=False,
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1982
1999
|
if not webbrowser.open(auth_url):
|
|
1983
2000
|
console.warn(
|
|
1984
2001
|
f"Unable to automatically open the browser. Please go to {auth_url} to authenticate."
|
|
@@ -28,7 +28,8 @@ def login(
|
|
|
28
28
|
loglevel: The log level to use.
|
|
29
29
|
|
|
30
30
|
Returns:
|
|
31
|
-
Information about the logged in user
|
|
31
|
+
Information about the newly logged in user or empty dict if already
|
|
32
|
+
logged in.
|
|
32
33
|
|
|
33
34
|
Raises:
|
|
34
35
|
SystemExit: If the command fails.
|
|
@@ -42,7 +43,7 @@ def login(
|
|
|
42
43
|
access_token, validated_info = hosting.authenticated_token()
|
|
43
44
|
if access_token:
|
|
44
45
|
console.print("You already logged in.")
|
|
45
|
-
return
|
|
46
|
+
return {}
|
|
46
47
|
|
|
47
48
|
# If not already logged in, open a browser window/tab to the login page.
|
|
48
49
|
access_token, validated_info = hosting.authenticate_on_browser()
|
|
@@ -179,19 +180,24 @@ def deploy(
|
|
|
179
180
|
if not description:
|
|
180
181
|
description = config.get("description", None)
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
project_id = hosting.normalize_project_id(project_id)
|
|
184
|
+
|
|
183
185
|
if project_name and not project_id:
|
|
184
186
|
result = hosting.search_project(
|
|
185
187
|
project_name, client=authenticated_client, interactive=interactive
|
|
186
188
|
)
|
|
187
|
-
project_id = result.get("id") if result else None
|
|
189
|
+
project_id = hosting.normalize_project_id(result.get("id")) if result else None
|
|
190
|
+
|
|
191
|
+
selected_project_id = hosting.get_selected_project()
|
|
188
192
|
|
|
193
|
+
validated_project: dict[str, Any] | None = None
|
|
189
194
|
try:
|
|
190
|
-
|
|
195
|
+
if not project_id:
|
|
196
|
+
project_id = selected_project_id
|
|
191
197
|
if project_id:
|
|
192
|
-
hosting.get_project(
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
validated_project = hosting.get_project(
|
|
199
|
+
project_id, client=authenticated_client
|
|
200
|
+
)
|
|
195
201
|
except httpx.HTTPStatusError as ex:
|
|
196
202
|
try:
|
|
197
203
|
console.error(ex.response.json().get("detail"))
|
|
@@ -209,9 +215,7 @@ def deploy(
|
|
|
209
215
|
try:
|
|
210
216
|
if app_name and not app_id:
|
|
211
217
|
search_project_id = project_id
|
|
212
|
-
if
|
|
213
|
-
search_project_id = hosting.get_selected_project()
|
|
214
|
-
elif interactive and not project:
|
|
218
|
+
if interactive and not project:
|
|
215
219
|
search_project_id = None
|
|
216
220
|
|
|
217
221
|
app = hosting.search_app(
|
|
@@ -230,16 +234,13 @@ def deploy(
|
|
|
230
234
|
raise click.exceptions.Exit(1) from ex
|
|
231
235
|
|
|
232
236
|
if app and interactive and not project and not app_id:
|
|
233
|
-
default_project_id =
|
|
237
|
+
default_project_id = selected_project_id
|
|
234
238
|
app_project_id = app.get("project_id")
|
|
235
239
|
|
|
236
240
|
if app_project_id and (
|
|
237
241
|
not default_project_id or app_project_id != default_project_id
|
|
238
242
|
):
|
|
239
|
-
|
|
240
|
-
app_project_id, client=authenticated_client
|
|
241
|
-
)
|
|
242
|
-
app_project_name = app_project.get("name", "Unknown")
|
|
243
|
+
app_project_name = (app.get("project") or {}).get("name") or app_project_id
|
|
243
244
|
if (
|
|
244
245
|
console.ask(
|
|
245
246
|
f"Deploy to app '{app['name']}' in project '{app_project_name}'?",
|
|
@@ -262,55 +263,40 @@ def deploy(
|
|
|
262
263
|
)
|
|
263
264
|
== "y"
|
|
264
265
|
):
|
|
265
|
-
# Check if we need confirmation for deploying to non-default project
|
|
266
266
|
if not project:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
default_project_id = hosting.get_default_project(
|
|
278
|
-
authenticated_client
|
|
267
|
+
needs_confirmation = not selected_project_id or (
|
|
268
|
+
project_id and project_id != selected_project_id
|
|
269
|
+
)
|
|
270
|
+
if needs_confirmation:
|
|
271
|
+
if project_id:
|
|
272
|
+
project_display_name = (
|
|
273
|
+
(
|
|
274
|
+
validated_project.get("name")
|
|
275
|
+
if validated_project
|
|
276
|
+
else None
|
|
279
277
|
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
default_project_id, client=authenticated_client
|
|
283
|
-
)
|
|
284
|
-
project_name = default_project.get(
|
|
285
|
-
"name", "Default Project"
|
|
286
|
-
)
|
|
287
|
-
else:
|
|
288
|
-
project_name = "Default Project"
|
|
289
|
-
except Exception:
|
|
290
|
-
project_name = "Unknown"
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
console.ask(
|
|
294
|
-
f"Create and deploy app '{app_name}' in project '{project_name}'?",
|
|
295
|
-
choices=["y", "n"],
|
|
296
|
-
default="y",
|
|
278
|
+
or project_name
|
|
279
|
+
or project_id
|
|
297
280
|
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
elif project_id and project_id != default_project_id:
|
|
303
|
-
try:
|
|
304
|
-
target_project = hosting.get_project(
|
|
305
|
-
project_id, client=authenticated_client
|
|
281
|
+
else:
|
|
282
|
+
project_display_name = "your default project"
|
|
283
|
+
fallback_project_id = hosting.get_default_project(
|
|
284
|
+
authenticated_client
|
|
306
285
|
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
286
|
+
if fallback_project_id:
|
|
287
|
+
try:
|
|
288
|
+
fallback_project = hosting.get_project(
|
|
289
|
+
fallback_project_id, client=authenticated_client
|
|
290
|
+
)
|
|
291
|
+
project_display_name = (
|
|
292
|
+
fallback_project.get("name") or project_display_name
|
|
293
|
+
)
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
310
296
|
|
|
311
297
|
if (
|
|
312
298
|
console.ask(
|
|
313
|
-
f"Create and deploy app '{app_name}' in project '{
|
|
299
|
+
f"Create and deploy app '{app_name}' in project '{project_display_name}'?",
|
|
314
300
|
choices=["y", "n"],
|
|
315
301
|
default="y",
|
|
316
302
|
)
|
|
@@ -386,6 +372,7 @@ def deploy(
|
|
|
386
372
|
console.error(
|
|
387
373
|
"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`."""
|
|
388
374
|
)
|
|
375
|
+
raise click.exceptions.Exit(1) from None
|
|
389
376
|
|
|
390
377
|
# Compile the app in production mode: backend first then frontend.
|
|
391
378
|
temporary_dir = tempfile.TemporaryDirectory()
|
|
@@ -12,6 +12,7 @@ from packaging import version
|
|
|
12
12
|
from reflex_cli import constants
|
|
13
13
|
from reflex_cli.utils import console
|
|
14
14
|
from reflex_cli.v2.apps import apps_cli
|
|
15
|
+
from reflex_cli.v2.gcp import deploy_command as gcp_deploy_command
|
|
15
16
|
from reflex_cli.v2.project import project_cli
|
|
16
17
|
from reflex_cli.v2.secrets import secrets_cli
|
|
17
18
|
from reflex_cli.v2.vmtypes_regions import vm_types_regions_cli
|
|
@@ -64,6 +65,10 @@ hosting_cli.add_command(
|
|
|
64
65
|
secrets_cli,
|
|
65
66
|
name="secrets",
|
|
66
67
|
)
|
|
68
|
+
hosting_cli.add_command(
|
|
69
|
+
gcp_deploy_command,
|
|
70
|
+
name="deploy",
|
|
71
|
+
)
|
|
67
72
|
for name, command in vm_types_regions_cli.commands.items():
|
|
68
73
|
# Add the command to the hosting CLI
|
|
69
74
|
hosting_cli.add_command(command, name=name)
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""GCP Cloud Run deploy commands for the Reflex Cloud CLI.
|
|
2
|
+
|
|
3
|
+
Fetches a Dockerfile + bash deploy script from Reflex and runs the script
|
|
4
|
+
against the user's source directory. The Dockerfile is materialized inside
|
|
5
|
+
a Cloud Build job (via a ``cloudbuild.yaml`` written to a tempfile and
|
|
6
|
+
referenced with ``gcloud builds submit --config=...``) — the user's project
|
|
7
|
+
tree is never modified. The script reads its parameters from environment
|
|
8
|
+
variables (GCP_PROJECT, GCP_REGION, SERVICE_NAME, AR_REPO, VERSION,
|
|
9
|
+
REFLEX_CLOUDBUILD_YAML).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from urllib.parse import urljoin
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from reflex_cli import constants
|
|
28
|
+
from reflex_cli.utils import console
|
|
29
|
+
|
|
30
|
+
GCP_MANIFEST_ENDPOINT = "/api/v1/cli/gcp-cloud-run-manifest"
|
|
31
|
+
|
|
32
|
+
DOCKERFILE_NAME = "Dockerfile"
|
|
33
|
+
|
|
34
|
+
# Environment variables passed to the deploy script.
|
|
35
|
+
ENV_GCP_PROJECT = "GCP_PROJECT"
|
|
36
|
+
ENV_GCP_REGION = "GCP_REGION"
|
|
37
|
+
ENV_SERVICE_NAME = "SERVICE_NAME"
|
|
38
|
+
ENV_AR_REPO = "AR_REPO"
|
|
39
|
+
ENV_VERSION = "VERSION"
|
|
40
|
+
# Path to the Cloud Build config file written by the CLI. The rewritten
|
|
41
|
+
# deploy script references it as ``--config="${REFLEX_CLOUDBUILD_YAML}"``.
|
|
42
|
+
ENV_REFLEX_CLOUDBUILD_YAML = "REFLEX_CLOUDBUILD_YAML"
|
|
43
|
+
|
|
44
|
+
# Pattern for the start of the `gcloud builds submit` invocation in the
|
|
45
|
+
# Reflex deploy script. We rewrite that whole multi-line command to use
|
|
46
|
+
# `--config=` so the Dockerfile lives inside a cloudbuild.yaml instead of
|
|
47
|
+
# being staged on disk next to the user's source.
|
|
48
|
+
_BUILDS_SUBMIT_PATTERN = re.compile(
|
|
49
|
+
r"(?P<indent>^[ \t]*)gcloud[ \t]+builds[ \t]+submit\b",
|
|
50
|
+
re.MULTILINE,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Manifest response field names from Reflex.
|
|
54
|
+
FIELD_DOCKERFILE = "dockerfile"
|
|
55
|
+
FIELD_DEPLOY_COMMAND = "deploy_command"
|
|
56
|
+
|
|
57
|
+
# Allowlist of host environment variables forwarded to the deploy script.
|
|
58
|
+
# We deliberately exclude things like AWS_*/GITHUB_TOKEN/SSH agent sockets so a
|
|
59
|
+
# compromised or tampered manifest cannot exfiltrate unrelated credentials.
|
|
60
|
+
DEPLOY_ENV_ALLOWLIST = frozenset({
|
|
61
|
+
"PATH",
|
|
62
|
+
"HOME",
|
|
63
|
+
"USER",
|
|
64
|
+
"LOGNAME",
|
|
65
|
+
"SHELL",
|
|
66
|
+
"TERM",
|
|
67
|
+
"LANG",
|
|
68
|
+
"LC_ALL",
|
|
69
|
+
"LC_CTYPE",
|
|
70
|
+
"TMPDIR",
|
|
71
|
+
"TEMP",
|
|
72
|
+
"TMP",
|
|
73
|
+
"XDG_CONFIG_HOME",
|
|
74
|
+
# gcloud configuration
|
|
75
|
+
"CLOUDSDK_CONFIG",
|
|
76
|
+
"CLOUDSDK_ACTIVE_CONFIG_NAME",
|
|
77
|
+
"CLOUDSDK_CORE_PROJECT",
|
|
78
|
+
"CLOUDSDK_CORE_ACCOUNT",
|
|
79
|
+
"CLOUDSDK_AUTH_ACCESS_TOKEN_FILE",
|
|
80
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
81
|
+
# docker configuration
|
|
82
|
+
"DOCKER_HOST",
|
|
83
|
+
"DOCKER_TLS_VERIFY",
|
|
84
|
+
"DOCKER_CERT_PATH",
|
|
85
|
+
"DOCKER_CONFIG",
|
|
86
|
+
"DOCKER_BUILDKIT",
|
|
87
|
+
# corporate proxy / TLS trust
|
|
88
|
+
"HTTP_PROXY",
|
|
89
|
+
"HTTPS_PROXY",
|
|
90
|
+
"NO_PROXY",
|
|
91
|
+
"http_proxy",
|
|
92
|
+
"https_proxy",
|
|
93
|
+
"no_proxy",
|
|
94
|
+
"SSL_CERT_FILE",
|
|
95
|
+
"SSL_CERT_DIR",
|
|
96
|
+
"REQUESTS_CA_BUNDLE",
|
|
97
|
+
"CURL_CA_BUNDLE",
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.command(name="deploy")
|
|
102
|
+
@click.option(
|
|
103
|
+
"--gcp",
|
|
104
|
+
"use_gcp",
|
|
105
|
+
is_flag=True,
|
|
106
|
+
default=False,
|
|
107
|
+
help="Deploy to GCP Cloud Run. Required (the only supported target today).",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--gcp-project",
|
|
111
|
+
"gcp_project",
|
|
112
|
+
default=None,
|
|
113
|
+
help="The GCP project ID to deploy into (sets GCP_PROJECT). Required with --gcp.",
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--region",
|
|
117
|
+
default="us-central1",
|
|
118
|
+
show_default=True,
|
|
119
|
+
help="The GCP region for Cloud Run (sets GCP_REGION).",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--service-name",
|
|
123
|
+
default="reflex-app",
|
|
124
|
+
show_default=True,
|
|
125
|
+
help="The Cloud Run service name (sets SERVICE_NAME).",
|
|
126
|
+
)
|
|
127
|
+
@click.option(
|
|
128
|
+
"--ar-repo",
|
|
129
|
+
default="reflex",
|
|
130
|
+
show_default=True,
|
|
131
|
+
help="The Artifact Registry repository name (sets AR_REPO).",
|
|
132
|
+
)
|
|
133
|
+
@click.option(
|
|
134
|
+
"--version",
|
|
135
|
+
"version_tag",
|
|
136
|
+
default=None,
|
|
137
|
+
help="The image version tag (sets VERSION). Defaults to a UTC timestamp.",
|
|
138
|
+
)
|
|
139
|
+
@click.option(
|
|
140
|
+
"--source",
|
|
141
|
+
"source_dir",
|
|
142
|
+
default=".",
|
|
143
|
+
show_default=True,
|
|
144
|
+
type=click.Path(file_okay=False, dir_okay=True),
|
|
145
|
+
help="The directory containing the Reflex app. Uploaded to Cloud Build as the build context; the source tree itself is not modified.",
|
|
146
|
+
)
|
|
147
|
+
@click.option("--token", help="The Reflex authentication token.")
|
|
148
|
+
@click.option(
|
|
149
|
+
"--interactive/--no-interactive",
|
|
150
|
+
is_flag=True,
|
|
151
|
+
default=True,
|
|
152
|
+
help="Whether to prompt before running the deploy script.",
|
|
153
|
+
)
|
|
154
|
+
@click.option(
|
|
155
|
+
"--dry-run",
|
|
156
|
+
is_flag=True,
|
|
157
|
+
default=False,
|
|
158
|
+
help="Print the manifest and generated cloudbuild.yaml without writing the tempfile or running the script.",
|
|
159
|
+
)
|
|
160
|
+
@click.option(
|
|
161
|
+
"--loglevel",
|
|
162
|
+
type=click.Choice([level.value for level in constants.LogLevel]),
|
|
163
|
+
default=constants.LogLevel.INFO.value,
|
|
164
|
+
help="The log level to use.",
|
|
165
|
+
)
|
|
166
|
+
def deploy_command(
|
|
167
|
+
use_gcp: bool,
|
|
168
|
+
gcp_project: str | None,
|
|
169
|
+
region: str,
|
|
170
|
+
service_name: str,
|
|
171
|
+
ar_repo: str,
|
|
172
|
+
version_tag: str | None,
|
|
173
|
+
source_dir: str,
|
|
174
|
+
token: str | None,
|
|
175
|
+
interactive: bool,
|
|
176
|
+
dry_run: bool,
|
|
177
|
+
loglevel: str,
|
|
178
|
+
):
|
|
179
|
+
"""Deploy a Reflex app to a cloud target.
|
|
180
|
+
|
|
181
|
+
Currently the only supported target is GCP Cloud Run via --gcp. The
|
|
182
|
+
command fetches a Dockerfile and bash deploy script from Reflex, embeds
|
|
183
|
+
the Dockerfile inside a generated ``cloudbuild.yaml`` (written to a
|
|
184
|
+
tempfile), rewrites the script's ``gcloud builds submit`` invocation to
|
|
185
|
+
reference that config, then runs the script with cwd= your source dir.
|
|
186
|
+
Your project tree is never modified.
|
|
187
|
+
"""
|
|
188
|
+
from reflex_cli.utils import hosting
|
|
189
|
+
|
|
190
|
+
console.set_log_level(loglevel)
|
|
191
|
+
|
|
192
|
+
if not use_gcp:
|
|
193
|
+
console.error(
|
|
194
|
+
"Specify a deploy target. Currently supported: --gcp (GCP Cloud Run)."
|
|
195
|
+
)
|
|
196
|
+
raise click.exceptions.Exit(2)
|
|
197
|
+
if not gcp_project:
|
|
198
|
+
console.error("--gcp-project is required when using --gcp.")
|
|
199
|
+
raise click.exceptions.Exit(2)
|
|
200
|
+
|
|
201
|
+
authenticated_client = hosting.get_authenticated_client(
|
|
202
|
+
token=token, interactive=interactive
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
bash_path = shutil.which("bash")
|
|
206
|
+
if not bash_path:
|
|
207
|
+
console.error(
|
|
208
|
+
"`bash` was not found on PATH; required to run the deploy script."
|
|
209
|
+
)
|
|
210
|
+
raise click.exceptions.Exit(1)
|
|
211
|
+
|
|
212
|
+
gcloud_path = shutil.which("gcloud")
|
|
213
|
+
if not gcloud_path:
|
|
214
|
+
console.error(
|
|
215
|
+
"The `gcloud` CLI was not found on PATH. Install it from "
|
|
216
|
+
"https://cloud.google.com/sdk/docs/install and run `gcloud auth login` "
|
|
217
|
+
"and `gcloud auth application-default login` before retrying."
|
|
218
|
+
)
|
|
219
|
+
raise click.exceptions.Exit(1)
|
|
220
|
+
|
|
221
|
+
if not shutil.which("docker"):
|
|
222
|
+
console.error(
|
|
223
|
+
"The `docker` CLI was not found on PATH; required to build the image."
|
|
224
|
+
)
|
|
225
|
+
raise click.exceptions.Exit(1)
|
|
226
|
+
|
|
227
|
+
if not _get_active_gcp_account(gcloud_path):
|
|
228
|
+
console.error(
|
|
229
|
+
"No active GCP account found. Run `gcloud auth login` and "
|
|
230
|
+
"`gcloud auth application-default login`, then retry."
|
|
231
|
+
)
|
|
232
|
+
raise click.exceptions.Exit(1)
|
|
233
|
+
|
|
234
|
+
dockerfile, deploy_script = _request_manifest(authenticated_client.token)
|
|
235
|
+
|
|
236
|
+
source_path = Path(source_dir).resolve()
|
|
237
|
+
if not source_path.is_dir():
|
|
238
|
+
console.error(f"Source directory does not exist: {source_path}")
|
|
239
|
+
raise click.exceptions.Exit(1)
|
|
240
|
+
|
|
241
|
+
cloudbuild_yaml = _build_cloudbuild_yaml(dockerfile)
|
|
242
|
+
try:
|
|
243
|
+
deploy_script = _rewrite_builds_submit(deploy_script)
|
|
244
|
+
except ValueError as ex:
|
|
245
|
+
console.error(str(ex))
|
|
246
|
+
raise click.exceptions.Exit(1) from ex
|
|
247
|
+
|
|
248
|
+
version_value = version_tag or datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
249
|
+
deploy_env = {
|
|
250
|
+
ENV_GCP_PROJECT: gcp_project,
|
|
251
|
+
ENV_GCP_REGION: region,
|
|
252
|
+
ENV_SERVICE_NAME: service_name,
|
|
253
|
+
ENV_AR_REPO: ar_repo,
|
|
254
|
+
ENV_VERSION: version_value,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.info("Received deploy manifest from Reflex.")
|
|
258
|
+
console.print("")
|
|
259
|
+
console.print(f"Source: {source_path}")
|
|
260
|
+
console.print("Deploy environment:")
|
|
261
|
+
for key, value in deploy_env.items():
|
|
262
|
+
console.print(f" {key}={value}")
|
|
263
|
+
console.print("")
|
|
264
|
+
console.print("Deploy script (rewritten to use cloudbuild.yaml):")
|
|
265
|
+
console.print("─" * 60)
|
|
266
|
+
console.print(deploy_script)
|
|
267
|
+
console.print("─" * 60)
|
|
268
|
+
console.info(
|
|
269
|
+
f"The script runs with a restricted env (only {len(DEPLOY_ENV_ALLOWLIST)} "
|
|
270
|
+
"allowlisted host variables forwarded plus the deploy variables above)."
|
|
271
|
+
)
|
|
272
|
+
console.info(
|
|
273
|
+
"The Dockerfile is embedded in a Cloud Build config written to a "
|
|
274
|
+
"tempfile; your source directory is not modified."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if dry_run:
|
|
278
|
+
console.print("")
|
|
279
|
+
console.print("cloudbuild.yaml contents:")
|
|
280
|
+
console.print("─" * 60)
|
|
281
|
+
console.print(cloudbuild_yaml)
|
|
282
|
+
console.print("─" * 60)
|
|
283
|
+
console.print("")
|
|
284
|
+
console.print("Dockerfile contents (embedded in the build step):")
|
|
285
|
+
console.print("─" * 60)
|
|
286
|
+
console.print(dockerfile)
|
|
287
|
+
console.print("─" * 60)
|
|
288
|
+
console.info("Dry run — nothing staged or executed.")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
if interactive:
|
|
292
|
+
answer = console.ask(
|
|
293
|
+
"Run the deploy script now?", choices=["y", "n"], default="y"
|
|
294
|
+
)
|
|
295
|
+
if answer != "y":
|
|
296
|
+
console.warn("Aborted by user.")
|
|
297
|
+
raise click.exceptions.Exit(1)
|
|
298
|
+
|
|
299
|
+
with _temp_cloudbuild_yaml(cloudbuild_yaml) as cloudbuild_path:
|
|
300
|
+
exit_code = _run_deploy_script(
|
|
301
|
+
bash_path=bash_path,
|
|
302
|
+
script=deploy_script,
|
|
303
|
+
cwd=source_path,
|
|
304
|
+
env_overrides={
|
|
305
|
+
**deploy_env,
|
|
306
|
+
ENV_REFLEX_CLOUDBUILD_YAML: str(cloudbuild_path),
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
if exit_code != 0:
|
|
310
|
+
console.error(f"Deploy script exited with status {exit_code}.")
|
|
311
|
+
raise click.exceptions.Exit(exit_code)
|
|
312
|
+
console.success("Deployment finished.")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_active_gcp_account(gcloud_path: str) -> str | None:
|
|
316
|
+
"""Return the email of the active gcloud account, or None.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
gcloud_path: Resolved path to the gcloud executable.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
The active account email or None if not logged in.
|
|
323
|
+
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
result = subprocess.run(
|
|
327
|
+
[
|
|
328
|
+
gcloud_path,
|
|
329
|
+
"auth",
|
|
330
|
+
"list",
|
|
331
|
+
"--filter=status:ACTIVE",
|
|
332
|
+
"--format=value(account)",
|
|
333
|
+
],
|
|
334
|
+
check=False,
|
|
335
|
+
capture_output=True,
|
|
336
|
+
text=True,
|
|
337
|
+
timeout=10,
|
|
338
|
+
)
|
|
339
|
+
except (OSError, subprocess.SubprocessError) as ex:
|
|
340
|
+
console.debug(f"Failed to query gcloud auth list: {ex}")
|
|
341
|
+
return None
|
|
342
|
+
account = result.stdout.strip().splitlines()
|
|
343
|
+
return account[0] if account else None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _request_manifest(token: str) -> tuple[str, str]:
|
|
347
|
+
"""Fetch the Dockerfile + deploy script from Reflex.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
token: The Reflex API token to authenticate with.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
A `(dockerfile, deploy_command)` tuple.
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
Exit: If the request fails or the response shape is invalid.
|
|
357
|
+
|
|
358
|
+
"""
|
|
359
|
+
import httpx
|
|
360
|
+
|
|
361
|
+
from reflex_cli.utils import hosting
|
|
362
|
+
|
|
363
|
+
url = urljoin(constants.Hosting.HOSTING_SERVICE, GCP_MANIFEST_ENDPOINT)
|
|
364
|
+
try:
|
|
365
|
+
response = httpx.get(
|
|
366
|
+
url,
|
|
367
|
+
headers=hosting.authorization_header(token),
|
|
368
|
+
timeout=constants.Hosting.TIMEOUT,
|
|
369
|
+
)
|
|
370
|
+
response.raise_for_status()
|
|
371
|
+
except httpx.HTTPStatusError as ex:
|
|
372
|
+
detail = ex.response.text
|
|
373
|
+
with contextlib.suppress(ValueError):
|
|
374
|
+
detail = ex.response.json().get("detail", detail)
|
|
375
|
+
if ex.response.status_code == 403:
|
|
376
|
+
console.error(
|
|
377
|
+
"Reflex denied the request (403). GCP Cloud Run deploys require an "
|
|
378
|
+
"Enterprise tier subscription."
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
console.error(f"Reflex rejected the manifest request: {detail}")
|
|
382
|
+
raise click.exceptions.Exit(1) from ex
|
|
383
|
+
except httpx.HTTPError as ex:
|
|
384
|
+
console.error(f"Failed to reach Reflex at {url}: {ex}")
|
|
385
|
+
raise click.exceptions.Exit(1) from ex
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
body = response.json()
|
|
389
|
+
except ValueError as ex:
|
|
390
|
+
console.error("Reflex returned a non-JSON response.")
|
|
391
|
+
raise click.exceptions.Exit(1) from ex
|
|
392
|
+
|
|
393
|
+
if not isinstance(body, dict):
|
|
394
|
+
console.error("Reflex returned an unexpected response shape.")
|
|
395
|
+
raise click.exceptions.Exit(1)
|
|
396
|
+
|
|
397
|
+
dockerfile = body.get(FIELD_DOCKERFILE)
|
|
398
|
+
deploy_command = body.get(FIELD_DEPLOY_COMMAND)
|
|
399
|
+
if not isinstance(dockerfile, str) or not dockerfile.strip():
|
|
400
|
+
console.error(
|
|
401
|
+
f"Reflex response is missing a non-empty {FIELD_DOCKERFILE!r} field."
|
|
402
|
+
)
|
|
403
|
+
raise click.exceptions.Exit(1)
|
|
404
|
+
if not isinstance(deploy_command, str) or not deploy_command.strip():
|
|
405
|
+
console.error(
|
|
406
|
+
f"Reflex response is missing a non-empty {FIELD_DEPLOY_COMMAND!r} field."
|
|
407
|
+
)
|
|
408
|
+
raise click.exceptions.Exit(1)
|
|
409
|
+
|
|
410
|
+
return dockerfile, deploy_command
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _build_cloudbuild_yaml(dockerfile_contents: str) -> str:
|
|
414
|
+
r"""Generate a Cloud Build config that materializes the Dockerfile inline.
|
|
415
|
+
|
|
416
|
+
The Dockerfile body is dropped into a bash heredoc (``cat <<'MARKER' >
|
|
417
|
+
Dockerfile``) inside the build step. The marker is single-quoted so bash
|
|
418
|
+
treats the body literally — no shell-meta expansion of ``$``, `` ` ``, or
|
|
419
|
+
``\``. YAML literal-block indentation gets stripped uniformly so the
|
|
420
|
+
closing marker line ends up at column 0 where bash expects it.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
dockerfile_contents: The Dockerfile body from Reflex.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
A complete ``cloudbuild.yaml`` body, ready to write to disk.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ValueError: If the Dockerfile contains a line that exactly matches the
|
|
430
|
+
heredoc marker (would terminate the heredoc early).
|
|
431
|
+
|
|
432
|
+
"""
|
|
433
|
+
marker = "REFLEX_DOCKERFILE_EOF"
|
|
434
|
+
if any(line.rstrip() == marker for line in dockerfile_contents.splitlines()):
|
|
435
|
+
raise ValueError(
|
|
436
|
+
f"Dockerfile content contains the reserved heredoc marker {marker!r}."
|
|
437
|
+
)
|
|
438
|
+
# Cloud Build runs its own substitution pass over `args`, so any `$NAME` or
|
|
439
|
+
# `${NAME}` in the Dockerfile (e.g. `ENV PATH="${UV_PROJECT_ENVIRONMENT}/bin"`)
|
|
440
|
+
# would be treated as a Cloud Build variable and fail with
|
|
441
|
+
# "not a valid built-in substitution". Escape literal `$` to `$$` so the
|
|
442
|
+
# parser restores `$` before bash runs.
|
|
443
|
+
escaped = dockerfile_contents.replace("$", "$$")
|
|
444
|
+
# 6 spaces to fit inside the YAML literal block under `args:\n - -c\n - |`.
|
|
445
|
+
indent = " "
|
|
446
|
+
body = "".join(f"{indent}{line}\n" for line in escaped.splitlines())
|
|
447
|
+
return (
|
|
448
|
+
"steps:\n"
|
|
449
|
+
"- name: gcr.io/cloud-builders/docker\n"
|
|
450
|
+
" entrypoint: bash\n"
|
|
451
|
+
" args:\n"
|
|
452
|
+
" - -c\n"
|
|
453
|
+
" - |\n"
|
|
454
|
+
f"{indent}cat > Dockerfile <<'{marker}'\n"
|
|
455
|
+
f"{body}"
|
|
456
|
+
f"{indent}{marker}\n"
|
|
457
|
+
f'{indent}docker build -t "$_IMAGE" .\n'
|
|
458
|
+
f'{indent}docker push "$_IMAGE"\n'
|
|
459
|
+
"images:\n"
|
|
460
|
+
" - $_IMAGE\n"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _rewrite_builds_submit(script: str) -> str:
|
|
465
|
+
"""Rewrite the Reflex script's `gcloud builds submit` invocation to use --config=.
|
|
466
|
+
|
|
467
|
+
Replaces the (possibly multi-line) ``gcloud builds submit --tag X .``
|
|
468
|
+
command with one that references our generated cloudbuild.yaml via the
|
|
469
|
+
``REFLEX_CLOUDBUILD_YAML`` environment variable and passes the image tag
|
|
470
|
+
through ``--substitutions=_IMAGE=...``.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
script: The Reflex deploy script body.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
The script with the build-submit step rewritten.
|
|
477
|
+
|
|
478
|
+
Raises:
|
|
479
|
+
ValueError: If `gcloud builds submit` cannot be located in the script.
|
|
480
|
+
|
|
481
|
+
"""
|
|
482
|
+
match = _BUILDS_SUBMIT_PATTERN.search(script)
|
|
483
|
+
if not match:
|
|
484
|
+
raise ValueError(
|
|
485
|
+
"Couldn't find `gcloud builds submit` in the deploy script. The "
|
|
486
|
+
"manifest format may have changed; Contact support@reflex.dev"
|
|
487
|
+
)
|
|
488
|
+
indent = match.group("indent")
|
|
489
|
+
line_start = script.rfind("\n", 0, match.start()) + 1
|
|
490
|
+
# Consume continuation lines (trailing backslash) until we hit a final line.
|
|
491
|
+
cursor = match.end()
|
|
492
|
+
while True:
|
|
493
|
+
nl = script.find("\n", cursor)
|
|
494
|
+
if nl == -1:
|
|
495
|
+
cmd_end = len(script)
|
|
496
|
+
break
|
|
497
|
+
if not script[cursor:nl].rstrip().endswith("\\"):
|
|
498
|
+
cmd_end = nl
|
|
499
|
+
break
|
|
500
|
+
cursor = nl + 1
|
|
501
|
+
|
|
502
|
+
replacement = (
|
|
503
|
+
f"{indent}gcloud builds submit \\\n"
|
|
504
|
+
f'{indent} --config="${{{ENV_REFLEX_CLOUDBUILD_YAML}}}" \\\n'
|
|
505
|
+
f'{indent} --substitutions=_IMAGE="${{IMAGE}}" \\\n'
|
|
506
|
+
f'{indent} --project "${{GCP_PROJECT}}" \\\n'
|
|
507
|
+
f"{indent} ."
|
|
508
|
+
)
|
|
509
|
+
return script[:line_start] + replacement + script[cmd_end:]
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@contextlib.contextmanager
|
|
513
|
+
def _temp_cloudbuild_yaml(contents: str):
|
|
514
|
+
"""Write a cloudbuild.yaml to a tempfile and yield its path; always clean up.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
contents: The cloudbuild.yaml body to write.
|
|
518
|
+
|
|
519
|
+
Yields:
|
|
520
|
+
The path to the written tempfile.
|
|
521
|
+
|
|
522
|
+
"""
|
|
523
|
+
fd, path_str = tempfile.mkstemp(prefix="reflex-cloudbuild-", suffix=".yaml")
|
|
524
|
+
path = Path(path_str)
|
|
525
|
+
try:
|
|
526
|
+
with os.fdopen(fd, "w") as fh:
|
|
527
|
+
fh.write(contents)
|
|
528
|
+
yield path
|
|
529
|
+
finally:
|
|
530
|
+
with contextlib.suppress(FileNotFoundError):
|
|
531
|
+
path.unlink()
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _run_deploy_script(
|
|
535
|
+
bash_path: str,
|
|
536
|
+
script: str,
|
|
537
|
+
cwd: Path,
|
|
538
|
+
env_overrides: dict[str, str],
|
|
539
|
+
) -> int:
|
|
540
|
+
"""Run the bash deploy script, streaming output to the user's terminal.
|
|
541
|
+
|
|
542
|
+
The script's environment is restricted to ``DEPLOY_ENV_ALLOWLIST`` (plus the
|
|
543
|
+
explicit ``env_overrides``) so unrelated host secrets like ``AWS_*`` or
|
|
544
|
+
``GITHUB_TOKEN`` cannot be exfiltrated by a tampered or compromised manifest.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
bash_path: Resolved path to the bash executable.
|
|
548
|
+
script: The bash script body received from Reflex.
|
|
549
|
+
cwd: Working directory to run the script in.
|
|
550
|
+
env_overrides: Environment variables required by the deploy script.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
The exit code of the bash process.
|
|
554
|
+
|
|
555
|
+
"""
|
|
556
|
+
env = {
|
|
557
|
+
name: value
|
|
558
|
+
for name, value in os.environ.items()
|
|
559
|
+
if name in DEPLOY_ENV_ALLOWLIST
|
|
560
|
+
}
|
|
561
|
+
env.update(env_overrides)
|
|
562
|
+
try:
|
|
563
|
+
result = subprocess.run(
|
|
564
|
+
[bash_path, "-s"],
|
|
565
|
+
input=script,
|
|
566
|
+
text=True,
|
|
567
|
+
cwd=cwd,
|
|
568
|
+
env=env,
|
|
569
|
+
check=False,
|
|
570
|
+
stdout=sys.stdout,
|
|
571
|
+
stderr=sys.stderr,
|
|
572
|
+
)
|
|
573
|
+
except OSError as ex:
|
|
574
|
+
console.error(f"Failed to launch bash: {ex}")
|
|
575
|
+
return 1
|
|
576
|
+
return result.returncode
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/compiler.py
RENAMED
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/constants/hosting.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/dependency.py
RENAMED
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/utils/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{reflex_hosting_cli-0.1.63 → reflex_hosting_cli-0.1.65a1}/src/reflex_cli/v2/vmtypes_regions.py
RENAMED
|
File without changes
|