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 +1 -0
- reflex_cli/cli.py +362 -0
- reflex_cli/constants/__init__.py +8 -0
- reflex_cli/constants/base.py +47 -0
- reflex_cli/constants/compiler.py +31 -0
- reflex_cli/constants/hosting.py +45 -0
- reflex_cli/deployments.py +354 -0
- reflex_cli/utils/__init__.py +1 -0
- reflex_cli/utils/console.py +155 -0
- reflex_cli/utils/dependency.py +132 -0
- reflex_cli/utils/hosting.py +1908 -0
- reflex_hosting_cli-0.1.13.dist-info/LICENSE +201 -0
- reflex_hosting_cli-0.1.13.dist-info/METADATA +35 -0
- reflex_hosting_cli-0.1.13.dist-info/RECORD +15 -0
- reflex_hosting_cli-0.1.13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""The Hosting CLI deployments sub-commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from tabulate import tabulate
|
|
10
|
+
|
|
11
|
+
from reflex_cli import constants
|
|
12
|
+
from reflex_cli.utils import console
|
|
13
|
+
|
|
14
|
+
deployments_cli = typer.Typer()
|
|
15
|
+
|
|
16
|
+
TIME_FORMAT_HELP = "Accepts ISO 8601 format, unix epoch or time relative to now. For time relative to now, use the format: <d><unit>. Valid units are d (day), h (hour), m (minute), s (second). For example, 1d for 1 day ago from now."
|
|
17
|
+
MIN_LOGS_LIMIT = 50
|
|
18
|
+
MAX_LOGS_LIMIT = 1000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@deployments_cli.command(name="list")
|
|
22
|
+
def list_deployments(
|
|
23
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
24
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
25
|
+
),
|
|
26
|
+
as_json: bool = typer.Option(
|
|
27
|
+
False, "-j", "--json", help="Whether to output the result in json format."
|
|
28
|
+
),
|
|
29
|
+
):
|
|
30
|
+
"""List all the hosted deployments of the authenticated user.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
loglevel: The log level to use.
|
|
34
|
+
as_json: Whether to output the result in json format.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
Exit: If the command fails.
|
|
38
|
+
"""
|
|
39
|
+
from reflex_cli.utils import hosting
|
|
40
|
+
|
|
41
|
+
console.set_log_level(loglevel)
|
|
42
|
+
try:
|
|
43
|
+
deployments = hosting.list_deployments()
|
|
44
|
+
except Exception as ex:
|
|
45
|
+
console.error(f"Unable to list deployments")
|
|
46
|
+
raise typer.Exit(1) from ex
|
|
47
|
+
|
|
48
|
+
if as_json:
|
|
49
|
+
console.print(json.dumps(deployments))
|
|
50
|
+
return
|
|
51
|
+
if deployments:
|
|
52
|
+
headers = list(deployments[0].keys())
|
|
53
|
+
table = [list(deployment.values()) for deployment in deployments]
|
|
54
|
+
console.print(tabulate(table, headers=headers))
|
|
55
|
+
else:
|
|
56
|
+
# If returned empty list, print the empty
|
|
57
|
+
console.print(str(deployments))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@deployments_cli.command(name="delete")
|
|
61
|
+
def delete_deployment(
|
|
62
|
+
key: str = typer.Argument(..., help="The name of the deployment."),
|
|
63
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
64
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
65
|
+
),
|
|
66
|
+
):
|
|
67
|
+
"""Delete a hosted instance.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
key: The name of the deployment.
|
|
71
|
+
loglevel: The log level to use.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
Exit: If the command fails.
|
|
75
|
+
"""
|
|
76
|
+
from reflex_cli.utils import hosting
|
|
77
|
+
|
|
78
|
+
console.set_log_level(loglevel)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
hosting.delete_deployment(key)
|
|
82
|
+
except Exception as ex:
|
|
83
|
+
console.error(f"Unable to delete deployment")
|
|
84
|
+
raise typer.Exit(1) from ex
|
|
85
|
+
console.print(f"Successfully deleted [ {key} ].")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@deployments_cli.command(name="status")
|
|
89
|
+
def get_deployment_status(
|
|
90
|
+
key: str = typer.Argument(..., help="The name of the deployment."),
|
|
91
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
92
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
93
|
+
),
|
|
94
|
+
):
|
|
95
|
+
"""Check the status of a deployment.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
key: The name of the deployment.
|
|
99
|
+
loglevel: The log level to use.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
Exit: If the command fails.
|
|
103
|
+
"""
|
|
104
|
+
from reflex_cli.utils import hosting
|
|
105
|
+
|
|
106
|
+
console.set_log_level(loglevel)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
console.print(f"Getting status for [ {key} ] ...\n")
|
|
110
|
+
status = hosting.get_deployment_status(key)
|
|
111
|
+
|
|
112
|
+
# TODO: refactor all these tabulate calls
|
|
113
|
+
status.backend.updated_at = hosting.convert_to_local_time_str(
|
|
114
|
+
status.backend.updated_at or "N/A"
|
|
115
|
+
)
|
|
116
|
+
backend_status = status.backend.dict(exclude_none=True)
|
|
117
|
+
headers = list(backend_status.keys())
|
|
118
|
+
table = list(backend_status.values())
|
|
119
|
+
console.print(tabulate([table], headers=headers))
|
|
120
|
+
# Add a new line in console
|
|
121
|
+
console.print("\n")
|
|
122
|
+
status.frontend.updated_at = hosting.convert_to_local_time_str(
|
|
123
|
+
status.frontend.updated_at or "N/A"
|
|
124
|
+
)
|
|
125
|
+
frontend_status = status.frontend.dict(exclude_none=True)
|
|
126
|
+
headers = list(frontend_status.keys())
|
|
127
|
+
table = list(frontend_status.values())
|
|
128
|
+
console.print(tabulate([table], headers=headers))
|
|
129
|
+
except Exception as ex:
|
|
130
|
+
console.error(f"Unable to get deployment status")
|
|
131
|
+
raise typer.Exit(1) from ex
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _process_command_options_timestamps_limit(
|
|
135
|
+
from_timestamp, to_timestamp, limit
|
|
136
|
+
) -> Tuple[Optional[str], Optional[str], Optional[int]]:
|
|
137
|
+
"""Helper function to process/sanity check the command options for timestamps and limit.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
from_timestamp: The start time of the logs.
|
|
141
|
+
to_timestamp: The end time of the logs.
|
|
142
|
+
limit: The maximum number of logs to return.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
Exit: If the command options format/value is invalid.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The processed timestamps and limit.
|
|
149
|
+
"""
|
|
150
|
+
from reflex_cli.utils import hosting
|
|
151
|
+
|
|
152
|
+
command_timestamp = datetime.now().astimezone()
|
|
153
|
+
if (
|
|
154
|
+
from_timestamp is not None
|
|
155
|
+
and (
|
|
156
|
+
from_timestamp := hosting.process_user_entered_timestamp(
|
|
157
|
+
from_timestamp, command_timestamp
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
is None
|
|
161
|
+
):
|
|
162
|
+
console.error("Unable to process --from timestamp.")
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
to_timestamp is not None
|
|
167
|
+
and (
|
|
168
|
+
to_timestamp := hosting.process_user_entered_timestamp(
|
|
169
|
+
to_timestamp, command_timestamp
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
is None
|
|
173
|
+
):
|
|
174
|
+
console.error("Unable to process --to timestamp.")
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
if limit is not None and not MIN_LOGS_LIMIT <= limit <= MAX_LOGS_LIMIT:
|
|
178
|
+
console.error(f"Limit must be between {MIN_LOGS_LIMIT} and {MAX_LOGS_LIMIT}.")
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
|
|
181
|
+
return from_timestamp, to_timestamp, limit
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@deployments_cli.command(name="logs")
|
|
185
|
+
def get_deployment_logs(
|
|
186
|
+
key: str = typer.Argument(..., help="The name of the deployment."),
|
|
187
|
+
from_timestamp: Optional[str] = typer.Option(
|
|
188
|
+
None,
|
|
189
|
+
"--from",
|
|
190
|
+
help=f"The start time of the logs. {TIME_FORMAT_HELP}",
|
|
191
|
+
),
|
|
192
|
+
to_timestamp: Optional[str] = typer.Option(
|
|
193
|
+
None, "--to", help=f"The end time of the logs. {TIME_FORMAT_HELP}"
|
|
194
|
+
),
|
|
195
|
+
limit: Optional[int] = typer.Option(
|
|
196
|
+
None,
|
|
197
|
+
"--limit",
|
|
198
|
+
help=f"The number of logs to return. The acceptable range is {MIN_LOGS_LIMIT}-{MAX_LOGS_LIMIT}.",
|
|
199
|
+
),
|
|
200
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
201
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
202
|
+
),
|
|
203
|
+
):
|
|
204
|
+
"""Get the logs for a deployment.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
key: The name of the deployment.
|
|
208
|
+
from_timestamp: The start time of the logs.
|
|
209
|
+
to_timestamp: The end time of the logs.
|
|
210
|
+
limit: The maximum number of logs to return.
|
|
211
|
+
loglevel: The log level to use.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
Exit: If the command fails.
|
|
215
|
+
"""
|
|
216
|
+
from reflex_cli.utils import hosting
|
|
217
|
+
|
|
218
|
+
console.set_log_level(loglevel)
|
|
219
|
+
|
|
220
|
+
from_timestamp, to_timestamp, limit = _process_command_options_timestamps_limit(
|
|
221
|
+
from_timestamp, to_timestamp, limit
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
console.print("Note: there is a few seconds delay for logs to be available.")
|
|
225
|
+
# This is a case where it is not streaming logs
|
|
226
|
+
if to_timestamp is not None or limit is not None:
|
|
227
|
+
hosting.get_logs(
|
|
228
|
+
key=key,
|
|
229
|
+
log_type=hosting.LogType.APP_LOG,
|
|
230
|
+
from_iso_timestamp=from_timestamp,
|
|
231
|
+
to_iso_timestamp=to_timestamp,
|
|
232
|
+
limit=limit,
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
try:
|
|
236
|
+
asyncio.get_event_loop().run_until_complete(
|
|
237
|
+
hosting.stream_logs(key, from_iso_timestamp=from_timestamp)
|
|
238
|
+
)
|
|
239
|
+
except Exception as ex:
|
|
240
|
+
console.error(f"Unable to get deployment logs")
|
|
241
|
+
raise typer.Exit(1) from ex
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@deployments_cli.command(name="build-logs")
|
|
245
|
+
def get_deployment_build_logs(
|
|
246
|
+
key: str = typer.Argument(..., help="The name of the deployment."),
|
|
247
|
+
from_timestamp: Optional[str] = typer.Option(
|
|
248
|
+
None,
|
|
249
|
+
"--from",
|
|
250
|
+
help=f"The start time of the logs. {TIME_FORMAT_HELP}",
|
|
251
|
+
),
|
|
252
|
+
to_timestamp: Optional[str] = typer.Option(
|
|
253
|
+
None, "--to", help=f"The end time of the logs. {TIME_FORMAT_HELP}"
|
|
254
|
+
),
|
|
255
|
+
limit: Optional[int] = typer.Option(
|
|
256
|
+
None,
|
|
257
|
+
"--limit",
|
|
258
|
+
help=f"The number of logs to return. The acceptable range is {MIN_LOGS_LIMIT}-{MAX_LOGS_LIMIT}.",
|
|
259
|
+
),
|
|
260
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
261
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
262
|
+
),
|
|
263
|
+
):
|
|
264
|
+
"""Get the build logs for a deployment.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
key: The name of the deployment.
|
|
268
|
+
from_timestamp: The start time of the logs.
|
|
269
|
+
to_timestamp: The end time of the logs.
|
|
270
|
+
limit: The maximum number of logs to return.
|
|
271
|
+
loglevel: The log level to use.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
Exit: If the command fails.
|
|
275
|
+
"""
|
|
276
|
+
from reflex_cli.utils import hosting
|
|
277
|
+
|
|
278
|
+
console.set_log_level(loglevel)
|
|
279
|
+
|
|
280
|
+
from_timestamp, to_timestamp, limit = _process_command_options_timestamps_limit(
|
|
281
|
+
from_timestamp, to_timestamp, limit
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
console.print("Note: there is a few seconds delay for logs to be available.")
|
|
285
|
+
# This is a case where it is not streaming logs
|
|
286
|
+
if to_timestamp is not None or limit is not None:
|
|
287
|
+
hosting.get_logs(
|
|
288
|
+
key=key,
|
|
289
|
+
log_type=hosting.LogType.BUILD_LOG,
|
|
290
|
+
from_iso_timestamp=from_timestamp,
|
|
291
|
+
to_iso_timestamp=to_timestamp,
|
|
292
|
+
limit=limit,
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
try:
|
|
296
|
+
# TODO: we need to find a way not to fetch logs
|
|
297
|
+
# that match the deployed app name but not previously of a different owner
|
|
298
|
+
# This should not happen often
|
|
299
|
+
asyncio.run(hosting.stream_logs(key, log_type=hosting.LogType.BUILD_LOG))
|
|
300
|
+
except Exception as ex:
|
|
301
|
+
console.error(f"Unable to get deployment logs")
|
|
302
|
+
raise typer.Exit(1) from ex
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@deployments_cli.command(name="regions")
|
|
306
|
+
def get_deployment_regions(
|
|
307
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
308
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
309
|
+
),
|
|
310
|
+
as_json: bool = typer.Option(
|
|
311
|
+
False, "-j", "--json", help="Whether to output the result in json format."
|
|
312
|
+
),
|
|
313
|
+
):
|
|
314
|
+
"""List all the regions of the hosting service.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
loglevel: The log level to use.
|
|
318
|
+
as_json: Whether to output the result in json format.
|
|
319
|
+
"""
|
|
320
|
+
from reflex_cli.utils import hosting
|
|
321
|
+
|
|
322
|
+
console.set_log_level(loglevel)
|
|
323
|
+
|
|
324
|
+
list_regions_info = hosting.get_regions()
|
|
325
|
+
if as_json:
|
|
326
|
+
console.print(json.dumps(list_regions_info))
|
|
327
|
+
return
|
|
328
|
+
if list_regions_info:
|
|
329
|
+
headers = list(list_regions_info[0].keys())
|
|
330
|
+
table = [list(deployment.values()) for deployment in list_regions_info]
|
|
331
|
+
console.print(tabulate(table, headers=headers))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@deployments_cli.command(name="share")
|
|
335
|
+
def share_deployment(
|
|
336
|
+
url: Optional[str] = typer.Option(
|
|
337
|
+
None,
|
|
338
|
+
help="The URL of the deployed app to share. If not provided, the latest deployment is shared.",
|
|
339
|
+
),
|
|
340
|
+
loglevel: constants.LogLevel = typer.Option(
|
|
341
|
+
constants.LogLevel.INFO, help="The log level to use."
|
|
342
|
+
),
|
|
343
|
+
):
|
|
344
|
+
"""Share a deployment.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
url: The URL of the deployed app to share.
|
|
348
|
+
loglevel: The log level to use.
|
|
349
|
+
"""
|
|
350
|
+
from reflex_cli.utils import hosting
|
|
351
|
+
|
|
352
|
+
console.set_log_level(loglevel)
|
|
353
|
+
|
|
354
|
+
hosting.collect_deployment_info_interactive(demo_url=url)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reflex utilities."""
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Functions to communicate to the user via console."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
|
|
9
|
+
from reflex_cli.constants import LogLevel
|
|
10
|
+
|
|
11
|
+
# Console for pretty printing.
|
|
12
|
+
_console = Console()
|
|
13
|
+
|
|
14
|
+
# The current log level.
|
|
15
|
+
_LOG_LEVEL = LogLevel.INFO
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_log_level(log_level: LogLevel):
|
|
19
|
+
"""Set the log level.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
log_level: The log level to set.
|
|
23
|
+
"""
|
|
24
|
+
global _LOG_LEVEL
|
|
25
|
+
_LOG_LEVEL = log_level
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print(msg: str, **kwargs):
|
|
29
|
+
"""Print a message.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
msg: The message to print.
|
|
33
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
34
|
+
"""
|
|
35
|
+
_console.print(msg, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def debug(msg: str, **kwargs):
|
|
39
|
+
"""Print a debug message.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
msg: The debug message.
|
|
43
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
44
|
+
"""
|
|
45
|
+
if _LOG_LEVEL <= LogLevel.DEBUG:
|
|
46
|
+
print(f"[blue]Debug: {msg}[/blue]", **kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def info(msg: str, **kwargs):
|
|
50
|
+
"""Print an info message.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
msg: The info message.
|
|
54
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
55
|
+
"""
|
|
56
|
+
if _LOG_LEVEL <= LogLevel.INFO:
|
|
57
|
+
print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def success(msg: str, **kwargs):
|
|
61
|
+
"""Print a success message.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
msg: The success message.
|
|
65
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
66
|
+
"""
|
|
67
|
+
if _LOG_LEVEL <= LogLevel.INFO:
|
|
68
|
+
print(f"[green]Success: {msg}[/green]", **kwargs)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def log(msg: str, **kwargs):
|
|
72
|
+
"""Takes a string and logs it to the console.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
msg: The message to log.
|
|
76
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
77
|
+
"""
|
|
78
|
+
if _LOG_LEVEL <= LogLevel.INFO:
|
|
79
|
+
_console.log(msg, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def rule(title: str, **kwargs):
|
|
83
|
+
"""Prints a horizontal rule with a title.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
title: The title of the rule.
|
|
87
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
88
|
+
"""
|
|
89
|
+
_console.rule(title, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def warn(msg: str, **kwargs):
|
|
93
|
+
"""Print a warning message.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
msg: The warning message.
|
|
97
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
98
|
+
"""
|
|
99
|
+
if _LOG_LEVEL <= LogLevel.WARNING:
|
|
100
|
+
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def error(msg: str, **kwargs):
|
|
104
|
+
"""Print an error message.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
msg: The error message.
|
|
108
|
+
kwargs: Keyword arguments to pass to the print function.
|
|
109
|
+
"""
|
|
110
|
+
if _LOG_LEVEL <= LogLevel.ERROR:
|
|
111
|
+
print(f"[red]{msg}[/red]", **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ask(
|
|
115
|
+
question: str, choices: list[str] | None = None, default: str | None = None
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Takes a prompt question and optionally a list of choices
|
|
118
|
+
and returns the user input.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
question: The question to ask the user.
|
|
122
|
+
choices: A list of choices to select from.
|
|
123
|
+
default: The default option selected.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A string with the user input.
|
|
127
|
+
"""
|
|
128
|
+
return Prompt.ask(question, choices=choices, default=default) # type: ignore
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def progress():
|
|
132
|
+
"""Create a new progress bar.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A new progress bar.
|
|
137
|
+
"""
|
|
138
|
+
return Progress(
|
|
139
|
+
*Progress.get_default_columns()[:-1],
|
|
140
|
+
MofNCompleteColumn(),
|
|
141
|
+
TimeElapsedColumn(),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def status(*args, **kwargs):
|
|
146
|
+
"""Create a status with a spinner.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
*args: Args to pass to the status.
|
|
150
|
+
**kwargs: Kwargs to pass to the status.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A new status.
|
|
154
|
+
"""
|
|
155
|
+
return _console.status(*args, **kwargs)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Building the app and initializing all prerequisites."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import charset_normalizer
|
|
12
|
+
|
|
13
|
+
from reflex_cli import constants
|
|
14
|
+
from reflex_cli.utils import console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def detect_encoding(filename: str) -> str | None:
|
|
18
|
+
"""Detect the encoding of the given file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filename: The file to detect encoding for.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
FileNotFoundError: If the file `filename` does not exist.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The encoding of the file if file exits and encoding is detected, otherwise None.
|
|
28
|
+
"""
|
|
29
|
+
if not os.path.exists(filename):
|
|
30
|
+
raise FileNotFoundError
|
|
31
|
+
# Detect the encoding of the original file
|
|
32
|
+
charset_matches = charset_normalizer.from_path(filename)
|
|
33
|
+
maybe_charset_match = charset_matches.best()
|
|
34
|
+
if maybe_charset_match is None:
|
|
35
|
+
console.warn(
|
|
36
|
+
f"Unable to detect encoding of {constants.RequirementsTxt.FILE} to check requirements. Please manually update it if required.."
|
|
37
|
+
)
|
|
38
|
+
return None
|
|
39
|
+
encoding = maybe_charset_match.encoding
|
|
40
|
+
console.debug(
|
|
41
|
+
f"Detected encoding for {constants.RequirementsTxt.FILE} as {encoding}."
|
|
42
|
+
)
|
|
43
|
+
return encoding
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_requirements():
|
|
47
|
+
"""Check if the requirements.txt needs update based on current environment.
|
|
48
|
+
Throw warnings if too many installed or unused (based on imports) packages in
|
|
49
|
+
the local environment.
|
|
50
|
+
"""
|
|
51
|
+
# First check the encoding of requirements.txt if applicable. If unable to determine encoding
|
|
52
|
+
# will not proceed to check for requirement updates.
|
|
53
|
+
encoding = "utf-8"
|
|
54
|
+
if (
|
|
55
|
+
os.path.exists(constants.RequirementsTxt.FILE)
|
|
56
|
+
and (encoding := detect_encoding(constants.RequirementsTxt.FILE)) is None
|
|
57
|
+
):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Run the pipdeptree command and get the output
|
|
61
|
+
try:
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
[sys.executable, "-m", "pipdeptree", "--warn", "silence"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
check=True,
|
|
67
|
+
)
|
|
68
|
+
except subprocess.CalledProcessError as cpe:
|
|
69
|
+
console.debug(f"Unable to run pipdeptree util in subprocess: {cpe}")
|
|
70
|
+
console.warn(
|
|
71
|
+
"Unable to detect installed packages in your environment. Please make sure your requirements.txt is up to date."
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Filter the output lines using a regular expression
|
|
76
|
+
lines = result.stdout.split("\n")
|
|
77
|
+
new_requirements_lines = []
|
|
78
|
+
for line in lines:
|
|
79
|
+
if re.match(r"^\w+", line):
|
|
80
|
+
# Special handling of psycopg2, force the binary version instead
|
|
81
|
+
if line.startswith("psycopg2=="):
|
|
82
|
+
line = line.replace("psycopg2==", "psycopg2-binary==")
|
|
83
|
+
new_requirements_lines.append(f"{line}\n")
|
|
84
|
+
|
|
85
|
+
current_requirements_lines = ""
|
|
86
|
+
if os.path.exists(constants.RequirementsTxt.FILE):
|
|
87
|
+
with open(constants.RequirementsTxt.FILE, "r", encoding=encoding) as f:
|
|
88
|
+
current_requirements_lines = list(f)
|
|
89
|
+
console.debug("Current requirements.txt:")
|
|
90
|
+
console.debug("".join(current_requirements_lines))
|
|
91
|
+
|
|
92
|
+
# Show the differences of current and the newly generated requirements.txt
|
|
93
|
+
diff_content = "".join(
|
|
94
|
+
difflib.unified_diff(
|
|
95
|
+
current_requirements_lines,
|
|
96
|
+
new_requirements_lines,
|
|
97
|
+
fromfile="requirements.txt",
|
|
98
|
+
tofile="new_requirements.txt",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not diff_content:
|
|
103
|
+
console.info("No updates required for the requirements.txt.")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if not current_requirements_lines:
|
|
107
|
+
console.info("It seems like there's no requirements.txt in your project.")
|
|
108
|
+
else:
|
|
109
|
+
console.info("The requirements.txt may need to be updated.")
|
|
110
|
+
|
|
111
|
+
console.print(diff_content)
|
|
112
|
+
|
|
113
|
+
user_choice = console.ask(
|
|
114
|
+
"Would you like to update requirements.txt based on the changes above?",
|
|
115
|
+
choices=["y", "n"],
|
|
116
|
+
)
|
|
117
|
+
if user_choice != "y":
|
|
118
|
+
console.warn("Please update requirements.txt manually if needed.")
|
|
119
|
+
# Not exit here since the newly generated requirements.txt is necessarily correct
|
|
120
|
+
# i.e., user enters `n` to override and proceed to deploy
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Write the filtered lines to requirements.txt
|
|
124
|
+
try:
|
|
125
|
+
with open(constants.RequirementsTxt.FILE, "w", encoding=encoding) as f:
|
|
126
|
+
f.writelines(new_requirements_lines)
|
|
127
|
+
console.info("requirements.txt updated.")
|
|
128
|
+
except OSError:
|
|
129
|
+
console.error(
|
|
130
|
+
f"Unable to write to {constants.RequirementsTxt.FILE}. Please manually update it."
|
|
131
|
+
)
|
|
132
|
+
sys.exit(1)
|